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,157 @@
|
||||
"""End-to-end integration test for the §19.2 "branch-name path routing"
|
||||
candidate that Slice 8 settles.
|
||||
|
||||
Slice 2's `branches/<branch>` endpoints used FastAPI's default
|
||||
`{branch}` matcher, which refuses slashes. Slice 2 worked around it
|
||||
with dash-separated branch names (`<login>-draft-<hex>`); Slice 8
|
||||
converts every `branches/<branch>` to `{branch:path}` and reorders
|
||||
the routes so the bare GET is declared last — the more-specific
|
||||
`threads` and `threads/{thread_id}/messages` GETs match first.
|
||||
|
||||
The tests prove:
|
||||
|
||||
* A branch with a slash in the name reads correctly via
|
||||
`GET /api/rfcs/<slug>/branches/<branch:path>`.
|
||||
* The deeper threads GET still works for unslashed branches.
|
||||
* The deeper threads GET still works for slashed branches —
|
||||
the ordering discipline holds.
|
||||
* The POST routes for slashed branches still resolve (visibility,
|
||||
chat-seen, threads).
|
||||
"""
|
||||
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_rfc_view_vertical import SEED_BODY, seed_active_rfc # noqa: F401
|
||||
|
||||
|
||||
def _seed_slashed_branch(fake: FakeGitea, *, slug: str, branch: str, body: str) -> None:
|
||||
"""Seed a branch with a slash in the name directly on FakeGitea +
|
||||
cached_branches so the GET / threads / etc. endpoints can read it."""
|
||||
from app import db
|
||||
|
||||
repo_full = f"wiggleverse/rfc-0001-{slug}"
|
||||
owner, repo = repo_full.split("/", 1)
|
||||
sha = fake._next_sha()
|
||||
fake.branches[(owner, repo)][branch] = {"sha": sha, "ts": "2026-05-23T00:00:00Z"}
|
||||
fake.files[(owner, repo, branch, "RFC.md")] = {"content": body, "sha": sha}
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO cached_branches (rfc_slug, branch_name, head_sha, state, last_commit_at)
|
||||
VALUES (?, ?, ?, 'open', datetime('now'))
|
||||
""",
|
||||
(slug, branch, sha),
|
||||
)
|
||||
|
||||
|
||||
def test_get_branch_view_reads_slashed_branch_name(app_with_fake_gitea):
|
||||
"""The §19.1 brief's headline assertion: a branch with a slash in
|
||||
the name reads correctly via the `{branch:path}` route shape."""
|
||||
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_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
|
||||
_seed_slashed_branch(
|
||||
fake, slug="ohm", branch="alice/feature-rename",
|
||||
body="# Slashed branch body\n",
|
||||
)
|
||||
sign_in_as(client, user_id=2, gitea_login="alice",
|
||||
display_name="Alice", role="contributor")
|
||||
|
||||
r = client.get("/api/rfcs/ohm/branches/alice/feature-rename")
|
||||
assert r.status_code == 200, r.text
|
||||
view = r.json()
|
||||
assert view["branch_name"] == "alice/feature-rename"
|
||||
assert "Slashed branch body" in view["body"]
|
||||
|
||||
|
||||
def test_deeper_threads_get_still_routes_for_unslashed_branch(app_with_fake_gitea):
|
||||
"""Ordering discipline: declaring the bare GET last means the
|
||||
`threads` GET still wins for unslashed `branches/foo/threads`."""
|
||||
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_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
|
||||
_seed_slashed_branch(
|
||||
fake, slug="ohm", branch="plain-branch",
|
||||
body="# plain\n",
|
||||
)
|
||||
sign_in_as(client, user_id=2, gitea_login="alice",
|
||||
display_name="Alice", role="contributor")
|
||||
|
||||
r = client.get("/api/rfcs/ohm/branches/plain-branch/threads")
|
||||
assert r.status_code == 200, r.text
|
||||
# Empty until a thread is posted, but the route fired.
|
||||
assert "items" in r.json()
|
||||
|
||||
|
||||
def test_deeper_threads_get_still_routes_for_slashed_branch(app_with_fake_gitea):
|
||||
"""The branch name carries a slash; the threads GET must still
|
||||
match (branch={branch:path} captures `alice/feature`, threads
|
||||
is the literal anchor)."""
|
||||
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_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
|
||||
_seed_slashed_branch(
|
||||
fake, slug="ohm", branch="alice/feature",
|
||||
body="# slashed\n",
|
||||
)
|
||||
sign_in_as(client, user_id=2, gitea_login="alice",
|
||||
display_name="Alice", role="contributor")
|
||||
|
||||
r = client.get("/api/rfcs/ohm/branches/alice/feature/threads")
|
||||
assert r.status_code == 200, r.text
|
||||
assert "items" in r.json()
|
||||
|
||||
|
||||
def test_post_visibility_resolves_for_slashed_branch(app_with_fake_gitea):
|
||||
"""The §11.1 visibility POST must also route correctly for a
|
||||
slashed branch — proves the {branch:path} matcher applies to the
|
||||
write paths too."""
|
||||
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_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
|
||||
_seed_slashed_branch(
|
||||
fake, slug="ohm", branch="alice/private-work",
|
||||
body="# private\n",
|
||||
)
|
||||
# Materialize the creator row so alice can flip her own branch.
|
||||
from app import db
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO branch_visibility (rfc_slug, branch_name, read_public, contribute_mode)
|
||||
VALUES ('ohm', 'alice/private-work', 1, 'just-me')
|
||||
"""
|
||||
)
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO actions (actor_user_id, on_behalf_of, action_kind, rfc_slug, branch_name)
|
||||
VALUES (2, 'alice', 'create_branch', 'ohm', 'alice/private-work')
|
||||
"""
|
||||
)
|
||||
sign_in_as(client, user_id=2, gitea_login="alice",
|
||||
display_name="Alice", role="contributor")
|
||||
|
||||
r = client.post(
|
||||
"/api/rfcs/ohm/branches/alice/private-work/visibility",
|
||||
json={"read_public": False, "contribute_mode": "just-me"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["ok"] is True
|
||||
Reference in New Issue
Block a user