Slice 8: v1 ships — integration coverage, runbook, spec corrections

- Five new integration test files raise the suite from 75 to 96 green:
  test_hygiene_vertical (7), test_branch_path_routing (4),
  test_metadata_pr_merge (3), test_cache_bootstrap (4), test_e2e_smoke
  (3). The smoke test walks propose → super-draft → edit branch →
  body-edit PR → graduate → active-RFC PR → merge → notification →
  hygiene-sweep deletion end-to-end.
- deploy/RUNBOOK.md replaces the prior DEPLOY.md stub as a real
  runbook: prerequisites, first-time bring-up, day-2 ops (logs, DB
  backup, secret rotation, the §12 hygiene cadence), rollback shape,
  troubleshooting table.
- backend/.env.example grows the SMTP block, HYGIENE_TICK_SECONDS,
  and WEBHOOK_EMAIL_BOUNCE_SECRET with inline commentary.
- README points to RUNBOOK.md; the "what the build lets you do"
  section adds Slices 7 and 8.
- docs/DEV.md gets a Slice 8 — shipped section; the "Next slice"
  footer becomes the v1-complete epitaph.
- SPEC corrections per the §19.3 working agreement: §10.7 names the
  shared §12 sweep; §12 names the bot as actuator and the per-user
  branch_chat_seen preservation contract; §19.1 marks v1 complete
  and records Slice 8; the five §19.2 candidates Slice 8 folded in
  are marked settled with pointers at the resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-25 04:14:50 -07:00
parent 1a0c4428af
commit 36635049c7
11 changed files with 1585 additions and 410 deletions
+260
View File
@@ -0,0 +1,260 @@
"""End-to-end integration test for the §19.2 "cache bootstrap from a
pre-existing meta repo" candidate that Slice 8 settles.
Per §4.1, the cache is rebuildable from Gitea. Per §15.9, the cache
resolves the actor on a PR by joining against the §6.5 `actions` log,
falling back to the `On-behalf-of:` trailer in the PR body, then to
the raw Gitea login as last resort. Slice 1 chose this fallback chain
and Slice 8 exercises it against history the bot did not author —
the disaster-recovery / transferred-meta-repo case.
The tests prove:
* When `actions` carries a matching row, the audit log wins —
`opened_by` reads the on-behalf-of login regardless of the bot's
appearance as Gitea opener.
* When `actions` is empty but the PR body carries
`On-behalf-of: Name <login>`, the trailer parses cleanly.
* When both are absent, the raw Gitea opener wins (and if even
that is the bot, the bot login is what surfaces — the v1
fallback, honest about who Gitea sees).
* The §15.9 framing holds: the bot login surfaces only in the
Git log and the trailer, never inside the audit-log path.
"""
from __future__ import annotations
import json
import pytest
from test_propose_vertical import ( # noqa: F401
FakeGitea,
app_with_fake_gitea,
provision_user_row,
sign_in_as,
tmp_env,
)
def _seed_meta_pr_directly_in_fake(
fake: FakeGitea,
*,
slug: str,
pr_number: int,
head_branch: str,
title: str,
body: str,
gitea_opener_login: str,
) -> None:
"""Push a PR into FakeGitea as if it existed before the app cache
was bootstrapped — no propose endpoint, no audit log row, just a
pull on the meta repo with a chosen `user.login`. The reconciler
pulls this on its first sweep."""
pr = {
"number": pr_number,
"title": title,
"body": body,
"head": {"ref": head_branch, "sha": "sha-bootstrap"},
"base": {"ref": "main"},
"state": "open",
"merged": False,
"merged_at": None,
"closed_at": None,
"created_at": "2026-05-23T00:00:00Z",
"user": {"login": gitea_opener_login},
}
fake.pulls.setdefault(("wiggleverse", "meta"), []).append(pr)
# Create a corresponding branch so refresh_meta_branches sees it.
fake.branches[("wiggleverse", "meta")][head_branch] = {
"sha": "sha-bootstrap", "ts": "2026-05-23T00:00:00Z",
}
def test_audit_log_first_wins_over_bot_opener(app_with_fake_gitea):
"""If `actions` carries a row for this PR, that's the authoritative
actor — even when Gitea reports the bot as the opener. This is the
common steady-state path (the bot opened on behalf of a human, the
audit log captured it)."""
from fastapi.testclient import TestClient
from app import cache as cache_mod, db
app, fake = app_with_fake_gitea
with TestClient(app) as client:
# Seed a cached RFC and an `actions` row for the PR before
# the reconciler runs.
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('alpha', 'Alpha', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
db.conn().execute(
"""
INSERT INTO actions
(actor_user_id, on_behalf_of, action_kind, rfc_slug, pr_number)
VALUES (NULL, 'alice', 'propose_rfc', 'alpha', 99)
"""
)
# Seed the fake's PR as if Gitea reports the bot as opener.
_seed_meta_pr_directly_in_fake(
fake,
slug="alpha", pr_number=99,
head_branch="propose/alpha",
title="Propose: Alpha",
body="Some idea\n",
gitea_opener_login="rfc-bot",
)
import asyncio
asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))
row = db.conn().execute(
"SELECT opened_by FROM cached_prs WHERE pr_number = 99"
).fetchone()
assert row is not None
# Audit-log row wins.
assert row["opened_by"] == "alice"
def test_trailer_parses_when_audit_log_missing(app_with_fake_gitea):
"""The cache-bootstrap-against-history-the-bot-did-not-author case:
no `actions` rows, but the PR body carries the §6.5
`On-behalf-of:` trailer. The trailer parses cleanly and the right
actor surfaces."""
from fastapi.testclient import TestClient
from app import cache as cache_mod, db
app, fake = app_with_fake_gitea
with TestClient(app) as client:
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('beta', 'Beta', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
# No actions row. PR body carries the trailer.
_seed_meta_pr_directly_in_fake(
fake,
slug="beta", pr_number=42,
head_branch="propose/beta",
title="Propose: Beta",
body="A new framing\n\nOn-behalf-of: Charlie <charlie>",
gitea_opener_login="rfc-bot",
)
import asyncio
asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))
row = db.conn().execute(
"SELECT opened_by FROM cached_prs WHERE pr_number = 42"
).fetchone()
assert row["opened_by"] == "charlie"
def test_raw_gitea_login_used_when_audit_and_trailer_both_absent(app_with_fake_gitea):
"""When neither the audit log nor the trailer carries an actor,
the raw Gitea login is the last-resort fallback per §15.9 / Slice 1.
A non-bot login surfaces as the actor; a bot login surfaces as
the bot (honest about what Gitea sees — the v1 contract)."""
from fastapi.testclient import TestClient
from app import cache as cache_mod, db
app, fake = app_with_fake_gitea
with TestClient(app) as client:
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('gamma', 'Gamma', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
# No audit row, no trailer — but Gitea reports a real user as opener.
_seed_meta_pr_directly_in_fake(
fake,
slug="gamma", pr_number=7,
head_branch="propose/gamma",
title="Propose: Gamma",
body="No trailer here\n",
gitea_opener_login="dana", # a real human, not the bot
)
import asyncio
asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))
row = db.conn().execute(
"SELECT opened_by FROM cached_prs WHERE pr_number = 7"
).fetchone()
assert row["opened_by"] == "dana"
def test_full_reconciler_sweep_resolves_actors_via_fallback_chain(app_with_fake_gitea):
"""End-to-end: a clean `cached_*` set plus a meta repo that has
history with no audit-log rows. The reconciler's first sweep must
bring everything up correctly — every PR resolves an actor through
the fallback chain without crashing."""
from fastapi.testclient import TestClient
from app import cache as cache_mod, db
app, fake = app_with_fake_gitea
with TestClient(app) as client:
# Three PRs, three fallback paths exercised.
# 1. With audit-log row.
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('alpha', 'Alpha', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
db.conn().execute(
"""
INSERT INTO actions (actor_user_id, on_behalf_of, action_kind, rfc_slug, pr_number)
VALUES (NULL, 'alice', 'propose_rfc', 'alpha', 1)
"""
)
_seed_meta_pr_directly_in_fake(
fake, slug="alpha", pr_number=1,
head_branch="propose/alpha", title="A", body="b",
gitea_opener_login="rfc-bot",
)
# 2. Trailer only.
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('beta', 'Beta', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
_seed_meta_pr_directly_in_fake(
fake, slug="beta", pr_number=2,
head_branch="propose/beta", title="B",
body="On-behalf-of: Bob <bob>",
gitea_opener_login="rfc-bot",
)
# 3. Gitea opener as last resort.
db.conn().execute(
"""
INSERT INTO cached_rfcs
(slug, title, state, owners_json, arbiters_json, tags_json, body)
VALUES ('gamma', 'Gamma', 'super-draft', '[]', '[]', '[]', 'pitch')
"""
)
_seed_meta_pr_directly_in_fake(
fake, slug="gamma", pr_number=3,
head_branch="propose/gamma", title="G", body="naked",
gitea_opener_login="carol",
)
import asyncio
asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))
prs = {
r["pr_number"]: r["opened_by"]
for r in db.conn().execute(
"SELECT pr_number, opened_by FROM cached_prs ORDER BY pr_number"
)
}
assert prs[1] == "alice" # audit log wins
assert prs[2] == "bob" # trailer wins
assert prs[3] == "carol" # raw Gitea opener wins