"""End-to-end integration tests for the Slice 4 vertical (§9.4–§9.7 in full). Walks the super-draft body-editing path end-to-end against the in-process FakeGitea from test_propose_vertical.py: * Seed a super-draft from the propose+merge flow already proven by Slice 1. * GET /api/rfcs//main returns the canonical body + breadcrumb data. * POST /api/rfcs//start-edit-branch cuts a meta-repo edit branch. * GET /api/rfcs//branches/ returns the body extracted from the entry envelope, ready for the editor. * POST .../changes//accept commits to rfcs/.md on the edit branch, with the frontmatter preserved. * POST .../manual-flush commits a manual edit similarly. * POST .../open-pr opens a meta_body_edit PR. * POST .../prs//merge propagates the body to meta-main, where it surfaces back into cached_rfcs.body for the next catalog render. * POST .../metadata opens a metadata PR; merge propagates the title. * Withdraw the body-edit PR; the body on the edit branch is untouched but the cache shows state='withdrawn'. The active-RFC PR flow tests in test_pr_flow_vertical.py exercise the parallel structural surface; this file's job is to prove the dispatch works against the meta-repo target uniformly. """ from __future__ import annotations import pytest from test_propose_vertical import ( # noqa: F401 FakeGitea, app_with_fake_gitea, provision_user_row, sign_in_as, tmp_env, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def seed_super_draft(fake: FakeGitea, *, slug: str, title: str, pitch: str, proposed_by: str = "alice", tags: list[str] | None = None) -> None: """Seed a super-draft entry directly on meta-main, skipping the propose+merge round-trip the Slice 1 tests cover separately. The cache is also primed so the API doesn't have to wait for a reconciler sweep before exercising super-draft endpoints. """ import json as _json import yaml from app import db fm = { "slug": slug, "title": title, "state": "super-draft", "id": None, "repo": None, "proposed_by": proposed_by, "proposed_at": "2026-05-23", "graduated_at": None, "graduated_by": None, "owners": [], "arbiters": [], "tags": tags or [], } body = pitch.strip() + "\n" entry_text = f"---\n{yaml.safe_dump(fm, sort_keys=False).rstrip()}\n---\n\n{body}" sha = fake._next_sha() fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = { "content": entry_text, "sha": sha, } # Advance meta-main's tip sha so the bot's create_branch snapshots # the freshly-seeded file. (Otherwise the branch snapshot starts # empty and the read fails until the next file commit.) fake.branches[("wiggleverse", "meta")]["main"]["sha"] = sha db.conn().execute( """ INSERT OR REPLACE INTO cached_rfcs (slug, title, state, rfc_id, repo, proposed_by, proposed_at, owners_json, arbiters_json, tags_json, body, body_sha, last_main_commit_at, last_entry_commit_at) VALUES (?, ?, 'super-draft', NULL, NULL, ?, '2026-05-23', ?, ?, ?, ?, ?, datetime('now'), datetime('now')) """, ( slug, title, proposed_by, _json.dumps([]), _json.dumps([]), _json.dumps(tags or []), body, sha, ), ) # Synthesize the per-slug meta-main row so has-commits-ahead works # without waiting for the reconciler. db.conn().execute( """ INSERT OR REPLACE INTO cached_branches (rfc_slug, branch_name, head_sha, state, last_commit_at) VALUES (?, 'main', ?, 'open', datetime('now')) """, (slug, sha), ) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- PITCH = ( "Open Human Model is a framework for representing humans.\n\n" "It defines consent, trait, and agency in compatible terms." ) def test_super_draft_main_view_returns_body_and_breadcrumb(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="Open Human Model", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.get("/api/rfcs/ohm/main") assert r.status_code == 200, r.text d = r.json() assert d["state"] == "super-draft" assert d["id"] is None assert d["repo"] is None assert "Open Human Model is a framework" in d["body"] # No edit branches yet; the dropdown is empty above 'canonical body'. assert d["branches"] == [] assert d["open_prs"] == [] def test_start_edit_branch_cuts_meta_branch_and_writes_cache(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.post("/api/rfcs/ohm/start-edit-branch", json={}) assert r.status_code == 200, r.text branch_name = r.json()["branch_name"] # §9.5: edit--<6hex> per Slice 4's naming convention. assert branch_name.startswith("edit-ohm-") # The branch landed on Gitea. assert branch_name in fake.branches[("wiggleverse", "meta")] # The bot's audit row records the gesture. rows = db.conn().execute( "SELECT action_kind, on_behalf_of, branch_name FROM actions WHERE action_kind = 'create_branch'" ).fetchall() assert any(r["branch_name"] == branch_name and r["on_behalf_of"] == "alice" for r in rows) # cached_branches sees the new row. cached = db.conn().execute( "SELECT branch_name FROM cached_branches WHERE rfc_slug = 'ohm' AND branch_name = ?", (branch_name,), ).fetchone() assert cached is not None def test_branch_view_extracts_body_from_entry_envelope(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"] r = client.get(f"/api/rfcs/ohm/branches/{branch}") assert r.status_code == 200, r.text view = r.json() # The frontmatter is stripped — the editable body is the pitch. assert view["body"].startswith("Open Human Model is a framework") assert "---" not in view["body"] assert view["main_thread_id"] def test_accept_change_commits_to_meta_repo_and_preserves_frontmatter(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db, entry as entry_mod app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"] view = client.get(f"/api/rfcs/ohm/branches/{branch}").json() thread_id = view["main_thread_id"] cur = db.conn().execute( """ INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state, original, proposed, reason) VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'tightens scope') """, ( branch, thread_id, "Open Human Model is a framework for representing humans.", "Open Human Model is a framework for representing humans across software systems.", ), ) change_id = cur.lastrowid r = client.post( f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept", json={ "proposed": "Open Human Model is a framework for representing humans across software systems.", "was_edited_before_accept": False, }, ) assert r.status_code == 200, r.text commit_sha = r.json()["commit_sha"] assert commit_sha # The file on the meta-repo edit branch carries both the # frontmatter and the mutated body — the round-trip preserved # the envelope. file_content = fake.files[("wiggleverse", "meta", branch, "rfcs/ohm.md")]["content"] entry = entry_mod.parse(file_content) assert entry.state == "super-draft" assert entry.slug == "ohm" assert "across software systems" in entry.body # The cached change row tracks the commit. row = db.conn().execute( "SELECT state, commit_sha FROM changes WHERE id = ?", (change_id,) ).fetchone() assert row["state"] == "accepted" assert row["commit_sha"] == commit_sha def test_manual_flush_commits_through_frontmatter_envelope(app_with_fake_gitea): from fastapi.testclient import TestClient from app import entry as entry_mod, db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"] new_body = PITCH + "\n\nA fresh closing paragraph that landed manually.\n" r = client.post( f"/api/rfcs/ohm/branches/{branch}/manual-flush", json={"new_content": new_body, "paragraph_count": 1}, ) assert r.status_code == 200, r.text assert r.json()["commit_sha"] file_content = fake.files[("wiggleverse", "meta", branch, "rfcs/ohm.md")]["content"] entry = entry_mod.parse(file_content) assert "fresh closing paragraph" in entry.body # §10.6: a system-author chat message records the flush. rows = db.conn().execute( """ SELECT m.role, m.text FROM thread_messages m JOIN threads t ON t.id = m.thread_id WHERE t.rfc_slug = 'ohm' AND t.branch_name = ? """, (branch,), ).fetchall() assert any(r["role"] == "system" and "manual edit" in r["text"] for r in rows) def test_open_pr_on_super_draft_lands_as_meta_body_edit(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"] # Accept one change so the branch has commits ahead of meta-main. view = client.get(f"/api/rfcs/ohm/branches/{branch}").json() thread_id = view["main_thread_id"] cur = db.conn().execute( """ INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state, original, proposed, reason) VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'test') """, ( branch, thread_id, "Open Human Model is a framework for representing humans.", "Open Human Model is a framework for representing humans across systems.", ), ) change_id = cur.lastrowid client.post( f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept", json={ "proposed": "Open Human Model is a framework for representing humans across systems.", "was_edited_before_accept": False, }, ) r = client.post( f"/api/rfcs/ohm/branches/{branch}/open-pr", json={"title": "Tighten scope", "description": "Scope to systems."}, ) assert r.status_code == 200, r.text pr_number = r.json()["pr_number"] # cached_prs records pr_kind='meta_body_edit'. row = db.conn().execute( "SELECT pr_kind, repo, head_branch FROM cached_prs WHERE pr_number = ?", (pr_number,), ).fetchone() assert row["pr_kind"] == "meta_body_edit" assert row["repo"] == "wiggleverse/meta" assert row["head_branch"] == branch # The §10.3 PR view payload renders against the meta repo with # the body extracted from the envelope on both sides. d = client.get(f"/api/rfcs/ohm/prs/{pr_number}").json() assert "across systems" in d["branch_body"] assert "across systems" not in d["main_body"] assert d["state"] == "open" def test_merge_super_draft_body_edit_propagates_to_canonical_body(app_with_fake_gitea): """The whole §9.5 loop end-to-end: cut an edit branch, accept a change, open a body-edit PR, merge it, and watch the super-draft's cached body update.""" from fastapi.testclient import TestClient from app import db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") provision_user_row(user_id=1, login="ben", role="owner") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"] view = client.get(f"/api/rfcs/ohm/branches/{branch}").json() thread_id = view["main_thread_id"] cur = db.conn().execute( """ INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state, original, proposed, reason) VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'tightens') """, ( branch, thread_id, "It defines consent, trait, and agency in compatible terms.", "It defines consent, trait, harm, and agency in compatible terms.", ), ) change_id = cur.lastrowid client.post( f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept", json={ "proposed": "It defines consent, trait, harm, and agency in compatible terms.", "was_edited_before_accept": False, }, ) pr_number = client.post( f"/api/rfcs/ohm/branches/{branch}/open-pr", json={"title": "Add harm", "description": "Adds harm to the dimension list."}, ).json()["pr_number"] # An unclaimed super-draft: only app admins/owners can merge per §9.5. # Alice is a contributor — refused. r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge") assert r.status_code == 403 # Ben (app owner) can merge. sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner") r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge") assert r.status_code == 200, r.text # Meta-main's rfcs/ohm.md now carries the body change; the # cache picks it up; the catalog/view render the new body. r = client.get("/api/rfcs/ohm/main") d = r.json() assert "harm" in d["body"] def test_metadata_pane_pr_propagates_title_on_merge(app_with_fake_gitea): from fastapi.testclient import TestClient from app import db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=1, login="ben", role="owner") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner") r = client.post( "/api/rfcs/ohm/metadata", json={"title": "Open Human Model", "tags": ["identity", "schema"]}, ) assert r.status_code == 200, r.text pr_number = r.json()["pr_number"] metadata_branch = r.json()["branch_name"] assert metadata_branch.startswith("metadata-ohm-") # cached_prs records pr_kind='meta_metadata'. row = db.conn().execute( "SELECT pr_kind, title FROM cached_prs WHERE pr_number = ?", (pr_number,), ).fetchone() assert row["pr_kind"] == "meta_metadata" # Merge the metadata PR — the per-RFC PR-flow merge endpoint # works only for meta_body_edit (rfc_branch) kinds. For metadata # PRs, we exercise the bot's merge against the meta repo directly # via the underlying Gitea client. from app.bot import Actor bot = app.state.bot actor = Actor(user_id=1, gitea_login="ben", display_name="Ben", email="ben@test") import asyncio asyncio.run(_merge_meta_pr(bot, actor, pr_number=pr_number, slug="ohm")) # Refresh the meta cache so the new title surfaces. from app import cache as cache_mod asyncio.run(cache_mod.refresh_meta_repo(app.state.config, app.state.gitea)) asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea)) d = client.get("/api/rfcs/ohm").json() assert d["title"] == "Open Human Model" assert "identity" in d["tags"] async def _merge_meta_pr(bot, actor, *, pr_number, slug): """Helper: invoke the bot's merge path against the meta repo. The metadata-PR merge surface isn't exposed via api_prs (which only handles body-edit PRs) — admins/owners merge through Gitea directly via the bot for v1. A dedicated metadata-PR merge endpoint earns its own §19.2 candidate if usage shows demand.""" # We use merge_branch_pr as the bot's generic meta-merge primitive; # it takes owner/repo, which dispatches to the meta repo here. await bot.merge_branch_pr( actor, owner="wiggleverse", repo="meta", pr_number=pr_number, head_branch=f"metadata-{slug}-stub", # name only used in the audit log slug=slug, ) def test_super_draft_canonical_body_branch_main_is_read_only(app_with_fake_gitea): """branchParam='main' on a super-draft view fetches meta-main's rfcs/.md but contributing is refused — the only edit path is via start-edit-branch.""" from fastapi.testclient import TestClient from app import db app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.get("/api/rfcs/ohm/branches/main") assert r.status_code == 200, r.text view = r.json() assert view["branch_name"] == "main" assert "Open Human Model" in view["body"] # Capabilities reflect read-only. assert view["capabilities"]["can_contribute"] is False # A manual-flush against 'main' is refused as a contribute check. r = client.post( "/api/rfcs/ohm/branches/main/manual-flush", json={"new_content": "hijacked\n", "paragraph_count": 1}, ) assert r.status_code == 403 def test_metadata_pane_refused_for_plain_contributor(app_with_fake_gitea): """§9.5: until the §13.1 claim runs, the metadata pane is limited to app admins/owners. A plain contributor is refused.""" from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.post( "/api/rfcs/ohm/metadata", json={"title": "Sneak title"}, ) assert r.status_code == 403