Slice 4: super-draft body editing per §9.5 + §9.6
The §17 routing-collapse rule lands in api_branches.py and api_prs.py — every branches/<branch>/... and prs/<n>/... route dispatches on the entry's state to pick the right Gitea repo, and the body extracted from the entry's frontmatter envelope is what the editor and the diff see. The bot grows open_metadata_pr; cache grows refresh_meta_branches. Two §17 routes added: start-edit-branch and metadata. The §9.4 super-draft view replaces RFCView.jsx's Slice 2 placeholder; a metadata pane modal opens from the breadcrumb. Branch naming uses edit-<slug>-<6hex> to dodge the §19.2 path-routing candidate while preserving §9.5's structural shape. Covered by tests/test_super_draft_vertical.py (10 tests). The full Slices 1-4 suite is 35/35 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
"""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/<slug>/main returns the canonical body + breadcrumb data.
|
||||
* POST /api/rfcs/<slug>/start-edit-branch cuts a meta-repo edit branch.
|
||||
* GET /api/rfcs/<slug>/branches/<edit-branch> returns the body extracted
|
||||
from the entry envelope, ready for the editor.
|
||||
* POST .../changes/<id>/accept commits to rfcs/<slug>.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/<n>/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-<slug>-<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/<slug>.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
|
||||
Reference in New Issue
Block a user