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>
331 lines
12 KiB
Python
331 lines
12 KiB
Python
"""
|
|
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
|