97 findings to zero. Most were mechanical — 52 unsorted import blocks, 10 unsorted __all__, assorted pyupgrade and simplify hints. Two were not, and both were visible only because the lint made me look. `webber version` did not exist. src/cli/commands/version.py defines show_version(), main.py imported it, and the registration line was never written — the CLI exposed chat and explore only. The import carried `# noqa: F401`, which is what kept the omission quiet: someone marked the symptom as intentional instead of asking why it was unused. show_version is not redundant with the --version flag; it prints the resolved Ollama URL, model and debug state, which is the form worth having when something is misconfigured. Registered, and the suppression dropped because the import is now genuinely used. test_spawn_explore_agent asserted nothing. It built a mock RunContext, patched get_agent, and stopped at the comment "For now, verify the explore agent would be called correctly". It had been counted as a passing test. An AST sweep of all 238 test functions found it was the only one, which is worth knowing — the problem was contained, not systemic. It is now skipped with a reason, so it reports as unfinished rather than as passing. Reducing it rather than deleting its imports was the point: tidying the imports would have made a hollow test look clean. Two findings were false positives, and both are recorded rather than silently worked around: B023 flagged run_agent closing over full_prompt and ctx. Traced: agent_task is awaited at line 326 before `continue` reaches the next iteration, so neither name can be rebound while the closure is pending, and the exception path cancels and awaits too. Not a bug. Bound as defaults anyway, because that stays true if the await ever moves. I had called it a live bug before tracing it, which is the mistake Rule 5 exists for. RUF012 flagged `rules: list[ApprovalRule] = []` on ApprovalRuleSet. Its suggested fix — annotate ClassVar — would remove the field from the model. ApprovalRuleSet is a pydantic model and pydantic deep-copies defaults per instance; verified by constructing two and confirming their lists are distinct objects. Suppressed with that evidence in the comment. Ruff cannot see the pydantic base because BaseSchema is a local subclass of BaseModel. Also moved a stray `from src.shared.logging import ...` that had drifted below a function definition, and merged a nested if in the ollama provider. 215 passed, 23 skipped, unchanged except for the new skip. `webber version` exercised end to end. mypy is NOT addressed here and the gate still fails on it — 55 errors in 14 files, 35 of them no-any-return from pydantic_ai's untyped returns. That was hidden behind ruff, because the gate stops at the first failing stage. Co-Authored-By: Claude <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.file.glob import GlobFilesTool
|
|
from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored
|
|
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
|