"""End-to-end integration test for the §19.2 "in-app merge for metadata PRs" candidate that Slice 8 settles. Slice 4 lands the §9.5 metadata pane that opens a `meta_metadata` PR (title/tags edit) on the meta repo. The Slice 4 build deferred the merge surface to Gitea web for v1 — `api_prs.merge_pr` was scoped to body-changing PRs (`rfc_branch` and `meta_body_edit`). Slice 8 extends `_require_pr` to include `meta_metadata` so the merge gesture lands in-app. The diff-rendered review surface degrades gracefully — a metadata PR doesn't have a body diff worth reviewing — but the merge button works. The tests prove: * `POST /api/rfcs//prs//merge` accepts a metadata PR and runs the underlying merge. * After the merge, the meta entry's title/tags carry forward and the cache reflects the new values. * A contributor (no role on the super-draft) is refused. * Withdraw also works against a metadata PR — the same surface supports the §10.8 withdraw gesture. """ 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, ) from test_super_draft_vertical import seed_super_draft # noqa: F401 PITCH = ( "Open Human Model is a framework for representing humans.\n\n" "It defines consent, trait, and agency in compatible terms." ) def test_metadata_pr_merges_in_app(app_with_fake_gitea): """The headline assertion: an owner can hit the same `prs//merge` endpoint for a metadata PR and the change lands.""" 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", email="ben@test") # Open the metadata PR via the §9.5 endpoint. 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"] # Verify the kind landed as meta_metadata. row = db.conn().execute( "SELECT pr_kind FROM cached_prs WHERE pr_number = ?", (pr_number,) ).fetchone() assert row["pr_kind"] == "meta_metadata" # Merge via the §10.5 endpoint — the Slice 8 extension. r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge") assert r.status_code == 200, r.text # The cached entry now carries the new title + tags. cached = db.conn().execute( "SELECT title, tags_json FROM cached_rfcs WHERE slug = 'ohm'" ).fetchone() import json as _json tags = _json.loads(cached["tags_json"]) assert cached["title"] == "Open Human Model" assert "identity" in tags and "schema" in tags # PR row's state is now 'merged'. post = db.conn().execute( "SELECT state FROM cached_prs WHERE pr_number = ?", (pr_number,) ).fetchone() assert post["state"] == "merged" def test_metadata_pr_merge_refused_for_plain_contributor(app_with_fake_gitea): """§6.1 + §6.3: only owners/arbiters/admins can merge. A plain contributor without any per-RFC authority gets 403, same as the existing body-edit PR merge surface. Confirms the extension didn't widen the permission gate.""" from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=1, login="ben", role="owner") 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=1, gitea_login="ben", display_name="Ben", role="owner", email="ben@test") # Ben opens the metadata PR. r = client.post( "/api/rfcs/ohm/metadata", json={"title": "OHM (revised)", "tags": ["identity"]}, ) pr_number = r.json()["pr_number"] # Alice (plain contributor) tries to merge — 403. sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge") assert r.status_code == 403 def test_metadata_pr_withdraw_works(app_with_fake_gitea): """§10.8 withdraw surface also handles meta_metadata PRs uniformly — the API doesn't care which kind.""" 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", email="ben@test") r = client.post( "/api/rfcs/ohm/metadata", json={"title": "Something else", "tags": []}, ) pr_number = r.json()["pr_number"] r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/withdraw") assert r.status_code == 200, r.text post = db.conn().execute( "SELECT state FROM cached_prs WHERE pr_number = ?", (pr_number,) ).fetchone() # Withdraw flips to 'withdrawn' via the audit-log marker the # reconciler reads, but a direct withdraw via api_prs may # leave it 'closed' depending on the refresh path. Either is # the closed-not-merged shape the surface needs. assert post["state"] in ("withdrawn", "closed")