Files
webber/webber-api/tests/test_tools.py
T
jpmschweitzerandClaude Opus 4.5 3b58fa4f8b
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 2m27s
refactor: reorganize into monorepo with separate subprojects
Structure webber into three independent subprojects:
- webber-api/: FastAPI backend server with all agent code
- webber-cli/: Standalone CLI client (renamed from cli/ to webber_cli/)
- webber-sandbox/: Test project for functional testing

Key changes:
- Each subproject has its own .venv (Python 3.12+)
- Added sandbox.sh for managing test project templates
- Created sandbox-templates/ with calculator-cli and empty starter
- Updated CI/CD for prefixed tags (api/v*, cli/v*)
- Added comprehensive AGENTS.md with operational instructions
- Added gitignore filtering to glob and grep tools
- Created pyproject.toml for each subproject

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 10:37:47 +01:00

235 lines
7.2 KiB
Python

"""
Tests for tool implementations.
"""
import tempfile
from pathlib import Path
import pytest
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.search.grep import GrepContentTool
from src.domains.tools.shell.bash import BashReadOnlyTool
class TestReadFileTool:
"""Tests for ReadFileTool."""
@pytest.fixture
def tool(self):
return ReadFileTool()
@pytest.fixture
def temp_file(self):
"""Create a temporary file with content."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
for i in range(100):
f.write(f"Line {i + 1}: This is test content\n")
f.flush()
yield Path(f.name)
Path(f.name).unlink(missing_ok=True)
@pytest.mark.anyio
async def test_read_file_success(self, tool, temp_file):
"""Test reading a file successfully."""
result = await tool.execute(file_path=str(temp_file))
assert result.success
assert "Line 1:" in result.data
assert result.metadata.get("total_lines") == 100
@pytest.mark.anyio
async def test_read_file_with_offset(self, tool, temp_file):
"""Test reading with offset."""
result = await tool.execute(file_path=str(temp_file), offset=10, limit=5)
assert result.success
assert "Line 11:" in result.data
assert result.metadata.get("lines_returned") == 5
@pytest.mark.anyio
async def test_read_file_not_found(self, tool):
"""Test reading non-existent file."""
result = await tool.execute(file_path="/nonexistent/file.txt")
assert not result.success
assert "not found" in result.error.lower()
@pytest.mark.anyio
async def test_read_file_path_restriction(self, temp_file):
"""Test path restriction enforcement."""
tool = ReadFileTool(allowed_paths=["/some/other/path"])
result = await tool.execute(file_path=str(temp_file))
assert not result.success
assert "not in allowed" in result.error.lower()
class TestGlobFilesTool:
"""Tests for GlobFilesTool."""
@pytest.fixture
def tool(self):
return GlobFilesTool()
@pytest.fixture
def temp_dir(self):
"""Create a temporary directory with files."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir)
# Create test files
(path / "file1.py").write_text("# Python file 1")
(path / "file2.py").write_text("# Python file 2")
(path / "readme.md").write_text("# Readme")
(path / "subdir").mkdir()
(path / "subdir" / "nested.py").write_text("# Nested")
yield path
@pytest.mark.anyio
async def test_glob_python_files(self, tool, temp_dir):
"""Test finding Python files."""
result = await tool.execute(pattern="**/*.py", path=str(temp_dir))
assert result.success
assert "file1.py" in result.data
assert "file2.py" in result.data
assert "nested.py" in result.data
@pytest.mark.anyio
async def test_glob_with_limit(self, tool, temp_dir):
"""Test result limiting."""
result = await tool.execute(pattern="**/*.py", path=str(temp_dir), limit=2)
assert result.success
assert result.metadata.get("returned") == 2
@pytest.mark.anyio
async def test_glob_no_matches(self, tool, temp_dir):
"""Test when no files match."""
result = await tool.execute(pattern="**/*.xyz", path=str(temp_dir))
assert result.success
assert "No files found" in result.data
class TestGrepContentTool:
"""Tests for GrepContentTool."""
@pytest.fixture
def tool(self):
return GrepContentTool()
@pytest.fixture
def temp_dir(self):
"""Create a temporary directory with searchable content."""
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir)
(path / "code.py").write_text("""
def hello_world():
print("Hello, World!")
def goodbye_world():
print("Goodbye!")
""")
(path / "config.py").write_text("""
DEBUG = True
API_KEY = "secret"
""")
yield path
@pytest.mark.anyio
async def test_grep_pattern(self, tool, temp_dir):
"""Test searching for a pattern."""
result = await tool.execute(pattern="def.*world", path=str(temp_dir))
assert result.success
assert "hello_world" in result.data
assert "goodbye_world" in result.data
@pytest.mark.anyio
async def test_grep_case_insensitive(self, tool, temp_dir):
"""Test case-insensitive search."""
result = await tool.execute(
pattern="DEBUG",
path=str(temp_dir),
case_sensitive=False
)
assert result.success
assert "DEBUG" in result.data
@pytest.mark.anyio
async def test_grep_with_file_glob(self, tool, temp_dir):
"""Test filtering by file glob."""
result = await tool.execute(
pattern="=",
path=str(temp_dir),
file_glob="config.py"
)
assert result.success
assert "config.py" in result.data
class TestBashReadOnlyTool:
"""Tests for BashReadOnlyTool."""
@pytest.fixture
def tool(self):
return BashReadOnlyTool()
@pytest.mark.anyio
async def test_ls_command(self, tool):
"""Test allowed ls command."""
result = await tool.execute(command="ls -la", cwd="/tmp")
assert result.success
@pytest.mark.anyio
async def test_pwd_command(self, tool):
"""Test allowed pwd command."""
result = await tool.execute(command="pwd", cwd="/tmp")
assert result.success
assert "/tmp" in result.data
@pytest.mark.anyio
async def test_forbidden_rm_command(self, tool):
"""Test that rm is blocked."""
result = await tool.execute(command="rm -rf /tmp/test")
assert not result.success
assert "forbidden" in result.error.lower() or "not allowed" in result.error.lower()
@pytest.mark.anyio
async def test_forbidden_redirect(self, tool):
"""Test that redirects are blocked."""
result = await tool.execute(command="echo test > /tmp/file")
assert not result.success
assert "forbidden" in result.error.lower()
@pytest.mark.anyio
async def test_forbidden_chaining(self, tool):
"""Test that command chaining is blocked."""
result = await tool.execute(command="ls && rm -rf /")
assert not result.success
@pytest.mark.anyio
async def test_git_status(self, tool, tmp_path):
"""Test git status on non-git directory."""
result = await tool.execute(command="git status", cwd=str(tmp_path))
# Should fail but not because command is forbidden
assert not result.success
assert "not a git repository" in result.error.lower() or "fatal" in result.data.lower()
@pytest.mark.anyio
async def test_forbidden_curl(self, tool):
"""Test that curl is blocked."""
result = await tool.execute(command="curl http://example.com")
assert not result.success
assert "forbidden" in result.error.lower() or "not allowed" in result.error.lower()