Slice 8 WIP: §12 hygiene + §10.7 + routing + rollback cleanup
- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
DigestScheduler shape; wires next to digest in main.py with the
same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
api_prs.py to {branch:path}; move the bare GET to the bottom of
the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
slashed-branch read; integration tests for the hygiene vertical
cover the 30d close, 90d delete, post-merge delete, pinned
exemption, per-user cursor preservation, no-notification rule,
and the graduation-rollback cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,8 +157,11 @@ class FakeGitea:
|
||||
items.append({"name": name, "commit": {"id": b["sha"], "timestamp": b.get("ts")}})
|
||||
return httpx.Response(200, json=items)
|
||||
|
||||
# GET /repos/{owner}/{repo}/branches/{branch}
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches/([^/]+)", path)
|
||||
# GET /repos/{owner}/{repo}/branches/{branch}. Branch name may
|
||||
# contain slashes per the §19.2 path-routing candidate Slice 8
|
||||
# settles — the FakeGitea matcher mirrors what real Gitea
|
||||
# accepts on the wire.
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches/(.+)", path)
|
||||
if method == "GET" and m:
|
||||
owner, repo, branch = m.groups()
|
||||
b = self.branches.get((owner, repo), {}).get(branch)
|
||||
@@ -166,6 +169,25 @@ class FakeGitea:
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
return httpx.Response(200, json={"name": branch, "commit": {"id": b["sha"]}})
|
||||
|
||||
# DELETE /repos/{owner}/{repo}/branches/{branch} — Slice 8 §12
|
||||
# hygiene actuator and the graduation-rollback branch cleanup
|
||||
# both reach this endpoint via `bot.delete_branch`. Branch path
|
||||
# may contain slashes (the §19.2 path-routing candidate) so the
|
||||
# regex catches the rest-of-path.
|
||||
m_delbr = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches/(.+)", path)
|
||||
if method == "DELETE" and m_delbr:
|
||||
owner, repo, branch = m_delbr.groups()
|
||||
br_map = self.branches.get((owner, repo), {})
|
||||
if branch not in br_map:
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
br_map.pop(branch, None)
|
||||
# Drop the branch's files too so a subsequent read 404s.
|
||||
self.files = {
|
||||
k: v for k, v in self.files.items()
|
||||
if not ((k[0], k[1], k[2]) == (owner, repo, branch))
|
||||
}
|
||||
return httpx.Response(204, json={})
|
||||
|
||||
# POST /repos/{owner}/{repo}/branches
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches", path)
|
||||
if method == "POST" and m:
|
||||
|
||||
Reference in New Issue
Block a user