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,149 @@
|
||||
"""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/<slug>/prs/<n>/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/<n>/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")
|
||||
Reference in New Issue
Block a user