agent-exec transport 15c16d890ff16327f11293fe6ecbfdde/f7c42d8ea545
This commit is contained in:
@@ -659,6 +659,34 @@ class EvidenceTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(message, result.stderr)
|
||||
|
||||
def test_local_continuation_rejects_receipt_target_branch_mismatch(self):
|
||||
"""Resume-local must refuse a receipt whose before.target_branch or
|
||||
after.target_branch points at a different branch than the registered
|
||||
task target. Re-sign the fixture receipt with the existing helper so
|
||||
the digest still validates, and assert the existing production
|
||||
rejection that flags the receipt as belonging to a different task
|
||||
branch (or, when the after snapshot already disagrees, the
|
||||
authoritative-target-changed guard that fires first)."""
|
||||
wt = self.local_task()
|
||||
original = self.capture("make precheck", cwd=wt)
|
||||
cases = (
|
||||
("before.target_branch",
|
||||
"Precheck belongs to a different task branch",
|
||||
lambda d: d["before"].update(target_branch="feature/other")),
|
||||
("after.target_branch",
|
||||
"Precheck authoritative target has changed",
|
||||
lambda d: d["after"].update(target_branch="feature/other")),
|
||||
)
|
||||
for name, message, change in cases:
|
||||
with self.subTest(case=name):
|
||||
data = json.loads(json.dumps(original))
|
||||
change(data)
|
||||
data["output_sha256"] = hashlib.sha256(data["output"].encode()).hexdigest()
|
||||
self.resign(data)
|
||||
result = self.validate(wt)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(message, result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -194,6 +194,61 @@ class TaskRecoveryTests(unittest.TestCase):
|
||||
# so a follow-up run can repeat the same check.
|
||||
self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward)
|
||||
|
||||
def test_local_precheck_rejects_reachable_target_divergence(self):
|
||||
"""Origin is reachable but its target was replaced by a divergent commit
|
||||
that shares R0 with the cached forward target without descending from it.
|
||||
Local precheck must refuse LOCAL_READY and surface the divergence so the
|
||||
owner can reconcile the target history instead of trusting the cache.
|
||||
|
||||
Both the cached forward target and the divergent commit are locally
|
||||
available so target_rewound() can compare them via merge-base."""
|
||||
f = self.fixture
|
||||
r0 = f.git(self.wt, "rev-parse", "origin/main").stdout.strip()
|
||||
# (1) Advance the remote and let local precheck refresh the cached origin/main.
|
||||
f.advance_remote()
|
||||
refresh = self.precheck()
|
||||
self.assertEqual(refresh.returncode, 0, refresh.stdout)
|
||||
forward = f.git(self.wt, "rev-parse", "origin/main").stdout.strip()
|
||||
self.assertNotEqual(forward, r0)
|
||||
# (2) Build a divergent commit on top of R0 in the main checkout; it shares
|
||||
# R0 with the cached forward target but does not descend from it.
|
||||
f.git(f.repo, "switch", "--detach", r0)
|
||||
(f.repo / "divergent-main").write_text("different target history\n")
|
||||
f.git(f.repo, "add", "divergent-main")
|
||||
f.git(f.repo, "commit", "-m", "Create divergent fixture history")
|
||||
divergent = f.git(f.repo, "rev-parse", "HEAD").stdout.strip()
|
||||
# Both targets must share R0 (otherwise they would just be unrelated refs).
|
||||
self.assertEqual(f.git(f.repo, "merge-base", forward, divergent).stdout.strip(), r0)
|
||||
# The cached forward target must NOT be an ancestor of the divergent commit.
|
||||
self.assertEqual(f.git(f.repo, "merge-base", "--is-ancestor", forward,
|
||||
divergent, check=False).returncode, 1)
|
||||
# Both commits must be locally available so target_rewound() can inspect them.
|
||||
self.assertEqual(f.git(self.wt, "cat-file", "-e", forward + "^{commit}",
|
||||
check=False).returncode, 0)
|
||||
self.assertEqual(f.git(self.wt, "cat-file", "-e", divergent + "^{commit}",
|
||||
check=False).returncode, 0)
|
||||
# (3) Push the divergent commit so the bare remote's object database sees it,
|
||||
# then replace refs/heads/main on the bare remote with the divergent commit.
|
||||
f.git(f.repo, "push", "origin", "HEAD:refs/heads/divergent")
|
||||
f.git(f.remote, "update-ref", "refs/heads/main", divergent, forward)
|
||||
self.assertEqual(f.git(f.remote, "rev-parse", "refs/heads/main").stdout.strip(), divergent)
|
||||
# The worktree cache still points at the forward descendant because we have
|
||||
# not fetched since.
|
||||
self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward)
|
||||
head_before = f.git(self.wt, "rev-parse", "HEAD").stdout
|
||||
# (4) Local precheck: fetch_target() refuses the non-fast-forward update to
|
||||
# the divergent commit, target_rewound() then surfaces the divergence, and
|
||||
# the precheck must not advertise LOCAL_READY.
|
||||
local = self.precheck()
|
||||
self.assertNotEqual(local.returncode, 0, local.stdout)
|
||||
self.assertNotIn("[verdict] LOCAL_READY", local.stdout)
|
||||
self.assertIn("[verdict] REMOTE_UNVERIFIED", local.stdout)
|
||||
self.assertIn("rewound or diverged", local.stdout)
|
||||
self.assertEqual(f.git(self.wt, "rev-parse", "HEAD").stdout, head_before)
|
||||
# After the failed fetch the cache still points at the forward descendant
|
||||
# so a follow-up run can repeat the same check.
|
||||
self.assertEqual(f.git(self.wt, "rev-parse", "origin/main").stdout.strip(), forward)
|
||||
|
||||
def test_unknown_work_mode_is_rejected(self):
|
||||
result = self.precheck(mode="anything")
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||
|
||||
Reference in New Issue
Block a user