Files
orchestrator/tests/test_frontmatter.py
claude-bot 92961d1d32 refactor(frontmatter): unified frontmatter contract + handoff spec (ORCH-52c)
src/frontmatter.py grows from a single-key reader into the full machine
contract: reader (read_frontmatter_value, unchanged), one parse primitive
(parse_frontmatter), writer (render/write_frontmatter), schema validator
(validate_schema/REQUIRED_FIELDS, warning-only by default) and a shared
strip_frontmatter helper. The five verdict gates (check_reviewer_verdict,
_parse_tests_verdict, _parse_deploy_status, _parse_staging_status,
parse_security_status) now read through the single parse_frontmatter point
instead of duplicated ad-hoc YAML logic; review_parse._strip_frontmatter and
security_gate.extract_security_findings reuse the shared helper.

Strictly backward compatible + never-raise: STAGE_TRANSITIONS, the QG_CHECKS
composition, verdict semantics (incl. ORCH-047 three-field tester + negative
token priority), reason-strings and worktree->origin/main fallback are 1:1.
The schema validator never influences a gate verdict by default; hard-fail is
reserved behind the frontmatter_validation_strict kill-switch (default False).

New formal handoff spec docs/_standards/HANDOFF_PROTOCOL.md ("stage -> required
output" + required frontmatter schema), aligned 1:1 with PIPELINE_DOCS.md.

Tests: test_frontmatter.py (TC-01..07), test_qg_verdicts.py (TC-08..15),
test_security_gate.py (TC-12), test_stages_invariants.py (TC-16). Full
tests/ green (1212).

Refs: ORCH-076

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:14:30 +03:00

245 lines
10 KiB
Python

"""ORCH-076 (ORCH-52c): unit tests for the unified frontmatter contract.
Covers TC-01..TC-07 of docs/work-items/ORCH-076/04-test-plan.yaml:
* writer (render/round-trip), validator (full / partial schema, strict on/off),
* reader contract preserved (read_frontmatter_value), never-raise on bad input.
The whole module honours a never-raise contract (NFR-2): no input shape may raise.
"""
import os
import tempfile
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
from src import frontmatter as fm # noqa: E402
from src.frontmatter import ( # noqa: E402
REQUIRED_FIELDS,
FrontmatterParse,
SchemaValidation,
maybe_warn_schema,
parse_frontmatter,
parse_frontmatter_dict,
read_frontmatter,
read_frontmatter_value,
render_frontmatter,
strip_frontmatter,
validate_schema,
write_frontmatter,
)
def _full_schema():
return {
"work_item": "ORCH-076",
"stage": "review",
"author_agent": "reviewer",
"status": "APPROVED",
"created_at": "2026-06-09",
"model_used": "claude-opus-4-8",
}
# --------------------------------------------------------------------------- #
# TC-01 — writer serialises a mapping into canonical leading YAML-frontmatter
# readable by the existing parsers (split("---", 2) + yaml.safe_load).
# --------------------------------------------------------------------------- #
def test_tc01_render_frontmatter_is_canonical_and_reparseable():
out = render_frontmatter({"verdict": "APPROVED", "work_item": "ORCH-076"}, "body text")
assert out.startswith("---\n")
# Existing parser shape: split on '---' into 3 segments + yaml.safe_load.
parts = out.split("---", 2)
assert len(parts) == 3
import yaml
data = yaml.safe_load(parts[1])
assert data["verdict"] == "APPROVED"
assert data["work_item"] == "ORCH-076"
# Body is preserved verbatim after the closing fence.
assert parts[2].lstrip("\n") == "body text"
# And our own primitive round-trips it.
assert parse_frontmatter(out).data == {"verdict": "APPROVED", "work_item": "ORCH-076"}
def test_tc01_render_empty_body_default():
out = render_frontmatter({"a": 1})
assert out == "---\na: 1\n---\n"
# --------------------------------------------------------------------------- #
# TC-02 — round-trip: writer -> reader read_frontmatter_value yields same values.
# --------------------------------------------------------------------------- #
def test_tc02_write_then_read_roundtrip():
data = _full_schema()
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "12-review.md")
assert write_frontmatter(path, data, "# Review body") is True
for key, val in data.items():
assert read_frontmatter_value(path, key) == val
# Whole-mapping read matches too.
assert read_frontmatter(path) == data
def test_tc02_render_parse_dict_roundtrip():
data = _full_schema()
rendered = render_frontmatter(data, "body")
assert parse_frontmatter_dict(rendered) == data
# --------------------------------------------------------------------------- #
# TC-03 — validator: full schema -> valid=True, no missing fields.
# --------------------------------------------------------------------------- #
def test_tc03_validate_full_schema_valid():
res = validate_schema(_full_schema())
assert isinstance(res, SchemaValidation)
assert res.valid is True
assert res.missing == []
# --------------------------------------------------------------------------- #
# TC-04 — validator: partial schema -> valid=False with the missing list,
# WITHOUT raising (warning-only by default).
# --------------------------------------------------------------------------- #
def test_tc04_validate_partial_schema_lists_missing():
res = validate_schema({"work_item": "ORCH-076", "stage": "review"})
assert res.valid is False
# The four absent required fields are reported (order = REQUIRED_FIELDS).
assert set(res.missing) == set(REQUIRED_FIELDS) - {"work_item", "stage"}
assert res.missing == [f for f in REQUIRED_FIELDS if f in res.missing]
def test_tc04_blank_and_none_count_as_missing():
data = _full_schema()
data["status"] = "" # blank -> missing
data["model_used"] = None # None -> missing
res = validate_schema(data)
assert res.valid is False
assert set(res.missing) == {"status", "model_used"}
# --------------------------------------------------------------------------- #
# TC-05 — never-raise: writer + validator on broken input return a safe value.
# --------------------------------------------------------------------------- #
def test_tc05_validate_non_mapping_never_raises():
for bad in (None, "not a mapping", 123, ["a", "b"]):
res = validate_schema(bad) # type: ignore[arg-type]
assert res.valid is False
assert set(res.missing) == set(REQUIRED_FIELDS)
def test_tc05_parse_broken_inputs_never_raise():
# No frontmatter.
p = parse_frontmatter("just prose, no fence")
assert p == FrontmatterParse()
assert p.data == {} and p.has_block is False
# Unterminated block.
p = parse_frontmatter("---\nkey: val\nno closing fence")
assert p.has_block is True and p.malformed is True and p.data == {}
# Bad YAML.
p = parse_frontmatter("---\nkey: : :\n bad\n---\n")
assert p.has_block is True and p.yaml_error is not None and p.data == {}
# Non-mapping scalar frontmatter.
p = parse_frontmatter("---\njust a string\n---\nbody")
assert p.has_block is True and p.data == {}
# Non-string input.
assert parse_frontmatter(None).data == {} # type: ignore[arg-type]
assert parse_frontmatter_dict(12345) == {} # type: ignore[arg-type]
def test_tc05_write_to_unwritable_path_returns_false():
# A path under a non-existent directory cannot be opened -> False, no raise.
ok = write_frontmatter("/nonexistent-dir-xyz/cannot/12-review.md", {"a": 1})
assert ok is False
def test_tc05_render_unserialisable_degrades_to_body():
class Bad:
pass
out = render_frontmatter({"x": Bad()}, "fallback-body")
# yaml cannot serialise an arbitrary object -> degrade to the body, never raise.
assert out == "fallback-body"
def test_tc05_read_missing_file_returns_empty():
assert read_frontmatter("/no/such/file.md") == {}
assert read_frontmatter_value("/no/such/file.md", "verdict") is None
# --------------------------------------------------------------------------- #
# TC-06 — reader read_frontmatter_value keeps its previous contract.
# --------------------------------------------------------------------------- #
def test_tc06_reader_contract_preserved():
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "doc.md")
with open(path, "w", encoding="utf-8") as f:
f.write("---\nverdict: Approved\nempty:\n---\nbody\n")
# strip + case preserved.
assert read_frontmatter_value(path, "verdict") == "Approved"
# empty value -> None.
assert read_frontmatter_value(path, "empty") is None
# absent key -> None.
assert read_frontmatter_value(path, "missing") is None
# No frontmatter -> None.
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "doc.md")
with open(path, "w", encoding="utf-8") as f:
f.write("no frontmatter here\n")
assert read_frontmatter_value(path, "verdict") is None
def test_tc06_reader_strips_whitespace():
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "doc.md")
with open(path, "w", encoding="utf-8") as f:
f.write('---\nverdict: " PASS "\n---\n')
assert read_frontmatter_value(path, "verdict") == "PASS"
# --------------------------------------------------------------------------- #
# TC-07 — kill-switch: strict False (default) is inert; strict True signals
# invalidity. maybe_warn_schema never changes a verdict either way.
# --------------------------------------------------------------------------- #
def test_tc07_maybe_warn_schema_default_warning_only(monkeypatch, caplog):
monkeypatch.setattr(fm, "logger", fm.logger)
from src.config import settings
monkeypatch.setattr(settings, "frontmatter_validation_strict", False)
incomplete = render_frontmatter({"verdict": "APPROVED"}, "body")
res = maybe_warn_schema(incomplete, "review report")
# Validation still reports invalidity (the data IS incomplete)...
assert res.valid is False
assert "model_used" in res.missing
# ...but the helper is inert: it returns a value, it does not raise / block.
def test_tc07_strict_flag_visible_to_helper(monkeypatch):
from src.config import settings
# Full schema -> valid regardless of the flag.
monkeypatch.setattr(settings, "frontmatter_validation_strict", True)
res_full = maybe_warn_schema(render_frontmatter(_full_schema(), "b"), "doc")
assert res_full.valid is True
# Incomplete -> invalid; strict True does not raise, just signals.
res_partial = maybe_warn_schema(render_frontmatter({"stage": "review"}, "b"), "doc")
assert res_partial.valid is False
def test_tc07_maybe_warn_schema_on_garbage_is_inert():
# Never-raise: a non-string / no-frontmatter input returns a SchemaValidation
# (reporting the missing fields) WITHOUT raising — the gate verdict is untouched.
for bad in ("no frontmatter", None, 123):
res = maybe_warn_schema(bad, "doc") # type: ignore[arg-type]
assert isinstance(res, SchemaValidation)
# --------------------------------------------------------------------------- #
# strip_frontmatter helper parity.
# --------------------------------------------------------------------------- #
def test_strip_frontmatter_parity():
assert strip_frontmatter("---\na: 1\n---\nbody") == "\nbody"
# No well-formed block -> unchanged.
assert strip_frontmatter("no fence") == "no fence"
assert strip_frontmatter("---\nunterminated") == "---\nunterminated"
# Never-raise on non-string.
assert strip_frontmatter(None) is None # type: ignore[arg-type]