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