"""Tests for scripts/audit_tier2_leaks.py. The audit script defends against tier-2 sandbox-only files leaking into the main repo's working tree (defense-in-depth: the pre-commit hook prevents leaks during commit, the audit catches anything that slips through). It scans the working tree and recent commits for files matching the forbidden patterns in conductor/tier2/githooks/forbidden-files.txt. """ import json import subprocess import sys from pathlib import Path import pytest AUDIT = Path("scripts/audit_tier2_leaks.py").resolve() @pytest.fixture def repo_root(tmp_path: Path) -> Path: """Create a minimal repo with the audit script's expected layout.""" repo = tmp_path / "repo" repo.mkdir() (repo / "conductor" / "tier2" / "githooks").mkdir(parents=True) (repo / "conductor" / "tier2" / "githooks" / "forbidden-files.txt").write_text( ".opencode/agents/tier2-\n" ".opencode/commands/tier-2-\n" "opencode.json\n" "mcp_paths.toml\n" ) # Copy the audit script into the repo so it can be invoked by relative path audit_dst = repo / "scripts" / "audit_tier2_leaks.py" audit_dst.parent.mkdir(parents=True) audit_dst.write_bytes(AUDIT.read_bytes()) return repo def _run_audit(cwd: Path, *args: str) -> subprocess.CompletedProcess: """Invoke the audit script with --json for machine-readable output.""" return subprocess.run( [sys.executable, "scripts/audit_tier2_leaks.py", "--json", *args], cwd=str(cwd), capture_output=True, text=True, ) def _files_block(result: subprocess.CompletedProcess) -> list[dict]: """Parse audit --json output's 'files' block.""" return json.loads(result.stdout)["files"] def test_audit_clean_working_tree_returns_zero(repo_root: Path) -> None: """No forbidden files in working tree: audit exits 0 (informational mode).""" result = _run_audit(repo_root) assert result.returncode == 0, f"unexpected failure: {result.stderr}" files = _files_block(result) assert files == [], f"unexpected files: {files}" def test_audit_detects_forbidden_agent_file_in_working_tree(repo_root: Path) -> None: """An untracked .opencode/agents/tier2-*.md file is reported.""" opencode_dir = repo_root / ".opencode" / "agents" opencode_dir.mkdir(parents=True) (opencode_dir / "tier2-autonomous.md").write_text("leak\n") result = _run_audit(repo_root) files = _files_block(result) paths = {f["path"] for f in files} assert ".opencode/agents/tier2-autonomous.md" in paths def test_audit_detects_forbidden_command_file_in_working_tree(repo_root: Path) -> None: """An untracked .opencode/commands/tier-2-*.md file is reported.""" cmd_dir = repo_root / ".opencode" / "commands" cmd_dir.mkdir(parents=True) (cmd_dir / "tier-2-auto-execute.md").write_text("leak\n") result = _run_audit(repo_root) paths = {f["path"] for f in _files_block(result)} assert ".opencode/commands/tier-2-auto-execute.md" in paths def test_audit_detects_modified_opencode_json(repo_root: Path) -> None: """A modified opencode.json (added to the working tree) is reported.""" (repo_root / "opencode.json").write_text('{"tier2-modified": true}\n') result = _run_audit(repo_root) paths = {f["path"] for f in _files_block(result)} assert "opencode.json" in paths def test_audit_detects_modified_mcp_paths_toml(repo_root: Path) -> None: """A modified mcp_paths.toml is reported.""" (repo_root / "mcp_paths.toml").write_text('[allowed_paths]\nextra_dirs = ["leaked"]\n') result = _run_audit(repo_root) paths = {f["path"] for f in _files_block(result)} assert "mcp_paths.toml" in paths def test_audit_ignores_non_forbidden_files(repo_root: Path) -> None: """Files NOT matching any pattern are not reported.""" (repo_root / "src.py").write_text("print('hi')\n") (repo_root / "README.md").write_text("# Hello\n") # conductor/tier2/agents/tier2-tech-lead.md is the INTERACTIVE tier-2 # tech-lead (main repo agent prompt), not the sandbox tier-2-autonomous. # It must NOT be flagged even though its path contains 'tier2-'. (repo_root / "conductor" / "tier2" / "agents").mkdir(parents=True, exist_ok=True) (repo_root / "conductor" / "tier2" / "agents" / "tier2-tech-lead.md").write_text( "# interactive tier-2 (allowed)\n" ) result = _run_audit(repo_root) assert result.returncode == 0 files = _files_block(result) assert files == [], f"false positives: {files}" def test_audit_reports_untracked_and_modified_separately(repo_root: Path) -> None: """Untracked forbidden files: status='untracked'. Modified tracked: 'modified'.""" # untracked case (repo_root / ".opencode" / "agents").mkdir(parents=True) (repo_root / ".opencode" / "agents" / "tier2-autonomous.md").write_text("a\n") result = _run_audit(repo_root) files = _files_block(result) status_by_path = {f["path"]: f["status"] for f in files} assert status_by_path[".opencode/agents/tier2-autonomous.md"] == "untracked" def test_audit_strict_exits_nonzero_on_leak(repo_root: Path) -> None: """--strict mode: any leak causes exit 1 (CI gate).""" (repo_root / ".opencode" / "agents").mkdir(parents=True) (repo_root / ".opencode" / "agents" / "tier2-autonomous.md").write_text("leak\n") result = _run_audit(repo_root, "--strict") assert result.returncode == 1, f"strict mode should fail: {result.returncode}" def test_audit_strict_exits_zero_when_clean(repo_root: Path) -> None: """--strict mode with clean tree: exit 0.""" result = _run_audit(repo_root, "--strict") assert result.returncode == 0, f"strict mode should pass: {result.returncode}" def test_audit_default_mode_exits_zero_even_with_leaks(repo_root: Path) -> None: """Default (informational) mode: leaks are reported but exit 0.""" (repo_root / "opencode.json").write_text('{"leaked": true}\n') result = _run_audit(repo_root) assert result.returncode == 0, f"informational mode should pass: {result.returncode}" # But the leak IS reported in --json output files = _files_block(result) paths = {f["path"] for f in files} assert "opencode.json" in paths def test_audit_handles_missing_config_gracefully(repo_root: Path) -> None: """If the forbidden-files.txt config is missing, the audit exits 0 with a warning. The audit should not crash on missing config (the hook would also no-op in this case; both layers degrade safely).""" (repo_root / "conductor" / "tier2" / "githooks" / "forbidden-files.txt").unlink() result = _run_audit(repo_root) assert result.returncode == 0, f"missing config should not fail: {result.stderr}" # No files should be reported (nothing to match against) assert _files_block(result) == [] def test_audit_human_readable_output_includes_path(repo_root: Path) -> None: """Without --json, the human-readable report mentions the leaked path.""" (repo_root / ".opencode" / "agents").mkdir(parents=True) (repo_root / ".opencode" / "agents" / "tier2-autonomous.md").write_text("leak\n") result = subprocess.run( [sys.executable, "scripts/audit_tier2_leaks.py"], cwd=str(repo_root), capture_output=True, text=True, ) assert result.returncode == 0 assert "tier2-autonomous.md" in result.stdout, ( f"expected path in stdout, got: {result.stdout!r}" ) def test_audit_summary_counts(repo_root: Path) -> None: """JSON output includes a 'summary' block with total counts.""" (repo_root / "opencode.json").write_text("a\n") (repo_root / "mcp_paths.toml").write_text("b\n") result = _run_audit(repo_root) data = json.loads(result.stdout) assert "summary" in data assert data["summary"]["total"] >= 2