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>
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
"""
|
||||
Tests for gitignore filtering functionality.
|
||||
"""
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored
|
||||
from src.domains.tools.file.glob import GlobFilesTool
|
||||
from src.domains.tools.search.grep import GrepContentTool
|
||||
|
||||
|
||||
class TestGitignoreFilter:
|
||||
"""Tests for GitignoreFilter class."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir_with_gitignore(self):
|
||||
"""Create a temporary directory with a .gitignore file."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir)
|
||||
|
||||
# Create .gitignore
|
||||
(path / ".gitignore").write_text("""
|
||||
# Python artifacts
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
# Custom patterns
|
||||
ignored_file.txt
|
||||
ignored_dir/
|
||||
""")
|
||||
|
||||
# Create various files and directories
|
||||
(path / "main.py").write_text("# Main file")
|
||||
(path / "test.pyc").write_bytes(b"compiled")
|
||||
(path / "ignored_file.txt").write_text("should be ignored")
|
||||
(path / "not_ignored.txt").write_text("should be visible")
|
||||
|
||||
# Create directories
|
||||
(path / "__pycache__").mkdir()
|
||||
(path / "__pycache__" / "module.cpython-312.pyc").write_bytes(b"cache")
|
||||
|
||||
(path / ".venv").mkdir()
|
||||
(path / ".venv" / "lib").mkdir(parents=True)
|
||||
(path / ".venv" / "lib" / "python.py").write_text("venv file")
|
||||
|
||||
(path / "ignored_dir").mkdir()
|
||||
(path / "ignored_dir" / "hidden.py").write_text("hidden")
|
||||
|
||||
(path / "src").mkdir()
|
||||
(path / "src" / "app.py").write_text("# App")
|
||||
(path / "src" / "utils.py").write_text("# Utils")
|
||||
|
||||
yield path
|
||||
|
||||
def test_filter_ignores_pyc_files(self, temp_dir_with_gitignore):
|
||||
"""Test that .pyc files are ignored."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
assert filter_instance.is_ignored(temp_dir_with_gitignore / "test.pyc")
|
||||
assert not filter_instance.is_ignored(temp_dir_with_gitignore / "main.py")
|
||||
|
||||
def test_filter_ignores_pycache_dir(self, temp_dir_with_gitignore):
|
||||
"""Test that __pycache__ directory is ignored."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
pycache = temp_dir_with_gitignore / "__pycache__"
|
||||
assert filter_instance.is_ignored(pycache)
|
||||
assert filter_instance.is_ignored(pycache / "module.cpython-312.pyc")
|
||||
|
||||
def test_filter_ignores_venv(self, temp_dir_with_gitignore):
|
||||
"""Test that .venv directory is ignored."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
venv = temp_dir_with_gitignore / ".venv"
|
||||
assert filter_instance.is_ignored(venv)
|
||||
assert filter_instance.is_ignored(venv / "lib" / "python.py")
|
||||
|
||||
def test_filter_ignores_custom_patterns(self, temp_dir_with_gitignore):
|
||||
"""Test that custom gitignore patterns work."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
assert filter_instance.is_ignored(temp_dir_with_gitignore / "ignored_file.txt")
|
||||
assert filter_instance.is_ignored(temp_dir_with_gitignore / "ignored_dir")
|
||||
assert filter_instance.is_ignored(temp_dir_with_gitignore / "ignored_dir" / "hidden.py")
|
||||
|
||||
def test_filter_allows_regular_files(self, temp_dir_with_gitignore):
|
||||
"""Test that regular files are not ignored."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
assert not filter_instance.is_ignored(temp_dir_with_gitignore / "main.py")
|
||||
assert not filter_instance.is_ignored(temp_dir_with_gitignore / "not_ignored.txt")
|
||||
assert not filter_instance.is_ignored(temp_dir_with_gitignore / "src" / "app.py")
|
||||
|
||||
def test_filter_paths_function(self, temp_dir_with_gitignore):
|
||||
"""Test the filter_paths helper function."""
|
||||
filter_instance = GitignoreFilter(temp_dir_with_gitignore)
|
||||
|
||||
paths = [
|
||||
temp_dir_with_gitignore / "main.py",
|
||||
temp_dir_with_gitignore / "test.pyc",
|
||||
temp_dir_with_gitignore / "src" / "app.py",
|
||||
temp_dir_with_gitignore / ".venv" / "lib" / "python.py",
|
||||
]
|
||||
|
||||
filtered = filter_instance.filter_paths(paths)
|
||||
|
||||
assert len(filtered) == 2
|
||||
assert temp_dir_with_gitignore / "main.py" in filtered
|
||||
assert temp_dir_with_gitignore / "src" / "app.py" in filtered
|
||||
assert temp_dir_with_gitignore / "test.pyc" not in filtered
|
||||
assert temp_dir_with_gitignore / ".venv" / "lib" / "python.py" not in filtered
|
||||
|
||||
def test_filter_gitignored_convenience(self, temp_dir_with_gitignore):
|
||||
"""Test the filter_gitignored convenience function."""
|
||||
paths = list(temp_dir_with_gitignore.rglob("*.py"))
|
||||
|
||||
filtered = filter_gitignored(paths, temp_dir_with_gitignore)
|
||||
|
||||
# Should only include main.py, src/app.py, src/utils.py
|
||||
# Should exclude .venv/lib/python.py, ignored_dir/hidden.py
|
||||
filenames = {p.name for p in filtered}
|
||||
assert "main.py" in filenames
|
||||
assert "app.py" in filenames
|
||||
assert "utils.py" in filenames
|
||||
|
||||
# Check that ignored files are not present
|
||||
ignored_paths = [str(p) for p in filtered]
|
||||
assert not any(".venv" in p for p in ignored_paths)
|
||||
assert not any("ignored_dir" in p for p in ignored_paths)
|
||||
|
||||
|
||||
class TestGlobWithGitignore:
|
||||
"""Tests for GlobFilesTool gitignore integration."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self):
|
||||
"""Create a directory with ignored and non-ignored files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir)
|
||||
|
||||
# Create .gitignore
|
||||
(path / ".gitignore").write_text("""
|
||||
*.log
|
||||
build/
|
||||
""")
|
||||
|
||||
# Create files
|
||||
(path / "main.py").write_text("# Main")
|
||||
(path / "debug.log").write_text("log content")
|
||||
(path / "src").mkdir()
|
||||
(path / "src" / "app.py").write_text("# App")
|
||||
|
||||
# Create .venv (default ignored)
|
||||
(path / ".venv").mkdir()
|
||||
(path / ".venv" / "script.py").write_text("venv")
|
||||
|
||||
# Create build directory (gitignore pattern)
|
||||
(path / "build").mkdir()
|
||||
(path / "build" / "output.py").write_text("build")
|
||||
|
||||
yield path
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_glob_honors_gitignore_by_default(self, temp_dir):
|
||||
"""Test that glob filters gitignored files by default."""
|
||||
tool = GlobFilesTool()
|
||||
|
||||
result = await tool.execute(pattern="**/*.py", path=str(temp_dir))
|
||||
|
||||
assert result.success
|
||||
assert "main.py" in result.data
|
||||
assert "app.py" in result.data
|
||||
assert ".venv" not in result.data
|
||||
assert "build" not in result.data
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_glob_filters_log_files(self, temp_dir):
|
||||
"""Test that custom gitignore patterns (*.log) work."""
|
||||
tool = GlobFilesTool()
|
||||
|
||||
result = await tool.execute(pattern="**/*", path=str(temp_dir))
|
||||
|
||||
assert result.success
|
||||
assert "debug.log" not in result.data
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_glob_can_disable_gitignore(self, temp_dir):
|
||||
"""Test that gitignore filtering can be disabled."""
|
||||
tool = GlobFilesTool(honor_gitignore=False)
|
||||
|
||||
result = await tool.execute(pattern="**/*.py", path=str(temp_dir))
|
||||
|
||||
assert result.success
|
||||
# When disabled, should include ignored files
|
||||
assert ".venv" in result.data or "build" in result.data
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_glob_override_per_call(self, temp_dir):
|
||||
"""Test per-call gitignore override."""
|
||||
tool = GlobFilesTool(honor_gitignore=True)
|
||||
|
||||
# Default behavior - filters
|
||||
result1 = await tool.execute(pattern="**/*.py", path=str(temp_dir))
|
||||
assert ".venv" not in result1.data
|
||||
|
||||
# Override to disable
|
||||
result2 = await tool.execute(
|
||||
pattern="**/*.py",
|
||||
path=str(temp_dir),
|
||||
honor_gitignore=False
|
||||
)
|
||||
assert ".venv" in result2.data or "build" in result2.data
|
||||
|
||||
|
||||
class TestGrepWithGitignore:
|
||||
"""Tests for GrepContentTool gitignore integration."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir(self):
|
||||
"""Create a directory with searchable content in ignored and non-ignored files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir)
|
||||
|
||||
# Create .gitignore
|
||||
(path / ".gitignore").write_text("""
|
||||
ignored/
|
||||
""")
|
||||
|
||||
# Create files with searchable content
|
||||
(path / "main.py").write_text("def search_target(): pass")
|
||||
(path / "src").mkdir()
|
||||
(path / "src" / "utils.py").write_text("def search_target(): # utils")
|
||||
|
||||
# Create .venv with matching content (default ignored)
|
||||
(path / ".venv").mkdir()
|
||||
(path / ".venv" / "site.py").write_text("def search_target(): # venv")
|
||||
|
||||
# Create ignored dir with matching content
|
||||
(path / "ignored").mkdir()
|
||||
(path / "ignored" / "hidden.py").write_text("def search_target(): # hidden")
|
||||
|
||||
yield path
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_grep_honors_gitignore_by_default(self, temp_dir):
|
||||
"""Test that grep filters gitignored files by default."""
|
||||
tool = GrepContentTool()
|
||||
|
||||
result = await tool.execute(pattern="search_target", path=str(temp_dir))
|
||||
|
||||
assert result.success
|
||||
assert "main.py" in result.data
|
||||
assert "utils.py" in result.data
|
||||
assert ".venv" not in result.data
|
||||
assert "ignored" not in result.data
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_grep_can_disable_gitignore(self, temp_dir):
|
||||
"""Test that gitignore filtering can be disabled."""
|
||||
tool = GrepContentTool(honor_gitignore=False)
|
||||
|
||||
result = await tool.execute(pattern="search_target", path=str(temp_dir))
|
||||
|
||||
assert result.success
|
||||
# When disabled, should include ignored files
|
||||
assert ".venv" in result.data or "ignored" in result.data
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_grep_override_per_call(self, temp_dir):
|
||||
"""Test per-call gitignore override."""
|
||||
tool = GrepContentTool(honor_gitignore=True)
|
||||
|
||||
# Default behavior - filters
|
||||
result1 = await tool.execute(pattern="search_target", path=str(temp_dir))
|
||||
assert ".venv" not in result1.data
|
||||
|
||||
# Override to disable
|
||||
result2 = await tool.execute(
|
||||
pattern="search_target",
|
||||
path=str(temp_dir),
|
||||
honor_gitignore=False
|
||||
)
|
||||
assert ".venv" in result2.data or "ignored" in result2.data
|
||||
|
||||
|
||||
class TestDefaultIgnores:
|
||||
"""Tests for default ignore patterns (no .gitignore file)."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir_no_gitignore(self):
|
||||
"""Create a directory without .gitignore but with common ignored dirs."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir)
|
||||
|
||||
# Create files
|
||||
(path / "main.py").write_text("# Main")
|
||||
|
||||
# Create commonly ignored directories
|
||||
(path / ".venv").mkdir()
|
||||
(path / ".venv" / "script.py").write_text("venv")
|
||||
|
||||
(path / "__pycache__").mkdir()
|
||||
(path / "__pycache__" / "cache.pyc").write_bytes(b"cache")
|
||||
|
||||
(path / "node_modules").mkdir()
|
||||
(path / "node_modules" / "package.js").write_text("js")
|
||||
|
||||
yield path
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_glob_ignores_defaults_without_gitignore(self, temp_dir_no_gitignore):
|
||||
"""Test that default ignores work even without .gitignore."""
|
||||
tool = GlobFilesTool()
|
||||
|
||||
result = await tool.execute(pattern="**/*", path=str(temp_dir_no_gitignore))
|
||||
|
||||
assert result.success
|
||||
assert "main.py" in result.data
|
||||
assert ".venv" not in result.data
|
||||
assert "__pycache__" not in result.data
|
||||
assert "node_modules" not in result.data
|
||||
Reference in New Issue
Block a user