From 3c56cd31f5e143ce3f048186128586653fb3688b Mon Sep 17 00:00:00 2001 From: Mateo Wang <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:22:29 -0700 Subject: [PATCH] chore(greptile): load nested AGENTS.md files as review context scoped to their directories (#45636) * chore(greptile): load nested AGENTS.md files as review context scoped to their directories * refactor(greptile): use a stdlib dataclass so the files.json check runs without uv * test(greptile): write fixture AGENTS.md files through a helper instead of rebinding a loop variable --- .github/workflows/test-linting.yml | 6 + .greptile/files.json | 543 ++++++++++++++++++ .../check_greptile_files_json.py | 71 +++ .../test_check_greptile_files_json.py | 75 +++ 4 files changed, 695 insertions(+) create mode 100644 .greptile/files.json create mode 100644 tests/code_coverage_tests/check_greptile_files_json.py create mode 100644 tests/code_coverage_tests/test_check_greptile_files_json.py diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index 95a32a41ad6..f00e9f28516 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -593,6 +593,12 @@ jobs: - name: check_migrations_no_data_rewrites run: uv run --no-sync python ./tests/code_coverage_tests/check_migrations_no_data_rewrites.py + - name: check_greptile_files_json + run: uv run --no-sync python ./tests/code_coverage_tests/check_greptile_files_json.py + + - name: test_check_greptile_files_json + run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_check_greptile_files_json.py + - name: check_no_publicly_known_master_key run: uv run --no-sync python ./tests/code_coverage_tests/check_no_publicly_known_master_key.py diff --git a/.greptile/files.json b/.greptile/files.json new file mode 100644 index 00000000000..4b84b6cb2b9 --- /dev/null +++ b/.greptile/files.json @@ -0,0 +1,543 @@ +{ + "files": [ + { + "path": "litellm/_v2/AGENTS.md", + "description": "Conventions for code under litellm/_v2/", + "scope": [ + "litellm/_v2/**" + ] + }, + { + "path": "litellm/_v2/cache/AGENTS.md", + "description": "Conventions for code under litellm/_v2/cache/", + "scope": [ + "litellm/_v2/cache/**" + ] + }, + { + "path": "litellm/proxy/_experimental/mcp_server/AGENTS.md", + "description": "Conventions for code under litellm/proxy/_experimental/mcp_server/", + "scope": [ + "litellm/proxy/_experimental/mcp_server/**" + ] + }, + { + "path": "litellm/rust_bridge/AGENTS.md", + "description": "Conventions for code under litellm/rust_bridge/", + "scope": [ + "litellm/rust_bridge/**" + ] + }, + { + "path": "litellm/rust_bridge/trace/AGENTS.md", + "description": "Conventions for code under litellm/rust_bridge/trace/", + "scope": [ + "litellm/rust_bridge/trace/**" + ] + }, + { + "path": "litellm/tracing/AGENTS.md", + "description": "Conventions for code under litellm/tracing/", + "scope": [ + "litellm/tracing/**" + ] + }, + { + "path": "litellm-rust/AGENTS.md", + "description": "Conventions for code under litellm-rust/", + "scope": [ + "litellm-rust/**" + ] + }, + { + "path": "litellm-rust/crates/cache-response/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/cache-response/", + "scope": [ + "litellm-rust/crates/cache-response/**" + ] + }, + { + "path": "litellm-rust/crates/callbacks-legacy-python/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/callbacks-legacy-python/", + "scope": [ + "litellm-rust/crates/callbacks-legacy-python/**" + ] + }, + { + "path": "litellm-rust/crates/coroutine/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/coroutine/", + "scope": [ + "litellm-rust/crates/coroutine/**" + ] + }, + { + "path": "litellm-rust/crates/gateway/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/gateway/", + "scope": [ + "litellm-rust/crates/gateway/**" + ] + }, + { + "path": "litellm-rust/crates/gateway-auth/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/gateway-auth/", + "scope": [ + "litellm-rust/crates/gateway-auth/**" + ] + }, + { + "path": "litellm-rust/crates/gateway-inference/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/gateway-inference/", + "scope": [ + "litellm-rust/crates/gateway-inference/**" + ] + }, + { + "path": "litellm-rust/crates/gateway-mcp/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/gateway-mcp/", + "scope": [ + "litellm-rust/crates/gateway-mcp/**" + ] + }, + { + "path": "litellm-rust/crates/host/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/host/", + "scope": [ + "litellm-rust/crates/host/**" + ] + }, + { + "path": "litellm-rust/crates/host/src/machine/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/host/src/machine/", + "scope": [ + "litellm-rust/crates/host/src/machine/**" + ] + }, + { + "path": "litellm-rust/crates/host-http/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/host-http/", + "scope": [ + "litellm-rust/crates/host-http/**" + ] + }, + { + "path": "litellm-rust/crates/host-native/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/host-native/", + "scope": [ + "litellm-rust/crates/host-native/**" + ] + }, + { + "path": "litellm-rust/crates/host-python/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/host-python/", + "scope": [ + "litellm-rust/crates/host-python/**" + ] + }, + { + "path": "litellm-rust/crates/http/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/http/", + "scope": [ + "litellm-rust/crates/http/**" + ] + }, + { + "path": "litellm-rust/crates/inference/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference/", + "scope": [ + "litellm-rust/crates/inference/**" + ] + }, + { + "path": "litellm-rust/crates/inference-chat/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference-chat/", + "scope": [ + "litellm-rust/crates/inference-chat/**" + ] + }, + { + "path": "litellm-rust/crates/inference-messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference-messages/", + "scope": [ + "litellm-rust/crates/inference-messages/**" + ] + }, + { + "path": "litellm-rust/crates/inference-ocr/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference-ocr/", + "scope": [ + "litellm-rust/crates/inference-ocr/**" + ] + }, + { + "path": "litellm-rust/crates/inference-responses/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference-responses/", + "scope": [ + "litellm-rust/crates/inference-responses/**" + ] + }, + { + "path": "litellm-rust/crates/inference-transcription/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/inference-transcription/", + "scope": [ + "litellm-rust/crates/inference-transcription/**" + ] + }, + { + "path": "litellm-rust/crates/llms/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/", + "scope": [ + "litellm-rust/crates/llms/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/anthropic/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/anthropic/", + "scope": [ + "litellm-rust/crates/llms/src/anthropic/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/anthropic/batches/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/anthropic/batches/", + "scope": [ + "litellm-rust/crates/llms/src/anthropic/batches/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/anthropic/count_tokens/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/anthropic/count_tokens/", + "scope": [ + "litellm-rust/crates/llms/src/anthropic/count_tokens/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/anthropic/messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/anthropic/messages/", + "scope": [ + "litellm-rust/crates/llms/src/anthropic/messages/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/aws_textract/ocr/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/aws_textract/ocr/", + "scope": [ + "litellm-rust/crates/llms/src/aws_textract/ocr/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/azure_ai/messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/azure_ai/messages/", + "scope": [ + "litellm-rust/crates/llms/src/azure_ai/messages/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/base_llm/messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/base_llm/messages/", + "scope": [ + "litellm-rust/crates/llms/src/base_llm/messages/**" + ] + }, + { + "path": "litellm-rust/crates/llms/src/bedrock/messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms/src/bedrock/messages/", + "scope": [ + "litellm-rust/crates/llms/src/bedrock/messages/**" + ] + }, + { + "path": "litellm-rust/crates/llms-types/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms-types/", + "scope": [ + "litellm-rust/crates/llms-types/**" + ] + }, + { + "path": "litellm-rust/crates/llms-types/src/formats/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms-types/src/formats/", + "scope": [ + "litellm-rust/crates/llms-types/src/formats/**" + ] + }, + { + "path": "litellm-rust/crates/llms-types/src/formats/messages/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms-types/src/formats/messages/", + "scope": [ + "litellm-rust/crates/llms-types/src/formats/messages/**" + ] + }, + { + "path": "litellm-rust/crates/llms-types/src/providers/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/llms-types/src/providers/", + "scope": [ + "litellm-rust/crates/llms-types/src/providers/**" + ] + }, + { + "path": "litellm-rust/crates/model-catalog/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/model-catalog/", + "scope": [ + "litellm-rust/crates/model-catalog/**" + ] + }, + { + "path": "litellm-rust/crates/python-bridge/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-bridge/", + "scope": [ + "litellm-rust/crates/python-bridge/**" + ] + }, + { + "path": "litellm-rust/crates/python-bridge/src/cache/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-bridge/src/cache/", + "scope": [ + "litellm-rust/crates/python-bridge/src/cache/**" + ] + }, + { + "path": "litellm-rust/crates/python-bridge/src/cache/native/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-bridge/src/cache/native/", + "scope": [ + "litellm-rust/crates/python-bridge/src/cache/native/**" + ] + }, + { + "path": "litellm-rust/crates/python-bridge/src/cache/python/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-bridge/src/cache/python/", + "scope": [ + "litellm-rust/crates/python-bridge/src/cache/python/**" + ] + }, + { + "path": "litellm-rust/crates/python-bridge/src/routes/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-bridge/src/routes/", + "scope": [ + "litellm-rust/crates/python-bridge/src/routes/**" + ] + }, + { + "path": "litellm-rust/crates/python-compat/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/python-compat/", + "scope": [ + "litellm-rust/crates/python-compat/**" + ] + }, + { + "path": "litellm-rust/crates/secrets-aws/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/secrets-aws/", + "scope": [ + "litellm-rust/crates/secrets-aws/**" + ] + }, + { + "path": "litellm-rust/crates/secrets-azure/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/secrets-azure/", + "scope": [ + "litellm-rust/crates/secrets-azure/**" + ] + }, + { + "path": "litellm-rust/crates/secrets-cyberark/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/secrets-cyberark/", + "scope": [ + "litellm-rust/crates/secrets-cyberark/**" + ] + }, + { + "path": "litellm-rust/crates/secrets-google/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/secrets-google/", + "scope": [ + "litellm-rust/crates/secrets-google/**" + ] + }, + { + "path": "litellm-rust/crates/secrets-hashicorp/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/secrets-hashicorp/", + "scope": [ + "litellm-rust/crates/secrets-hashicorp/**" + ] + }, + { + "path": "litellm-rust/crates/storage-clickhouse/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/storage-clickhouse/", + "scope": [ + "litellm-rust/crates/storage-clickhouse/**" + ] + }, + { + "path": "litellm-rust/crates/traces/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/", + "scope": [ + "litellm-rust/crates/traces/**" + ] + }, + { + "path": "litellm-rust/crates/traces/src/normalize/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/src/normalize/", + "scope": [ + "litellm-rust/crates/traces/src/normalize/**" + ] + }, + { + "path": "litellm-rust/crates/traces/src/normalize/format/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/src/normalize/format/", + "scope": [ + "litellm-rust/crates/traces/src/normalize/format/**" + ] + }, + { + "path": "litellm-rust/crates/traces/src/normalize/instrumentation/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/src/normalize/instrumentation/", + "scope": [ + "litellm-rust/crates/traces/src/normalize/instrumentation/**" + ] + }, + { + "path": "litellm-rust/crates/traces/src/otlp/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/src/otlp/", + "scope": [ + "litellm-rust/crates/traces/src/otlp/**" + ] + }, + { + "path": "litellm-rust/crates/traces/src/resolve/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces/src/resolve/", + "scope": [ + "litellm-rust/crates/traces/src/resolve/**" + ] + }, + { + "path": "litellm-rust/crates/traces-cache/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces-cache/", + "scope": [ + "litellm-rust/crates/traces-cache/**" + ] + }, + { + "path": "litellm-rust/crates/traces-clickhouse/AGENTS.md", + "description": "Conventions for code under litellm-rust/crates/traces-clickhouse/", + "scope": [ + "litellm-rust/crates/traces-clickhouse/**" + ] + }, + { + "path": "litellm-rust/docs/adrs/AGENTS.md", + "description": "Conventions for code under litellm-rust/docs/adrs/", + "scope": [ + "litellm-rust/docs/adrs/**" + ] + }, + { + "path": "tests/AGENTS.md", + "description": "Conventions for code under tests/", + "scope": [ + "tests/**" + ] + }, + { + "path": "tests/e2e/AGENTS.md", + "description": "Conventions for code under tests/e2e/", + "scope": [ + "tests/e2e/**" + ] + }, + { + "path": "tests/e2e_harness/AGENTS.md", + "description": "Conventions for code under tests/e2e_harness/", + "scope": [ + "tests/e2e_harness/**" + ] + }, + { + "path": "tests/integration/AGENTS.md", + "description": "Conventions for code under tests/integration/", + "scope": [ + "tests/integration/**" + ] + }, + { + "path": "tests/integration/translation/AGENTS.md", + "description": "Conventions for code under tests/integration/translation/", + "scope": [ + "tests/integration/translation/**" + ] + }, + { + "path": "tests/rust-python-harness/AGENTS.md", + "description": "Conventions for code under tests/rust-python-harness/", + "scope": [ + "tests/rust-python-harness/**" + ] + }, + { + "path": "tests/rust-python-harness/strategies/e2e_parity/AGENTS.md", + "description": "Conventions for code under tests/rust-python-harness/strategies/e2e_parity/", + "scope": [ + "tests/rust-python-harness/strategies/e2e_parity/**" + ] + }, + { + "path": "tests/rust-python-harness/strategies/trace_parity/AGENTS.md", + "description": "Conventions for code under tests/rust-python-harness/strategies/trace_parity/", + "scope": [ + "tests/rust-python-harness/strategies/trace_parity/**" + ] + }, + { + "path": "tests/rust-python-harness/strategies/unit_tests_parity/AGENTS.md", + "description": "Conventions for code under tests/rust-python-harness/strategies/unit_tests_parity/", + "scope": [ + "tests/rust-python-harness/strategies/unit_tests_parity/**" + ] + }, + { + "path": "tests/rust-python-harness/strategies/unit_tests_rust/AGENTS.md", + "description": "Conventions for code under tests/rust-python-harness/strategies/unit_tests_rust/", + "scope": [ + "tests/rust-python-harness/strategies/unit_tests_rust/**" + ] + }, + { + "path": "tests/test_litellm_rust/AGENTS.md", + "description": "Conventions for code under tests/test_litellm_rust/", + "scope": [ + "tests/test_litellm_rust/**" + ] + }, + { + "path": "tests/unit/AGENTS.md", + "description": "Conventions for code under tests/unit/", + "scope": [ + "tests/unit/**" + ] + }, + { + "path": "tests/unit/rust_bridge/AGENTS.md", + "description": "Conventions for code under tests/unit/rust_bridge/", + "scope": [ + "tests/unit/rust_bridge/**" + ] + }, + { + "path": "ui/litellm-dashboard/AGENTS.md", + "description": "Conventions for code under ui/litellm-dashboard/", + "scope": [ + "ui/litellm-dashboard/**" + ] + }, + { + "path": "ui/litellm-dashboard/src/components/chat/AGENTS.md", + "description": "Conventions for code under ui/litellm-dashboard/src/components/chat/", + "scope": [ + "ui/litellm-dashboard/src/components/chat/**" + ] + }, + { + "path": "ui/litellm-dashboard/src/components/lens/traces/AGENTS.md", + "description": "Conventions for code under ui/litellm-dashboard/src/components/lens/traces/", + "scope": [ + "ui/litellm-dashboard/src/components/lens/traces/**" + ] + } + ] +} diff --git a/tests/code_coverage_tests/check_greptile_files_json.py b/tests/code_coverage_tests/check_greptile_files_json.py new file mode 100644 index 00000000000..a3e9658fea0 --- /dev/null +++ b/tests/code_coverage_tests/check_greptile_files_json.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import dataclasses +import difflib +import json +import os +import subprocess +import sys +from pathlib import Path, PurePosixPath +from typing import Final + +REPO_ROOT: Final = Path(__file__).resolve().parents[2] +FILES_JSON: Final = PurePosixPath(".greptile/files.json") +WRITE_FLAG: Final = "--write" + + +@dataclasses.dataclass(frozen=True, slots=True) +class _ContextFile: + path: str + description: str + scope: tuple[str, ...] + + +def _nested_agents_md_paths(repo_root: Path) -> tuple[PurePosixPath, ...]: + result: Final = subprocess.run( + ["git", "-C", os.fspath(repo_root), "ls-files", "-z", "--", "*/AGENTS.md"], + check=True, + stdout=subprocess.PIPE, + ) + return tuple(sorted(PurePosixPath(os.fsdecode(path)) for path in result.stdout.split(b"\0") if path)) + + +def _context_file(agents_md: PurePosixPath) -> _ContextFile: + return _ContextFile( + path=str(agents_md), + description=f"Conventions for code under {agents_md.parent}/", + scope=(f"{agents_md.parent}/**",), + ) + + +def render(repo_root: Path) -> str: + files: Final = tuple(dataclasses.asdict(_context_file(path)) for path in _nested_agents_md_paths(repo_root)) + return json.dumps({"files": files}, indent=2) + "\n" + + +def main(argv: tuple[str, ...]) -> int: + expected: Final = render(REPO_ROOT) + target: Final = REPO_ROOT / FILES_JSON + if WRITE_FLAG in argv: + target.parent.mkdir(exist_ok=True) + target.write_text(expected) + print(f"Wrote {FILES_JSON}") + return 0 + actual: Final = target.read_text() if target.is_file() else "" + if actual == expected: + print(f"{FILES_JSON} lists every nested AGENTS.md") + return 0 + sys.stdout.writelines( + difflib.unified_diff( + actual.splitlines(keepends=True), expected.splitlines(keepends=True), str(FILES_JSON), "expected" + ) + ) + print( + f"\n{FILES_JSON} is out of sync with the nested AGENTS.md files. Regenerate it with:\n" + f" python {Path(__file__).resolve().relative_to(REPO_ROOT)} {WRITE_FLAG}" + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main(tuple(sys.argv[1:]))) diff --git a/tests/code_coverage_tests/test_check_greptile_files_json.py b/tests/code_coverage_tests/test_check_greptile_files_json.py new file mode 100644 index 00000000000..8fdc070d7ff --- /dev/null +++ b/tests/code_coverage_tests/test_check_greptile_files_json.py @@ -0,0 +1,75 @@ +import json +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Final + +GUARD: Final = Path(__file__).resolve().parent / "check_greptile_files_json.py" +FILES_JSON: Final = Path(".greptile/files.json") + + +def _git(repo: Path, *args: str) -> None: + subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True) + + +def _write_agents_md(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("# conventions\n") + + +def _repo_with(tmp_path: Path, files: tuple[str, ...]) -> Path: + guard_copy: Final = tmp_path / "tests" / "code_coverage_tests" / GUARD.name + guard_copy.parent.mkdir(parents=True) + shutil.copy(GUARD, guard_copy) + for relative_path in files: + _write_agents_md(tmp_path / relative_path) + _git(tmp_path, "init", "-q") + _git(tmp_path, "add", "-A") + return guard_copy + + +def _run(guard: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run([sys.executable, "-I", str(guard), *args], capture_output=True, text=True) + + +def test_write_scopes_each_nested_agents_md_to_its_own_directory(tmp_path: Path) -> None: + guard: Final = _repo_with(tmp_path, ("AGENTS.md", "tests/AGENTS.md", "litellm/_v2/cache/AGENTS.md")) + + assert _run(guard, "--write").returncode == 0 + + written: Final = json.loads((tmp_path / FILES_JSON).read_text()) + assert [(entry["path"], entry["scope"]) for entry in written["files"]] == [ + ("litellm/_v2/cache/AGENTS.md", ["litellm/_v2/cache/**"]), + ("tests/AGENTS.md", ["tests/**"]), + ] + assert _run(guard).returncode == 0 + + +def test_new_agents_md_missing_from_files_json_fails(tmp_path: Path) -> None: + guard: Final = _repo_with(tmp_path, ("tests/AGENTS.md",)) + assert _run(guard, "--write").returncode == 0 + _write_agents_md(tmp_path / "ui" / "AGENTS.md") + _git(tmp_path, "add", "-A") + + result: Final = _run(guard) + + assert result.returncode == 1 + assert '+ "path": "ui/AGENTS.md",' in result.stdout + + +def test_deleted_agents_md_left_in_files_json_fails(tmp_path: Path) -> None: + guard: Final = _repo_with(tmp_path, ("tests/AGENTS.md", "ui/AGENTS.md")) + assert _run(guard, "--write").returncode == 0 + _git(tmp_path, "rm", "-q", "-f", "ui/AGENTS.md") + + result: Final = _run(guard) + + assert result.returncode == 1 + assert '- "path": "ui/AGENTS.md",' in result.stdout + + +def test_missing_files_json_fails(tmp_path: Path) -> None: + guard: Final = _repo_with(tmp_path, ("tests/AGENTS.md",)) + + assert _run(guard).returncode == 1