"""G-15 — the branch/body subsystem is three-tier (project/collection) aware. Before G-15 the branch-body GET resolved every meta-resident entry to the default project's content repo at `rfcs/.md`, so an entry in a named collection (subfolder) or a non-default project's repo rendered a BLANK canonical body and its edit/PR/body-write paths hit the wrong file. These tests seed entries outside the default collection and assert the collection-scoped body-read routes (and the now-collection-aware slug-only routes) read the correct repo + subfolder. """ from __future__ import annotations import asyncio from app import cache as cache_mod, db, gitea as gitea_mod from app.config import load_config from fastapi.testclient import TestClient # noqa: E402 from test_propose_vertical import ( # noqa: E402,F401 app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as, ) def _entry_md(slug, title, state="active"): return f"---\nslug: {slug}\ntitle: {title}\nstate: {state}\n---\nthe canonical body\n" def _add_features_collection(): """A named 'features' collection (subfolder 'features') under the seeded default project — same content repo ('meta'), distinct subfolder.""" db.conn().execute( "INSERT OR REPLACE INTO collections (id, project_id, type, subfolder, " "initial_state, visibility, name, created_at, updated_at) VALUES " "('features','default','document','features','super-draft','public','Features', " "datetime('now'), datetime('now'))") def _add_distinct_project(): """A second project with its OWN content repo + a default (root) collection — the live OHM dogfood shape (rfc-app project at rfc-app-content).""" db.conn().execute( "INSERT OR REPLACE INTO projects (id, name, content_repo, visibility, updated_at) " "VALUES ('rfc-app','RFC App','rfc-app-content','public', datetime('now'))") db.conn().execute( "INSERT OR REPLACE INTO collections (id, project_id, type, subfolder, " "initial_state, visibility, name, created_at, updated_at) VALUES " "('rfc-app','rfc-app','document','','super-draft','public','RFC App', " "datetime('now'), datetime('now'))") def _mirror(): cfg = load_config() asyncio.run(cache_mod.refresh_meta_repo(cfg, gitea_mod.Gitea(cfg))) # --- named collection (subfolder) under the default project ------------------- def test_branch_view_named_collection_reads_subfolder(app_with_fake_gitea): app, fake = app_with_fake_gitea with TestClient(app) as client: _add_features_collection() fake.files[("wiggleverse", "meta", "main", "features/rfcs/feat.md")] = { "content": _entry_md("feat", "Feature Entry"), "sha": "sf"} _mirror() # cached_rfcs is keyed by the named collection. assert db.conn().execute( "SELECT collection_id FROM cached_rfcs WHERE slug='feat'" ).fetchone()["collection_id"] == "features" # Collection-scoped branch view renders the body (was blank pre-G-15). r = client.get( "/api/projects/default/collections/features/rfcs/feat/branches/main") assert r.status_code == 200, r.text assert r.json()["body"] == "the canonical body\n" # The slug-only legacy route is now collection-aware too: it resolves # the entry's own subfolder via the cached row, so it also renders. r2 = client.get("/api/rfcs/feat/branches/main") assert r2.status_code == 200, r2.text assert r2.json()["body"] == "the canonical body\n" def test_main_view_named_collection_scoped_route(app_with_fake_gitea): app, fake = app_with_fake_gitea with TestClient(app) as client: _add_features_collection() fake.files[("wiggleverse", "meta", "main", "features/rfcs/feat.md")] = { "content": _entry_md("feat", "Feature Entry"), "sha": "sf"} _mirror() r = client.get("/api/projects/default/collections/features/rfcs/feat/main") assert r.status_code == 200, r.text assert r.json()["title"] == "Feature Entry" # --- non-default project with a DISTINCT content repo (the OHM dogfood) ------- def test_branch_view_distinct_project_repo(app_with_fake_gitea): app, fake = app_with_fake_gitea with TestClient(app) as client: _add_distinct_project() fake.files[("wiggleverse", "rfc-app-content", "main", "rfcs/scoped.md")] = { "content": _entry_md("scoped", "Scoped Admin IA"), "sha": "s1"} _mirror() assert db.conn().execute( "SELECT collection_id FROM cached_rfcs WHERE slug='scoped'" ).fetchone()["collection_id"] == "rfc-app" # Scoped read resolves the entry's OWN content repo (rfc-app-content). r = client.get( "/api/projects/rfc-app/collections/rfc-app/rfcs/scoped/branches/main") assert r.status_code == 200, r.text assert r.json()["body"] == "the canonical body\n" # And the slug-only route resolves the right repo via the cached row. r2 = client.get("/api/rfcs/scoped/branches/main") assert r2.status_code == 200, r2.text assert r2.json()["body"] == "the canonical body\n" # --- guards + regression ------------------------------------------------------ def test_scoped_branch_route_404_for_collection_outside_project(app_with_fake_gitea): app, _ = app_with_fake_gitea with TestClient(app) as client: r = client.get( "/api/projects/default/collections/nope/rfcs/x/branches/main") assert r.status_code == 404 def _seed_super_draft_in_collection(fake, *, slug, collection_id, subfolder, owners): import json as _json import yaml md_path = f"{subfolder}/rfcs/{slug}.md" if subfolder else f"rfcs/{slug}.md" fm = {"slug": slug, "title": slug.title(), "state": "super-draft", "id": None, "repo": None, "proposed_by": owners[0], "proposed_at": "2026-05-23", "graduated_at": None, "graduated_by": None, "owners": owners, "arbiters": owners[:1], "tags": []} body = "the body\n" text = f"---\n{yaml.safe_dump(fm, sort_keys=False).rstrip()}\n---\n\n{body}" sha = fake._next_sha() fake.files[("wiggleverse", "meta", "main", md_path)] = {"content": text, "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, collection_id, last_main_commit_at, last_entry_commit_at) " "VALUES (?,?, 'super-draft', NULL, NULL, ?, '2026-05-23', ?, ?, '[]', ?, ?, ?, " "datetime('now'), datetime('now'))", (slug, slug.title(), owners[0], _json.dumps(owners), _json.dumps(owners[:1]), body, sha, collection_id)) def test_graduate_in_named_collection_writes_to_subfolder(app_with_fake_gitea): """G-15 write path: graduating a super-draft that lives in a named collection flips the entry in that collection's `/rfcs/.md`, not the default `rfcs/.md`.""" app, fake = app_with_fake_gitea with TestClient(app) as client: _add_features_collection() provision_user_row(user_id=1, login="ben", role="owner") _seed_super_draft_in_collection( fake, slug="gradme", collection_id="features", subfolder="features", owners=["ben"]) sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner", email="ben@x") r = client.post("/api/rfcs/gradme/graduate?_sync=1", json={"rfc_id": "RFC-0007", "owners": ["ben"]}) assert r.status_code == 200, r.text # The flip landed in the collection's subfolder, not the repo root. sc = fake.files.get( ("wiggleverse", "meta", "main", "features/rfcs/gradme.meta.yaml")) assert sc is not None, "sidecar not written under the collection subfolder" import yaml as _yaml assert _yaml.safe_load(sc["content"])["state"] == "active" # Nothing was written to the default repo-root path. assert ("wiggleverse", "meta", "main", "rfcs/gradme.meta.yaml") not in fake.files def test_default_collection_entry_still_renders(app_with_fake_gitea): """Regression: the default-collection path (repo root `rfcs/.md` in the default content repo) is unchanged by the G-15 resolution.""" app, fake = app_with_fake_gitea with TestClient(app) as client: fake.files[("wiggleverse", "meta", "main", "rfcs/base.md")] = { "content": _entry_md("base", "Baseline"), "sha": "sb"} _mirror() r = client.get("/api/rfcs/base/branches/main") assert r.status_code == 200, r.text assert r.json()["body"] == "the canonical body\n" r2 = client.get("/api/projects/default/collections/default/rfcs/base/branches/main") assert r2.status_code == 200, r2.text assert r2.json()["body"] == "the canonical body\n"