From fbb2aacb71ba907476748419b5e1c59a4156fb08 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:04:13 +0000 Subject: [PATCH] agent-exec transport 973ce511e924ce5ab0953e16e46a42ec/7671c49f7667 --- tests/test_task_recovery.py | 203 ++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index f2627c1d..b24bf541 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -469,6 +469,209 @@ class TaskRecoveryTests(unittest.TestCase): self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout, cached_before) + def write_no_cache_wrapper(self, bin_dir, advertised, log_name="git.log"): + """Create a disposable git wrapper for the missing-tracking-ref scenarios. + + Intercepts: + fetch -> fail (origin unreachable; the wrapper itself exits 128 + and writes the standard "unable to access origin" stderr line + so the precheck captures the same `fetch_err` it would observe + against a real unreachable remote) + ls-remote origin refs/heads/main -> configured ADVERTISED (or + nothing when ADVERTISED is empty, simulating an origin that + has no configured target branch) + branch --merged origin/main -> empty output (the cached + tracking ref is deleted locally; the real git would otherwise + fail the pipeline under set -e/pipefail and the precheck would + silently exit before the verdict is printed) + + All other git calls delegate to the real binary via the resolved + REAL_GIT absolute path (and a stripped PATH so the wrapper cannot + recurse into itself) so the registered task context stays + observable normally. + + Returns (bin_dir, wrapper_log_path).""" + f = self.fixture + real_git = subprocess.check_output( + ["bash", "-c", "command -v git"], env=f.env, text=True).strip() + bin_dir.mkdir(exist_ok=True) + wrapper_log = bin_dir / log_name + wrapper = bin_dir / "git" + wrapper.write_text( + "#!/usr/bin/env bash\n" + "# Disposable fixture wrapper for the missing-tracking-ref\n" + "# scenarios:\n" + "# fetch -> fail (128)\n" + "# ls-remote origin refs/heads/main -> $ADVERTISED\n" + "# branch --merged origin/main -> empty\n" + "# Anything else delegates to the real binary so the registered\n" + "# task context (HEAD, branch, R0, worktree, .task/) stays\n" + "# observable normally.\n" + f"REAL_GIT={shlex.quote(real_git)}\n" + f"LOG={shlex.quote(str(wrapper_log))}\n" + f"ADVERTISED={shlex.quote(advertised)}\n" + "WRAPPER_BIN=$(cd \"$(dirname \"$0\")\" && pwd)\n" + "echo \"configured advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + "stripped=\"\"\n" + "old_ifs=\"$IFS\"\n" + "IFS=':'\n" + "for p in $PATH; do\n" + " [ \"$p\" = \"$WRAPPER_BIN\" ] && continue\n" + " if [ -z \"$stripped\" ]; then stripped=\"$p\"; else stripped=\"$stripped:$p\"; fi\n" + "done\n" + "IFS=\"$old_ifs\"\n" + "export PATH=\"$stripped\"\n" + "case \"$1\" in\n" + " fetch)\n" + " echo \"fetch-intercepted\" >> \"$LOG\"\n" + " echo 'fatal: unable to access origin' >&2\n" + " exit 128 ;;\n" + " ls-remote)\n" + " if [ \"${2:-}\" = origin ] && [ \"${3:-}\" = refs/heads/main ]; then\n" + " echo \"ls-remote-served advertised-target=$ADVERTISED\" >> \"$LOG\"\n" + " if [ -n \"$ADVERTISED\" ]; then\n" + " printf '%s\\trefs/heads/main\\n' \"$ADVERTISED\"\n" + " fi\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + " branch)\n" + " # refs/remotes/origin/main is deleted locally; the real git\n" + " # `git branch --merged origin/main` would fail and the\n" + " # precheck pipeline (with pipefail + set -e) would silently\n" + " # exit. Stub it so the verdict is actually emitted.\n" + " if [ \"${2:-}\" = --merged ] && [ \"${3:-}\" = origin/main ]; then\n" + " echo \"branch-merged-stub no-merged-branches\" >> \"$LOG\"\n" + " exit 0\n" + " fi\n" + " exec \"$REAL_GIT\" \"$@\" ;;\n" + "esac\n" + "exec \"$REAL_GIT\" \"$@\"\n") + wrapper.chmod(0o755) + return bin_dir, wrapper_log + + def test_local_precheck_without_cached_ref_rejects_uncomparable_remote(self): + """Cached refs/remotes/origin/main is absent; origin advertises a + SHA whose object is unavailable locally. Local precheck must not + advertise LOCAL_READY because ancestry cannot be compared against + any locally known target — the cached target cannot be trusted + as authoritative without an ancestry check, and the advertised + SHA is not locally reachable so no forward-descendant + relationship can be established either. + + The current implementation lets target_history() short-circuit at + the missing cached-ref step (return "ok") and unconditionally + print LOCAL_READY in local mode. The verdict is wrong; this test + records the expected red result.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # Detach the registered branch from its (now stale) upstream so + # no in-process code accidentally queries HEAD@{u} / remote.merge + # against the deleted cached ref. + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + synthetic = "1" * 40 + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-uncomparable-bin", synthetic) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + # Proof: the wrapper was loaded (configured marker) and served + # the failure case (fetch intercepted). ls-remote is served by + # the wrapper if and when the post-fix implementation queries it + # for the missing cached-ref path; the current implementation + # short-circuits before ls-remote, so the assertion below does + # not require an ls-remote entry. + log = wrapper_log.read_text() + self.assertIn(f"configured advertised-target={synthetic}", log) + self.assertIn("fetch-intercepted", log) + + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + def test_local_precheck_without_cached_ref_rejects_missing_remote_target(self): + """Cached refs/remotes/origin/main is absent; origin returns no + configured target branch (ls-remote outputs nothing). Local + precheck must not advertise LOCAL_READY because nothing can be + verified against the registered task's R0 — the cached target + is the only handle on what origin claimed, and without it (and + without an ls-remote response) the precheck has no + authoritative signal to confirm the target even exists. + + The current implementation lets target_history() short-circuit at + the missing cached-ref step (return "ok") and unconditionally + print LOCAL_READY in local mode. The verdict is wrong; this test + records the expected red result.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-missing-target-bin", "") + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + log = wrapper_log.read_text() + # Empty advertised value means the wrapper emits no ls-remote line. + self.assertIn("configured advertised-target=", log) + self.assertIn("fetch-intercepted", log) + + self.assertNotEqual(local.returncode, 0, local.stdout) + self.assertNotIn("[verdict] LOCAL_READY", local.stdout) + self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + + def test_local_precheck_without_cached_ref_allows_comparable_forward_target(self): + """Cached refs/remotes/origin/main is absent; origin advertises + the task's R0 (or a forward descendant that is locally + available). Local precheck must continue to advertise + LOCAL_READY — the explicit missing-tracking-ref continuation + contract — because the registered task context is the same as + the cached ref would have represented, and the ls-remote + response can be compared against the registered baseline to + confirm origin is at or past the task's initial state. + + The current implementation short-circuits at the missing + cached-ref step (return "ok"), which coincidentally returns + LOCAL_READY for any registered task. The verdict is right; this + test records the expected green result so the post-fix + implementation preserves the contract when the ls-remote check + is added against the registered R0.""" + f = self.fixture + head_before = f.git(self.wt, "rev-parse", "HEAD").stdout + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + f.git(self.wt, "branch", "--unset-upstream", "feature/test") + f.git(self.wt, "update-ref", "-d", "refs/remotes/origin/main") + + bin_dir, wrapper_log = self.write_no_cache_wrapper( + f.root / "no-cache-comparable-bin", r0) + path_env = f"{bin_dir}{os.pathsep}{f.env.get('PATH', os.environ['PATH'])}" + wrapper_env = f.env | {"PATH": path_env} + + local = f.task(self.wt, "precheck", check=False, + env=wrapper_env | {"TASK_WORK_MODE": "local"}) + + log = wrapper_log.read_text() + self.assertIn(f"configured advertised-target={r0}", log) + self.assertIn("fetch-intercepted", log) + + self.assertEqual(local.returncode, 0, local.stdout) + self.assertIn("[verdict] LOCAL_READY", local.stdout) + self.assertNotIn("[verdict] REMOTE_UNVERIFIED", local.stdout) + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before) + if __name__ == "__main__": unittest.main()