f05ee59763
§22.4a SLICE-1 of docs/design/2026-06-06-configurable-collection-metadata.md (§7.2). Entry metadata can live in a per-entry `<slug>.meta.yaml` sidecar with the `.md` kept as pure prose (INV-2). Additive and non-breaking — with no sidecars present every corpus stays on the legacy frontmatter path, byte-identical (N=1 unchanged). - Dual-read (app/metadata.py `read_entry`) — sidecar-else-legacy-frontmatter, identical records (INV-6); unknown/forward-compat keys ride along through parse→serialize and migration (INV-7, `Entry.extra`). A degenerate sidecar (malformed/empty/slug-less) never drops the entry — slug backstopped from the filename stem, flagged not lost (INV-3). - Migration tool (`metadata.migrate_collection`) — idempotent, one ChangeFiles commit per collection (new `gitea.change_files`). Tested as a function; its Owner-gated operator trigger is DEFERRED to SLICE-4 (write paths must become sidecar-aware first — see the design's SLICE-4 note + INV-8). No production trigger ships here, so no corpus is rewritten. - Malformed flag — migration 033 adds `cached_rfcs.metadata_malformed` (additive); the corpus mirror derives it; catalog list + entry-detail APIs surface `metadata_malformed`. - INV-7 at graduation — graduation now carries `Entry.extra` through the rebuild instead of dropping forward-compat keys. Gate: backend 575 passed (28 new: test_metadata / _migration / _cache + graduation extra-preservation). Frontend untouched. CHANGELOG 0.47.0 + upgrade-steps; VERSION + frontend/package.json -> 0.47.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
"""SLICE-1 integration — the corpus mirror reads sidecars (dual-read) and
|
|
derives the malformed flag (PUC-6, INV-3/INV-6).
|
|
|
|
Per docs/design/2026-06-06-configurable-collection-metadata.md §6.2-6.3.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app import cache, db, gitea as gitea_mod
|
|
from app.config import load_config
|
|
|
|
from test_propose_vertical import ( # noqa: F401 (fixtures)
|
|
app_with_fake_gitea,
|
|
tmp_env,
|
|
)
|
|
|
|
|
|
def _refresh():
|
|
cfg = load_config()
|
|
asyncio.run(cache.refresh_meta_repo(cfg, gitea_mod.Gitea(cfg)))
|
|
|
|
|
|
def _row(slug):
|
|
return db.conn().execute(
|
|
"SELECT * FROM cached_rfcs WHERE slug = ?", (slug,)
|
|
).fetchone()
|
|
|
|
|
|
def test_mirror_reads_metadata_from_sidecar(app_with_fake_gitea):
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app):
|
|
# A migrated entry: body-only .md + a sidecar holding the metadata.
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/sidecar-one.md")] = {
|
|
"content": "Just the prose body.\n", "sha": "s1"}
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/sidecar-one.meta.yaml")] = {
|
|
"content": "slug: sidecar-one\ntitle: From Sidecar\nstate: active\ntags:\n- alpha\n",
|
|
"sha": "m1"}
|
|
_refresh()
|
|
|
|
row = _row("sidecar-one")
|
|
assert row is not None
|
|
assert row["title"] == "From Sidecar"
|
|
assert row["state"] == "active"
|
|
assert row["body"].strip() == "Just the prose body."
|
|
assert row["metadata_malformed"] == 0
|
|
|
|
|
|
def test_malformed_sidecar_flags_but_still_loads(app_with_fake_gitea):
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app):
|
|
# .md still has frontmatter; sidecar is malformed (a list, not a map).
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/bad-meta.md")] = {
|
|
"content": "---\nslug: bad-meta\ntitle: Legacy Title\nstate: active\n---\n\nBody.\n",
|
|
"sha": "b1"}
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/bad-meta.meta.yaml")] = {
|
|
"content": "- not\n- a\n- mapping\n", "sha": "b2"}
|
|
_refresh()
|
|
|
|
row = _row("bad-meta")
|
|
assert row is not None # INV-3: still loads
|
|
assert row["metadata_malformed"] == 1
|
|
# Falls back to the legacy .md frontmatter for the metadata.
|
|
assert row["title"] == "Legacy Title"
|
|
|
|
|
|
def test_malformed_flag_surfaces_in_catalog_api(app_with_fake_gitea):
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app) as client:
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/flagged.md")] = {
|
|
"content": "---\nslug: flagged\ntitle: Flagged\nstate: active\n---\n\nB.\n",
|
|
"sha": "f1"}
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/flagged.meta.yaml")] = {
|
|
"content": "just a scalar\n", "sha": "f2"}
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/clean.md")] = {
|
|
"content": "---\nslug: clean\ntitle: Clean\nstate: active\n---\n\nB.\n",
|
|
"sha": "c1"}
|
|
_refresh()
|
|
|
|
items = {i["slug"]: i for i in client.get("/api/rfcs").json()["items"]}
|
|
assert items["flagged"]["metadata_malformed"] is True
|
|
assert items["clean"]["metadata_malformed"] is False
|
|
|
|
# And on the detail view.
|
|
assert client.get("/api/rfcs/flagged").json()["metadata_malformed"] is True
|
|
|
|
|
|
def test_malformed_sidecar_on_migrated_entry_still_loads_flagged(app_with_fake_gitea):
|
|
# INV-3 regression: a migrated (body-only .md) entry whose sidecar is
|
|
# corrupt must NOT vanish from the catalog — it loads (slug from the
|
|
# filename stem) and is flagged malformed.
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app):
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/orphaned.md")] = {
|
|
"content": "Just the body, no frontmatter.\n", "sha": "o1"}
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/orphaned.meta.yaml")] = {
|
|
"content": "- corrupt\n- list\n", "sha": "o2"}
|
|
_refresh()
|
|
|
|
row = _row("orphaned")
|
|
assert row is not None # did not vanish
|
|
assert row["metadata_malformed"] == 1
|
|
assert row["body"].strip() == "Just the body, no frontmatter."
|
|
|
|
|
|
def test_legacy_collection_without_sidecars_unchanged(app_with_fake_gitea):
|
|
app, fake = app_with_fake_gitea
|
|
with TestClient(app):
|
|
fake.files[("wiggleverse", "meta", "main", "rfcs/legacy.md")] = {
|
|
"content": "---\nslug: legacy\ntitle: Legacy\nstate: super-draft\n---\n\nPitch.\n",
|
|
"sha": "l1"}
|
|
_refresh()
|
|
|
|
row = _row("legacy")
|
|
assert row is not None
|
|
assert row["title"] == "Legacy"
|
|
assert row["state"] == "super-draft"
|
|
assert row["body"].strip() == "Pitch."
|
|
assert row["metadata_malformed"] == 0
|