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>
200 lines
6.6 KiB
Python
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"
|