diff --git a/tests/fixture_git.py b/tests/fixture_git.py new file mode 100644 index 0000000000..e0f6bec0b3 --- /dev/null +++ b/tests/fixture_git.py @@ -0,0 +1,24 @@ +"""The one git identity every test repository commits under, given to each command. + +No test writes identity or signing into a repository's config. When GIT_DIR leaks in +(``git bisect run`` from a linked worktree, a hook), ``git config`` writes the caller's +repository instead of the fixture's: on 10-23 two fixtures stamped "Citation fixture" and +"Ratchet test" into the shared agent-comms config, and 106 real commits carried the second. +A setting given per command persists nothing, whichever repository git resolves.""" + +SETTINGS = ( + ("user.name", "Test fixture"), + ("user.email", "fixture@example.invalid"), + ("commit.gpgsign", "false"), +) + +# Global options for one git command: ("git", *IDENTITY, "-C", path, ...). +IDENTITY = tuple(part for key, value in SETTINGS for part in ("-c", f"{key}={value}")) + +# The same settings for a git that the code under test starts, through git's own +# GIT_CONFIG_COUNT channel; set with monkeypatch so they end with the test. +ENVIRONMENT = { + "GIT_CONFIG_COUNT": str(len(SETTINGS)), + **{f"GIT_CONFIG_KEY_{index}": key for index, (key, _) in enumerate(SETTINGS)}, + **{f"GIT_CONFIG_VALUE_{index}": value for index, (_, value) in enumerate(SETTINGS)}, +} diff --git a/tests/guards/test_debt_ratchet.py b/tests/guards/test_debt_ratchet.py index 13a9c89234..c07467a696 100644 --- a/tests/guards/test_debt_ratchet.py +++ b/tests/guards/test_debt_ratchet.py @@ -15,6 +15,7 @@ from agent_comms.child_process import ObservedProcess, SpawnedProcess from agent_comms.field_codec import FieldCodec +from fixture_git import IDENTITY from process_tree_fixture import TREE, identities from tools.debt_ratchet import ( @@ -130,12 +131,10 @@ def __init__(self, path: Path, root: str, ratchet: RatchetServer): self.root = root self.ratchet = ratchet self.git("init", "-q") - self.git("config", "user.email", "ratchet@example.invalid") - self.git("config", "user.name", "Ratchet test") def git(self, *args: str) -> str: return subprocess.check_output( - ["git", "-C", str(self.path), *args], text=True, timeout=10 + ["git", *IDENTITY, "-C", str(self.path), *args], text=True, timeout=10 ).strip() def commit(self, files: dict[str, str | None]) -> str: @@ -147,7 +146,7 @@ def commit(self, files: dict[str, str | None]) -> str: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(source) self.git("add", "--all") - self.git("-c", "commit.gpgsign=false", "commit", "-qm", "test", "--allow-empty") + self.git("commit", "-qm", "test", "--allow-empty") return self.git("rev-parse", "HEAD") def arguments(self, base: str, head: str) -> tuple[str, ...]: diff --git a/tests/source_revision_fixture.py b/tests/source_revision_fixture.py index 457274def5..9ca9dab132 100644 --- a/tests/source_revision_fixture.py +++ b/tests/source_revision_fixture.py @@ -9,6 +9,7 @@ from agent_comms.repository_name import RepositoryName from agent_comms.change_report import FileLine from agent_comms.todos import FixScope, FixShape, PullRequestLink +from fixture_git import IDENTITY class SourceRepository(Repo): @@ -36,10 +37,16 @@ def scope(cls) -> FixScope: parent = next(node for node in ast.parse(cls.main_source).body if isinstance(node, ast.ClassDef)) return FixScope(FixShape(parents=(FileLine('owner.py', parent.lineno),), members=())) + @staticmethod + def _run(checkout: Path | str, *args: str, timeout: float = 60, + input: bytes = b"") -> subprocess.CompletedProcess[bytes]: + """Repo's one git subprocess, every command carrying the fixture identity.""" + return Repo._run(checkout, *IDENTITY, *args, timeout=timeout, input=input) + @staticmethod def git_input(repository: Path, text: str, *arguments: str) -> str: return subprocess.run( - ('git', '-C', str(repository), *arguments), input=text, text=True, + ('git', *IDENTITY, '-C', str(repository), *arguments), input=text, text=True, capture_output=True, check=True, timeout=30, ).stdout.strip() @@ -58,9 +65,6 @@ def create(cls, root: Path, name: RepositoryName = RepositoryName.from_wire('exa repository.mkdir(parents=True) remote = cls.remote_for(root, name) cls.git(repository, 'init', '--initial-branch=main') - cls.git(repository, 'config', 'user.name', 'Citation fixture') - cls.git(repository, 'config', 'user.email', 'citation@example.invalid') - cls.git(repository, 'config', 'commit.gpgsign', 'false') cls.git(repository, 'remote', 'add', 'origin', str(remote)) main_blob = cls.git_input(repository, cls.main_source, 'hash-object', '-w', '--stdin') diff --git a/tests/test_carry.py b/tests/test_carry.py index 8c7ac13b9b..441ac3f15b 100644 --- a/tests/test_carry.py +++ b/tests/test_carry.py @@ -48,6 +48,7 @@ from cutover.carry_commander_project import CommanderProjectCarry from cutover.carry_drop_connectivity import DropConnectivity from cutover.carry_parked_threads import ParkedThreads +from fixture_git import IDENTITY REPO = Path(__file__).resolve().parents[1] @@ -65,11 +66,9 @@ class Repository: def __init__(self, path: Path): self.path = path self.git("init", "-q") - self.git("config", "user.email", "carry@example.invalid") - self.git("config", "user.name", "Carry test") def git(self, *args: str) -> str: - return subprocess.check_output(["git", "-C", str(self.path), *args], text=True, timeout=10).strip() + return subprocess.check_output(["git", *IDENTITY, "-C", str(self.path), *args], text=True, timeout=10).strip() def commit(self, steps: dict[str, str | None]) -> str: """One commit per step, in order, as the main line adds steps one at a time.""" diff --git a/tests/test_checkout_submodules.py b/tests/test_checkout_submodules.py index 9db3097a4c..18cd68b0c3 100644 --- a/tests/test_checkout_submodules.py +++ b/tests/test_checkout_submodules.py @@ -8,11 +8,12 @@ import pytest from agent_comms.repo import Checkout, CheckoutHoldsWorkError, ScratchCheckout +from fixture_git import IDENTITY def git(path: Path, *args: str) -> str: return subprocess.run( - ("git", "-C", str(path), "-c", "user.name=t", "-c", "user.email=t@t", *args), + ("git", *IDENTITY, "-C", str(path), *args), check=True, capture_output=True, text=True, diff --git a/tests/test_checkpoint_command.py b/tests/test_checkpoint_command.py index c29797a385..beaf61d425 100644 --- a/tests/test_checkpoint_command.py +++ b/tests/test_checkpoint_command.py @@ -9,6 +9,15 @@ CheckpointCliCommand, CleanCheckpoint, PushUnknownCheckpoint, PushedCheckpoint, RefusedCheckpoint, ) from bus_record_fixture import context_comms +from fixture_git import ENVIRONMENT + + +@pytest.fixture(autouse=True) +def _fixture_identity(monkeypatch): + """The checkpoint command commits through its own git, so the identity rides the + environment that git inherits, never a repository's config.""" + for name, value in ENVIRONMENT.items(): + monkeypatch.setenv(name, value) def _git(worktree: Path, *arguments: str): @@ -23,8 +32,6 @@ def _repo(tmp_path: Path, name: str, *, remote: bool = True) -> Path: worktree = tmp_path / name worktree.mkdir() _git(worktree, "init", "-q", "-b", "main") - _git(worktree, "config", "user.name", "agent") - _git(worktree, "config", "user.email", "agent@example.com") if remote: _git(tmp_path, "init", "-q", "--bare", f"{name}.origin.git") _git(worktree, "remote", "add", "origin", str(tmp_path / f"{name}.origin.git")) diff --git a/tests/test_installed_merge.py b/tests/test_installed_merge.py index 29d38f7e62..629c4b382e 100644 --- a/tests/test_installed_merge.py +++ b/tests/test_installed_merge.py @@ -16,9 +16,9 @@ def test_a_merge_made_after_the_last_fetch_is_fetched_then_answered(tmp_path): origin, installed = tmp_path / 'origin', tmp_path / 'installed' origin.mkdir() git(origin, 'init', '-q', '-b', 'main') - git(origin, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '--allow-empty', '-m', 'base') + git(origin, 'commit', '-q', '--allow-empty', '-m', 'base') git(tmp_path, 'clone', '-q', str(origin), str(installed)) - git(origin, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '--allow-empty', '-m', 'merged later') + git(origin, 'commit', '-q', '--allow-empty', '-m', 'merged later') sha = git(origin, 'rev-parse', 'HEAD') repo = Repo(checkout=installed, github=RepositoryName.from_wire('test/repo')) diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index f1b04ee4ca..285c020d3f 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -12,6 +12,7 @@ import pytest from test_todos import order from source_revision_fixture import SourceRepository +from fixture_git import IDENTITY from agent_comms.comms import wire from agent_comms.goal_actions import CreateProblemGoalAction @@ -28,7 +29,7 @@ def git(checkout, *args): - return subprocess.run(['git', '-C', str(checkout), *args], check=True, capture_output=True, text=True).stdout.strip() + return subprocess.run(['git', *IDENTITY, '-C', str(checkout), *args], check=True, capture_output=True, text=True).stdout.strip() def test_prior_declarations_do_not_enter_native_request_schemas(): @@ -58,7 +59,7 @@ def unit_checkout(path): git(path, 'init', '-q') (path / 'owner.py').write_text('one\n') git(path, 'add', 'owner.py') - git(path, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', 'base') + git(path, 'commit', '-q', '-m', 'base') return path @@ -103,7 +104,7 @@ def test_a_submodule_with_a_missing_git_directory_does_not_stop_reading_edits(tm package = unit_checkout(tmp_path / 'package') worktree = unit_checkout(tmp_path / 'worker--P1') git(worktree, '-c', 'protocol.file.allow=always', 'submodule', 'add', '-q', str(package), 'pinned') - git(worktree, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', 'pin') + git(worktree, 'commit', '-q', '-m', 'pin') (worktree / 'pinned' / '.git').write_text('gitdir: ../.git/modules/removed\n') assert WorkBranches(worktree).uncommitted() == () edited(worktree, 'owner.py', 700.0) diff --git a/tests/test_phase_evidence.py b/tests/test_phase_evidence.py index e8029a1d0d..dec0195224 100644 --- a/tests/test_phase_evidence.py +++ b/tests/test_phase_evidence.py @@ -29,10 +29,11 @@ from agent_comms.plan_phases import BusDeletionPhase, FLAG, ExternalBoundariesPhase, PlanPhase, RostersPhase from agent_comms.repo import Repo from agent_comms.repository_name import RepositoryName +from fixture_git import IDENTITY def git(checkout, *args): - return subprocess.run(("git", "-C", str(checkout), *args), check=True, + return subprocess.run(("git", *IDENTITY, "-C", str(checkout), *args), check=True, capture_output=True, text=True, timeout=30).stdout.strip() @@ -41,7 +42,7 @@ def test_evidence_reads_origin_main_and_never_the_worktree(tmp_path): (tmp_path / "src").mkdir() (tmp_path / "src/kept.py").write_text("flag = True\n") git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "main") + git(tmp_path, "commit", "-qm", "main") sha = git(tmp_path, "rev-parse", "HEAD") git(tmp_path, "update-ref", "refs/remotes/origin/main", sha) (tmp_path / "src/kept.py").unlink() @@ -66,7 +67,7 @@ def test_boundary_evidence_ignores_its_declaration_and_exception_groups(tmp_path "try:\n pass\nexcept ExceptionGroup:\n raise\n" ) git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "boundary") + git(tmp_path, "commit", "-qm", "boundary") sha = git(tmp_path, "rev-parse", "HEAD") git(tmp_path, "update-ref", "refs/remotes/origin/main", sha) reading = PlanReading((), MainTree.of( @@ -75,7 +76,7 @@ def test_boundary_evidence_ignores_its_declaration_and_exception_groups(tmp_path source.write_text("try:\n pass\nexcept Exception:\n raise\n") git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "broad") + git(tmp_path, "commit", "-qm", "broad") git(tmp_path, "update-ref", "refs/remotes/origin/main", git(tmp_path, "rev-parse", "HEAD")) changed = PlanReading((), MainTree.of(reading.main.repo)) assert not changed.holds(ExternalBoundariesPhase.evidence) @@ -93,7 +94,7 @@ def main_of(tmp_path, files): (tmp_path / path).parent.mkdir(parents=True, exist_ok=True) (tmp_path / path).write_text(text) git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "main") + git(tmp_path, "commit", "-qm", "main") git(tmp_path, "update-ref", "refs/remotes/origin/main", git(tmp_path, "rev-parse", "HEAD")) return PlanReading((), MainTree.of( Repo(checkout=tmp_path, github=RepositoryName.from_wire("OpenHCSDev/agent-comms")))) @@ -123,7 +124,7 @@ def test_roster_evidence_keeps_external_abi_and_each_remaining_internal_registry ): (tmp_path / "src/remaining.py").write_text(remaining) git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "registry") + git(tmp_path, "commit", "-qm", "registry") git(tmp_path, "update-ref", "refs/remotes/origin/main", git(tmp_path, "rev-parse", "HEAD")) changed = PlanReading((), MainTree.of(reading.main.repo)) assert not changed.holds(RostersPhase.evidence), remaining @@ -138,7 +139,7 @@ def test_main_tree_grep_uses_the_same_repository_tree_as_its_module_reader(tmp_p reading = main_of(checkout, {"src/owner.py": "class OtherOwner:\n members = {}\n"}) git(checkout, "-c", "protocol.file.allow=always", "submodule", "add", "-q", str(dependency), "src/dependency") - git(checkout, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qam", "dependency") + git(checkout, "commit", "-qam", "dependency") git(checkout, "update-ref", "refs/remotes/origin/main", git(checkout, "rev-parse", "HEAD")) git(checkout, "config", "submodule.recurse", "true") assert "src/dependency" in git(checkout, "grep", "--recurse-submodules", "-n", @@ -172,7 +173,7 @@ def test_bus_deletion_evidence_reads_uses_without_matching_its_own_declaration(t ): (tmp_path / "src/uses.py").write_text(source) git(tmp_path, "add", ".") - git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "use") + git(tmp_path, "commit", "-qm", "use") git(tmp_path, "update-ref", "refs/remotes/origin/main", git(tmp_path, "rev-parse", "HEAD")) changed = PlanReading((), MainTree.of(reading.main.repo)) assert not changed.holds(BusDeletionPhase.evidence), source diff --git a/tests/test_proof_build.py b/tests/test_proof_build.py index 3a1cf3cadc..b0ed3fcf61 100644 --- a/tests/test_proof_build.py +++ b/tests/test_proof_build.py @@ -48,6 +48,7 @@ ) from agent_comms.tool_caller import ThreadCaller from agent_comms.tools import CommsOrderDoneTool, CommsProblemCloseTool, CommsTodoCreateTool +from fixture_git import IDENTITY from order_scope_fixture import FIX_SCOPE from source_revision_fixture import SourceRepository @@ -78,12 +79,7 @@ def proof_build_checkouts( subprocess.run( ( "git", - "-c", - "user.name=Proof Build", - "-c", - "user.email=proof@example.invalid", - "-c", - "commit.gpgsign=false", + *IDENTITY, "commit", "--allow-empty", "-m",