fix: three defects found by codex review of today's merged work

No P1s. All three confirmed in the code before fixing.

[P2] Invites were destroyed by a transient forge error. apply_invites() popped
the whole pending list BEFORE attempting the collaborator PUT, so a 502 or a
timeout while someone first signed in meant they got no access and re-linking
never retried -- the promise was gone. Now it peeks, and consumes each grant
only after that grant actually lands. A partial failure keeps exactly the
grants that failed.

[P2] Snapshots lost staged-only work. build_snapshot() read HEAD into a scratch
index and staged the WORKTREE, so a hunk you staged and then edited further
survived only in its later worktree form. git keeps index and worktree as
separate states and the backup now does too: the real index is read without
being touched, and when it differs from both HEAD and the worktree it rides
along as a second parent.

[P3] A truncated repo listing could resolve a bare name to the WRONG repo.
resolve_granted() discarded the truncation flag, so a name whose only match sat
beyond the 2000-repo cap fell back to <login>/<name> and would clone that
instead. Truncation now means "unknown", not "absent": it refuses and asks for
the owner. A complete listing still falls back, because absence is then real.

209 tests (was 204).
This commit is contained in:
claude
2026-08-23 16:14:21 -04:00
parent 9511093b14
commit 062f8d1863
4 changed files with 158 additions and 12 deletions
+48
View File
@@ -1310,5 +1310,53 @@ class TestDeviceFlowOutputIsVisible(unittest.TestCase):
self.assertTrue(all(seen[:2]),
"the URL and code must be printed with flush=True")
class TestCodexReviewFindings(GitScenarioBase):
"""Regressions for the three issues codex found in today's merged work."""
def test_staged_only_work_survives_a_snapshot(self):
"""[P2] Stage a hunk, edit further, lose the laptop: the staged version
must still be recoverable, not just the later worktree one."""
self.write(self.local, "a.txt", "committed")
run_git(self.local, "add", "-A")
run_git(self.local, "commit", "-m", "base")
self.write(self.local, "a.txt", "THE CAREFULLY STAGED VERSION")
run_git(self.local, "add", "a.txt") # staged
self.write(self.local, "a.txt", "later scratch edit") # worktree moved on
commit, tree = client.build_snapshot(self.local)
# the worktree state is the snapshot's own tree
self.assertEqual(run_git(self.local, "show", f"{commit}:a.txt"),
"later scratch edit")
# ...and the staged state is reachable through the extra parent
parents = run_git(self.local, "log", "-1", "--format=%P", commit).split()
staged = [p for p in parents
if run_git(self.local, "show", f"{p}:a.txt")
== "THE CAREFULLY STAGED VERSION"]
self.assertTrue(staged, f"staged version unreachable from {parents}")
def test_a_snapshot_does_not_disturb_the_index(self):
self.write(self.local, "a.txt", "one")
run_git(self.local, "add", "-A")
run_git(self.local, "commit", "-m", "base")
self.write(self.local, "a.txt", "staged")
run_git(self.local, "add", "a.txt")
before = run_git(self.local, "status", "--porcelain")
client.build_snapshot(self.local)
self.assertEqual(run_git(self.local, "status", "--porcelain"), before)
def test_a_truncated_listing_refuses_instead_of_guessing(self):
"""[P3] A name that is merely beyond the page cap must not resolve to a
different repo that happens to exist under your own account."""
cfg = {"login": "alice", "gitea_base": "http://forge.example", "token": "t"}
with self.assertRaises(SystemExit) as e:
client.resolve_granted(cfg, "notes", repos=[], truncated=True)
self.assertIn("truncated", str(e.exception))
# a complete listing still falls back, because absence is then real
self.assertEqual(
client.resolve_granted(cfg, "notes", repos=[], truncated=False),
"alice/notes")
if __name__ == "__main__":
unittest.main()
+41
View File
@@ -960,6 +960,47 @@ class TestInvites(ServiceTestBase):
_, mine = self.svc.audit_read({"token": self.alice["token"]})
self.assertIn("invite", [e["event"] for e in mine["events"]])
class TestInviteSurvivesAFailedGrant(ServiceTestBase):
"""[P2, codex] A transient forge error must not destroy the promise."""
def setUp(self):
super().setUp()
self.enable_test_mode()
self.alice = self.stub_link("s1", "alice", email="[email protected]",
verified=True, device_id="dev-a")[1]
StubUpstream.state["repo_owner"]["alice/notes"] = "alice"
StubUpstream.state["repo_owner"]["alice/reports"] = "alice"
def test_a_failed_grant_leaves_the_invite_pending_for_next_time(self):
self.svc.invite({"token": self.alice["token"], "email": "[email protected]",
"repos": ["notes"]})
# the forge loses the repo mid-flight -> the PUT 404s
StubUpstream.state["repo_owner"].pop("alice/notes")
status, resp = self.stub_link("s9", "carol", email="[email protected]",
verified=True, device_id="dev-c")
self.assertEqual(status, 200) # sign-in still succeeds
self.assertIsNone(resp.get("granted_repos"))
# the promise is STILL THERE rather than silently consumed
self.assertEqual([g["repo"] for g in self.svc.state.peek_invites("[email protected]")],
["alice/notes"])
# and it lands on the next link, once the repo is back
StubUpstream.state["repo_owner"]["alice/notes"] = "alice"
status, resp = self.stub_link("s9", "carol", email="[email protected]",
verified=True, device_id="dev-c2")
self.assertEqual(resp.get("granted_repos"), ["alice/notes"])
self.assertEqual(self.svc.state.peek_invites("[email protected]"), [])
def test_a_partial_failure_only_consumes_what_landed(self):
self.svc.invite({"token": self.alice["token"], "email": "[email protected]",
"repos": ["notes", "reports"]})
StubUpstream.state["repo_owner"].pop("alice/reports") # one of two fails
_, resp = self.stub_link("s10", "dan", email="[email protected]",
verified=True, device_id="dev-d")
self.assertEqual(resp.get("granted_repos"), ["alice/notes"])
self.assertEqual([g["repo"] for g in self.svc.state.peek_invites("[email protected]")],
["alice/reports"])
if __name__ == "__main__":
unittest.main()