Slice 5: graduation per §13
The §13.3 transactional sequence flips a super-draft to active — five steps with paired undoes, an in-process orchestrator fed by an asyncio.Queue, the §17 SSE endpoint streaming step transitions to the dialog. Each step is a new bot primitive that logs an `actions` row, bracketed by `graduate_start` / `graduate_complete` for the linkable audit sequence. Rollback runs the undoes in reverse from the last completed step; merge_pr has no undo by design per §13.5. The §9.8 precondition gate is enforced server-side at the top of POST /graduate so the §13.3 rollback complexity does not grow. The §13.4 chat migration is a database semantic no-op — the (slug, branch_name='main') threads keep their identity, only the interpretation changes. The §9.8 pre-graduation history surfaces via a new _is_meta_target(rfc, branch) dispatch helper and lands as pre_graduation_history on /main. §13.1 claim flow landed alongside since it's the prerequisite for non-admin graduation — bot.open_claim_pr plus broadening api_prs._require_pr to accept meta_claim. 45/45 tests green; ten new integration tests cover the validator, the §9.8 precondition refusal, happy path with audit verification, mid-sequence rollback at steps 2 and 3, concurrent refusal, chat-survives-without-data-movement, pre-graduation history, and the §13.1 claim PR cycle. SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew four candidates surfaced during the slice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,18 @@ class FakeGitea:
|
||||
return httpx.Response(200, json={"name": repo, "full_name": f"{owner}/{repo}"})
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
|
||||
# DELETE /repos/{owner}/{repo} — Slice 5 graduation rollback uses
|
||||
# this to undo step 1 (repo create). The FakeGitea drops every
|
||||
# file, branch, and PR tied to the repo so a subsequent retry
|
||||
# graduation can re-create the repo cleanly.
|
||||
if method == "DELETE" and m_repo:
|
||||
owner, repo = m_repo.groups()
|
||||
self.repos.discard((owner, repo))
|
||||
self.branches.pop((owner, repo), None)
|
||||
self.pulls.pop((owner, repo), None)
|
||||
self.files = {k: v for k, v in self.files.items() if (k[0], k[1]) != (owner, repo)}
|
||||
return httpx.Response(204, json={})
|
||||
|
||||
# POST /orgs/{org}/repos
|
||||
m = re.fullmatch(r"/orgs/([^/]+)/repos", path)
|
||||
if method == "POST" and m:
|
||||
|
||||
Reference in New Issue
Block a user