From 5b5537aa0fc40349793822f9a5d2b861b689a84f Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Sun, 18 Oct 2026 14:58:04 +0000 Subject: [PATCH] agent-exec transport 0579081fa29ea1bd6d1d532a7a177e36/f55c4a162b49 --- tests/test_task_recovery.py | 139 ++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/test_task_recovery.py b/tests/test_task_recovery.py index 7e5856ac..12eb08cc 100644 --- a/tests/test_task_recovery.py +++ b/tests/test_task_recovery.py @@ -519,6 +519,145 @@ class TaskRecoveryTests(unittest.TestCase): self.assertEqual(receipt.read_bytes(), original) self.assertIn(receipt_sha, child_card.read_text()) + def test_local_child_resume_rejects_reachable_missing_parent(self): + """Child: configure the task card as child with + immediate_target=feature/parent, create that parent branch on the + bare remote AND locally at the recorded parent_head_sha, capture + a local receipt, then remove ONLY refs/heads/feature/parent from + the bare remote while keeping origin reachable (the bare remote + still serves refs/heads/main). `task resume` in local mode must + refuse with REMOTE_UNVERIFIED instead of silently accepting the + local parent reference: the registered immediate parent branch + is absent remotely, so the local-only reference cannot be + reconciled against the authoritative origin even though it + exists in the worktree. + + Currently RED: resolve_parent_ref() does not distinguish + `ls-remote rc=2` (ref missing on a reachable origin) from the + unreachable branch. The function falls through the + `elif [ $rc -ne 2 ]` guard and lets `local_ref` satisfy the + resolver, so cmd_resume reaches IMPORT_RECEIPT and prints + `[resumed]` even though origin is fully reachable and just + missing the registered parent ref. Post-fix the local path must + print `[verdict] REMOTE_UNVERIFIED — origin is reachable but + feature/parent is missing; preserve local work, receipt not + imported` and refuse to import the receipt. + + The fixture mirrors + test_local_child_resume_uses_local_parent_when_origin_is_unreachable + but adds the local refs/heads/feature/parent branch and only + deletes the bare remote ref — the worktree URL stays on the + disposable bare remote and main remains fetchable, so the only + authority gap is the absent remote parent ref.""" + f = self.fixture + r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip() + # (1) Create the parent initiative branch in the main repo and + # push it to the bare remote so the worktree can fetch it once + # at the start, then create the local refs/heads/feature/parent + # branch inside the worktree (the post-fix path must refuse + # even when this local branch exists), branch off the parent + # tip into feature/parent-child, then switch the worktree to + # the child branch. + f.git(f.repo, "checkout", "-b", "feature/parent") + (f.repo / "parent-tip").write_text("authorized parent initiative tip\n") + f.git(f.repo, "add", "parent-tip") + f.git(f.repo, "commit", "-m", "Parent initiative tip commit") + parent_head = f.git(f.repo, "rev-parse", "HEAD").stdout.strip() + f.git(f.repo, "push", "origin", "feature/parent") + f.git(self.wt, "fetch", "origin", + "feature/parent:refs/remotes/origin/feature/parent") + # The shared refs/heads namespace means the worktree already + # sees refs/heads/feature/parent (it was created in f.repo via + # `checkout -b feature/parent`); the test must reject even + # when this local branch exists, so leave it intact. + self.assertEqual( + f.git(self.wt, "rev-parse", "--verify", + "refs/heads/feature/parent").stdout.strip(), parent_head) + f.git(self.wt, "branch", "feature/parent-child", parent_head) + f.git(self.wt, "checkout", "feature/parent-child") + # (2) Write the child card on the child branch so branch_card + # finds it (the file is named after the branch slug) and the + # resume's immediate_target + parent_head_sha fields resolve + # to feature/parent and the recorded parent tip. + child_card = self.wt / ".task" / "feature-parent-child.md" + self.assertFalse(child_card.exists()) + child_card.parent.mkdir(exist_ok=True) + child_card.write_text( + "# feature/parent-child\n" + "任务主记录:fixture;状态:进行中;owner:Lifecycle Test\n" + f"- 权威仓:fixture;目标分支:main;R0(initial_base_sha)= {r0}\n" + f"- 工作区:{self.wt}(worktree);分支:feature/parent-child\n" + "- 分支类型:child;initiative_id:feature-parent;root_target:main;" + "immediate_target:feature/parent;accepting_requirements:false;" + "parent_branch:feature/parent;" + f"parent_head_sha:{parent_head};" + "parent_pr:无;parent_source:local(非正式准入)\n" + ) + f.git(self.wt, "add", str(child_card.relative_to(self.wt))) + f.git(self.wt, "commit", "-m", "Register child fixture task") + # (3) Capture a local receipt on the child branch BEFORE + # removing the bare remote ref, so the receipt reflects the + # state where the remote parent ref is still present. + receipt = self.capture() + original = receipt.read_bytes() + receipt_sha = json.loads(original)["receipt_sha256"] + head = f.git(self.wt, "rev-parse", "HEAD").stdout + child_card_before = child_card.read_bytes() + # (4) Remove ONLY refs/heads/feature/parent from the bare + # remote. The worktree's origin URL still points at the + # disposable bare remote and refs/heads/main stays intact, so + # the only gap in authority is the absent remote parent ref. + # The local refs/heads/feature/parent branch remains in the + # worktree (this is the "even if a local parent branch + # exists" half of the scenario). + self.assertEqual( + f.git(f.remote, "rev-parse", "--verify", + "refs/heads/feature/parent").stdout.strip(), parent_head) + f.git(f.remote, "update-ref", "-d", "refs/heads/feature/parent") + # Confirm the bare remote now refuses to serve the parent ref + # while still serving main (so origin is reachable, just + # missing the registered immediate parent ref). + self.assertNotEqual( + f.git(f.remote, "show-ref", "--verify", "--hash", + "refs/heads/feature/parent", check=False).returncode, 0) + self.assertEqual( + f.git(f.remote, "rev-parse", "--verify", + "refs/heads/main").stdout.strip(), r0) + # (5) Local resume must refuse with REMOTE_UNVERIFIED instead + # of silently accepting the local parent ref. No [resumed] + # line, the receipt is not imported (card bytes unchanged and + # receipt_sha256 not embedded), the receipt file is byte-for- + # byte unchanged, and HEAD is preserved. + result = self.resume(receipt, mode="local") + self.assertNotEqual(result.returncode, 0, + f"local child resume must reject when origin is reachable " + f"but the registered parent ref is missing; current code " + f"falls through resolve_parent_ref's rc=2 case and accepts " + f"the local parent, returning 0 and printing [resumed]; " + f"stdout={result.stdout}") + self.assertIn("[verdict] REMOTE_UNVERIFIED", result.stdout, + "the resume must surface REMOTE_UNVERIFIED for the reachable-but-" + "missing-parent scenario so the owner can reconcile the parent " + f"initiative instead of silently resuming on an unsynced tip; " + f"stdout={result.stdout}") + self.assertNotIn("[resumed]", result.stdout, + "the resume must NOT import the receipt under a reachable " + "origin whose registered parent ref is missing — accepting " + "the local-only parent would advertise delivery readiness " + f"for an initiative the worktree cannot reconcile; stdout={result.stdout}") + self.assertEqual(child_card.read_bytes(), child_card_before, + "the child card must not be updated with receipt_sha256 when " + "the resume refuses to import the receipt") + self.assertEqual(receipt.read_bytes(), original, + "the receipt file must be byte-for-byte unchanged when the " + "resume refuses to import it") + self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head, + "the child HEAD must not move when the resume refuses to " + "import the receipt") + self.assertNotIn(receipt_sha, child_card.read_text(), + "the child card must not embed the receipt sha256 when " + "the resume refuses to import the receipt") + def test_local_precheck_rejects_reachable_uncomparable_target(self): """Origin advertises a target SHA but the advertised object is unavailable locally, so ancestry cannot be compared against the