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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user