from __future__ import annotations import os import shutil import subprocess from pathlib import Path from repo_manager.provenance import assistant_report, install_hook SOURCE_HOOK = Path(__file__).parents[1] / ".githooks" / "prepare-commit-msg" ASSISTANT_ENV_PREFIXES = ("ASSISTANT_", "CLAUDE", "CODEX_", "GROK", "XAI_") def _human_env() -> dict[str, str]: return { key: value for key, value in os.environ.items() if not any(key.startswith(prefix) for prefix in ASSISTANT_ENV_PREFIXES) } def _git(repo: Path, *args: str, env: dict[str, str] | None = None) -> None: subprocess.run(["git", *args], cwd=repo, env=env, check=True, capture_output=True) def _fixture(tmp_path: Path) -> Path: repo = tmp_path / "pilot" repo.mkdir() _git(repo, "init") _git(repo, "config", "user.email", "test@example.com") _git(repo, "config", "user.name", "Test") hooks = repo / ".githooks" hooks.mkdir() shutil.copy2(SOURCE_HOOK, hooks / "prepare-commit-msg") (hooks / "prepare-commit-msg").chmod(0o755) _git(repo, "config", "core.hooksPath", str(hooks)) return repo def _commit(repo: Path, name: str, *, session: str | None = None) -> None: (repo / name).write_text(name, encoding="utf-8") _git(repo, "add", name) env = _human_env() if session: env.update( ASSISTANT_NAME="codex", ASSISTANT_MODEL="gpt-test", ASSISTANT_PROCESS="42@test-host", ASSISTANT_SESSION=session, ) _git(repo, "commit", "-m", name, env=env) def test_hook_is_silent_for_humans_and_idempotent_for_assistants(tmp_path: Path) -> None: message = tmp_path / "message" message.write_text("Change records\n", encoding="utf-8") subprocess.run([str(SOURCE_HOOK), str(message)], env=_human_env(), check=True) assert message.read_text(encoding="utf-8") == "Change records\n" env = _human_env() env.update( ASSISTANT_NAME="codex", ASSISTANT_MODEL="gpt-test", ASSISTANT_PROCESS="42@test-host", ASSISTANT_SESSION="session-a", ) subprocess.run([str(SOURCE_HOOK), str(message)], env=env, check=True) subprocess.run([str(SOURCE_HOOK), str(message)], env=env, check=True) text = message.read_text(encoding="utf-8") assert text.count("Assistant: codex") == 1 assert text.count("Assistant-Model: gpt-test") == 1 assert text.count("Assistant-Session: session-a") == 1 def test_report_detects_interleaved_sessions_and_unknown_human(tmp_path: Path) -> None: repo = _fixture(tmp_path) _commit(repo, "human") _commit(repo, "a1", session="session-a") _commit(repo, "b1", session="session-b") _commit(repo, "a2", session="session-a") report = assistant_report(repo) assert report["assistants"]["codex"]["commit_count"] == 3 assert report["assistants"]["codex"]["models"] == ["gpt-test"] assert report["interleaved_sessions"] == [["session-a", "session-b"]] assert report["unattributed"]["before_cutover"] == 1 def test_install_sets_only_global_hooks_path(tmp_path: Path, monkeypatch) -> None: hooks = tmp_path / "hooks" hooks.mkdir() shutil.copy2(SOURCE_HOOK, hooks / "prepare-commit-msg") (hooks / "prepare-commit-msg").chmod(0o755) global_config = tmp_path / "gitconfig" monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(global_config)) result = install_hook(hooks) assert result["ok"] is True configured = subprocess.run( ["git", "config", "--global", "--get", "core.hooksPath"], check=True, capture_output=True, text=True, env={**os.environ, "GIT_CONFIG_GLOBAL": str(global_config)}, ).stdout.strip() assert configured == str(hooks.resolve())