from pathlib import Path import pytest from agentci.integrations.codegraph import CodeGraph, CodeGraphError class FakeProcess: def __init__(self, returncode: int = 0, stderr: bytes = b"") -> None: self.returncode = returncode self.stderr = stderr async def communicate(self) -> tuple[bytes, bytes]: return b"", self.stderr async def test_initializes_incomplete_index_and_excludes_it_from_git( tmp_path: Path, monkeypatch ) -> None: workspace = tmp_path / "repo" (workspace / ".git" / "info").mkdir(parents=True) (workspace / ".codegraph").mkdir() calls: list[tuple[object, ...]] = [] async def create_subprocess_exec(*args, **_kwargs): calls.append(args) return FakeProcess() monkeypatch.setattr( "agentci.integrations.codegraph.asyncio.create_subprocess_exec", create_subprocess_exec, ) await CodeGraph().prepare(workspace) assert calls == [("codegraph", "init", str(workspace))] assert (workspace / ".git" / "info" / "exclude").read_text() == ".codegraph/\n" async def test_syncs_an_existing_index_without_duplicating_exclude( tmp_path: Path, monkeypatch ) -> None: workspace = tmp_path / "repo" (workspace / ".git" / "info").mkdir(parents=True) (workspace / ".codegraph").mkdir() (workspace / ".codegraph" / "codegraph.db").touch() exclude = workspace / ".git" / "info" / "exclude" exclude.write_text("# local excludes\n.codegraph/\n") calls: list[tuple[object, ...]] = [] async def create_subprocess_exec(*args, **_kwargs): calls.append(args) return FakeProcess() monkeypatch.setattr( "agentci.integrations.codegraph.asyncio.create_subprocess_exec", create_subprocess_exec, ) await CodeGraph().prepare(workspace) assert calls == [("codegraph", "sync", str(workspace))] assert exclude.read_text() == "# local excludes\n.codegraph/\n" @pytest.mark.parametrize( ("initial", "expected"), [ ("*.pyc", "*.pyc\n.codegraph/\n"), ("# .codegraph/ is documented here\n", "# .codegraph/ is documented here\n.codegraph/\n"), (" .codegraph/ \n", " .codegraph/ \n"), ], ) async def test_handles_exclude_file_boundaries( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, initial: str, expected: str, ) -> None: workspace = tmp_path / "repo" exclude = workspace / ".git" / "info" / "exclude" exclude.parent.mkdir(parents=True) exclude.write_text(initial) async def create_subprocess_exec(*_args, **_kwargs): return FakeProcess() monkeypatch.setattr( "agentci.integrations.codegraph.asyncio.create_subprocess_exec", create_subprocess_exec, ) await CodeGraph().prepare(workspace) assert exclude.read_text() == expected async def test_reports_missing_executable(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: workspace = tmp_path / "repo" workspace.mkdir() async def create_subprocess_exec(*_args, **_kwargs): raise FileNotFoundError(2, "No such file or directory", "codegraph") monkeypatch.setattr( "agentci.integrations.codegraph.asyncio.create_subprocess_exec", create_subprocess_exec, ) with pytest.raises(CodeGraphError, match="Could not run CodeGraph:.*codegraph") as raised: await CodeGraph().prepare(workspace) assert isinstance(raised.value.__cause__, FileNotFoundError) assert (workspace / ".git" / "info" / "exclude").read_text() == ".codegraph/\n" async def test_reports_nonzero_exit_with_bounded_non_utf8_stderr( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: workspace = tmp_path / "repo" workspace.mkdir() stderr = b"discarded-prefix" + (b"x" * 1200) + b"\xff useful-tail" async def create_subprocess_exec(*_args, **_kwargs): return FakeProcess(returncode=7, stderr=stderr) monkeypatch.setattr( "agentci.integrations.codegraph.asyncio.create_subprocess_exec", create_subprocess_exec, ) with pytest.raises(CodeGraphError, match="codegraph init failed") as raised: await CodeGraph().prepare(workspace) message = str(raised.value) assert "discarded-prefix" not in message assert "useful-tail" in message assert len(message.removeprefix("codegraph init failed: ")) == 1000