agent-exec transport 973ce511e924ce5ab0953e16e46a42ec/7671c49f7667
This commit is contained in:
@@ -469,6 +469,209 @@ class TaskRecoveryTests(unittest.TestCase):
|
|||||||
self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout,
|
self.assertEqual(f.git(self.wt, "rev-parse", "refs/remotes/origin/main").stdout,
|
||||||
cached_before)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user