agent-exec transport 0579081fa29ea1bd6d1d532a7a177e36/f55c4a162b49
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user