Files
rfc-app/backend/tests/test_metadata.py
T
Ben Stull f05ee59763 v0.47.0 — SLICE-1: metadata sidecars — storage + dual-read + migration tool
§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>
2026-06-07 08:24:27 -07:00

200 lines
6.6 KiB
Python

"""SLICE-1 unit tests — sidecar metadata: dual-read, unknown-key preservation,
frontmatter stripping, malformed detection.
Pure functions only (no DB / no Gitea). Per
docs/design/2026-06-06-configurable-collection-metadata.md §7.2 (SLICE-1) and
INV-6 (dual-read), INV-7 (unknown keys ride along), INV-2 (clean body).
"""
from __future__ import annotations
import yaml
from app import entry as entry_mod
from app import metadata
LEGACY_MD = """---
slug: view-metrics
title: View today's metrics
state: active
owners:
- ben.stull
tags:
- dashboard
- analytics
priority: P1
owner: hasan
---
This is the prose body.
Second paragraph.
"""
# ---- INV-7: unknown keys ride along ----
def test_parse_preserves_unknown_keys_in_extra():
e = entry_mod.parse(LEGACY_MD)
assert e.extra == {"priority": "P1", "owner": "hasan"}
def test_serialize_round_trip_preserves_unknown_keys():
e = entry_mod.parse(LEGACY_MD)
text = entry_mod.serialize(e)
e2 = entry_mod.parse(text)
assert e2.extra == {"priority": "P1", "owner": "hasan"}
assert e2.tags == ["dashboard", "analytics"]
assert e2.owners == ["ben.stull"]
def test_known_keys_never_leak_into_extra():
e = entry_mod.parse(LEGACY_MD)
for known in ("slug", "title", "state", "owners", "tags"):
assert known not in e.extra
# ---- metadata_dict / sidecar_yaml ----
def test_metadata_dict_merges_known_and_extra():
e = entry_mod.parse(LEGACY_MD)
d = metadata.metadata_dict(e)
assert d["slug"] == "view-metrics"
assert d["title"] == "View today's metrics"
assert d["state"] == "active"
assert d["tags"] == ["dashboard", "analytics"]
# forward-compat keys present
assert d["priority"] == "P1"
assert d["owner"] == "hasan"
def test_sidecar_yaml_is_parseable_and_has_no_frontmatter_fences():
e = entry_mod.parse(LEGACY_MD)
sc = metadata.sidecar_yaml(e)
assert "---" not in sc.splitlines()[0]
loaded = yaml.safe_load(sc)
assert loaded["slug"] == "view-metrics"
assert loaded["priority"] == "P1"
# ---- strip_frontmatter (INV-2) ----
def test_strip_frontmatter_removes_leading_block():
body = metadata.strip_frontmatter(LEGACY_MD)
assert body.startswith("This is the prose body.")
assert "slug:" not in body
assert "priority:" not in body
def test_strip_frontmatter_passthrough_when_no_frontmatter():
plain = "Just a body.\n\nNo frontmatter here.\n"
assert metadata.strip_frontmatter(plain).strip() == plain.strip()
# ---- parse_sidecar (malformed detection, INV-3) ----
def test_parse_sidecar_good():
values, malformed = metadata.parse_sidecar("slug: a\ntitle: A\npriority: P0\n")
assert malformed is False
assert values == {"slug": "a", "title": "A", "priority": "P0"}
def test_parse_sidecar_non_mapping_is_malformed():
values, malformed = metadata.parse_sidecar("- just\n- a\n- list\n")
assert malformed is True
assert values == {}
def test_parse_sidecar_invalid_yaml_is_malformed():
values, malformed = metadata.parse_sidecar("slug: : : not yaml\n bad: [unclosed\n")
assert malformed is True
assert values == {}
def test_parse_sidecar_empty_is_empty_not_malformed():
values, malformed = metadata.parse_sidecar("")
assert malformed is False
assert values == {}
# ---- read_entry dual-read equivalence (INV-6) ----
def test_dual_read_sidecar_matches_legacy():
legacy_entry, legacy_bad = metadata.read_entry(LEGACY_MD, None)
# The migrated form: body-only .md + a sidecar holding the metadata.
migrated_md = metadata.strip_frontmatter(LEGACY_MD)
sidecar_text = metadata.sidecar_yaml(legacy_entry)
sidecar_entry, sidecar_bad = metadata.read_entry(migrated_md, sidecar_text)
assert legacy_bad is False
assert sidecar_bad is False
# Identical resulting records (INV-6).
assert sidecar_entry.slug == legacy_entry.slug
assert sidecar_entry.title == legacy_entry.title
assert sidecar_entry.state == legacy_entry.state
assert sidecar_entry.owners == legacy_entry.owners
assert sidecar_entry.tags == legacy_entry.tags
assert sidecar_entry.extra == legacy_entry.extra
assert sidecar_entry.body.strip() == legacy_entry.body.strip()
def test_read_entry_sidecar_takes_precedence_over_md_frontmatter():
# A not-yet-migrated .md still carrying frontmatter, plus a sidecar that
# disagrees: the sidecar wins for metadata; the body comes from the .md.
md_with_fm = "---\nslug: old\ntitle: Old Title\nstate: super-draft\n---\n\nBody.\n"
sidecar = "slug: new\ntitle: New Title\nstate: active\n"
e, malformed = metadata.read_entry(md_with_fm, sidecar)
assert malformed is False
assert e.title == "New Title"
assert e.state == "active"
assert e.body.strip() == "Body."
def test_read_entry_malformed_sidecar_still_loads_entry():
# INV-3: a malformed sidecar never hard-fails the read. The entry loads
# (from the .md frontmatter if present) and is flagged malformed.
md = "---\nslug: x\ntitle: X\nstate: active\n---\n\nBody.\n"
e, malformed = metadata.read_entry(md, "- not a mapping\n")
assert malformed is True
assert e.slug == "x"
assert e.title == "X"
assert e.body.strip() == "Body."
# ---- dual-read robustness: degenerate sidecars never drop the entry ----
def test_empty_sidecar_falls_back_to_md_frontmatter():
# An empty sidecar has no metadata to override with — keep the .md's.
md = "---\nslug: keep\ntitle: Keep Me\nstate: active\n---\n\nBody.\n"
e, malformed = metadata.read_entry(md, "", fallback_slug="keep")
assert malformed is False
assert e.slug == "keep"
assert e.title == "Keep Me"
def test_malformed_sidecar_on_body_only_md_loads_with_fallback_slug():
# The .md is already body-only (migrated) and the sidecar is corrupt:
# the entry must still load (INV-3), taking its slug from the filename stem.
e, malformed = metadata.read_entry("Just a body.\n", "- a\n- list\n", fallback_slug="foo")
assert malformed is True
assert e.slug == "foo"
def test_slugless_sidecar_uses_fallback_slug():
md = "Body only.\n"
sidecar = "title: No Slug Here\nstate: active\n"
e, malformed = metadata.read_entry(md, sidecar, fallback_slug="bar")
assert malformed is False
assert e.slug == "bar"
assert e.title == "No Slug Here"
# ---- sidecar filename helpers ----
def test_sidecar_filename_helpers():
assert metadata.sidecar_name("view-metrics") == "view-metrics.meta.yaml"
assert metadata.is_sidecar("view-metrics.meta.yaml") is True
assert metadata.is_sidecar("view-metrics.md") is False
assert metadata.slug_of_sidecar("view-metrics.meta.yaml") == "view-metrics"