generated from coulomb/repo-seed
WP-0001 (Foundation & LEVEL1 Core):
- manifest model (FR-100), MD→DOCX builder (FR-200), DOCX→MD importer
(FR-300/400), template family registry (FR-600), drift detector (FR-700),
CLI wiring, pre-commit config, CI skeleton, regression harness
WP-0002 (Service Interfaces & Workflow Orchestration):
- REST service via FastAPI (FR-900): /health, /version, /capabilities,
/templates, /styles, /validate, /build, /import, /compare,
/templates/register, /workflows/{name}, /evidence/{run_id}
- Evidence & report store (FR-1400): JSON-backed, per-run, retrievable
through all interfaces, classification (pass/warnings/failed)
- Composite workflow orchestration (FR-1300): single-file-roundtrip,
multi-file-roundtrip, release-regression, family-switch-build
- MCP server via FastMCP (FR-1000): all tools + resources
- CLI additions: `markidocx serve`, `markidocx workflow`, `markidocx mcp`
- Interface parity tests: CLI / REST / MCP produce equivalent results
135 tests passing, ruff + mypy clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
213 lines
7.8 KiB
Python
213 lines
7.8 KiB
Python
"""Tests for T03 — Evidence & report storage (FR-1400)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from markidocx.evidence import EvidenceStore, ReportContext, RunReport
|
||
|
||
|
||
@pytest.fixture()
|
||
def store(tmp_path: Path) -> EvidenceStore:
|
||
return EvidenceStore(base_dir=tmp_path / "evidence")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# EvidenceStore basics (FR-1401–FR-1404)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_new_run_id_is_unique(store: EvidenceStore) -> None:
|
||
ids = {store.new_run_id() for _ in range(10)}
|
||
assert len(ids) == 10
|
||
|
||
|
||
def test_save_and_get_report(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
data = {"status": "ok", "project": "Test", "warnings": [], "errors": []}
|
||
store.save_report(run_id, "validation", data)
|
||
report = store.get_report(run_id, "validation")
|
||
assert report is not None
|
||
assert report.run_id == run_id
|
||
assert report.report_type == "validation"
|
||
assert report.data["status"] == "ok"
|
||
|
||
|
||
def test_get_missing_report_returns_none(store: EvidenceStore) -> None:
|
||
assert store.get_report("nonexistent-id", "validation") is None
|
||
|
||
|
||
def test_save_report_writes_json_file(store: EvidenceStore, tmp_path: Path) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
path = tmp_path / "evidence" / run_id / "build.json"
|
||
assert path.exists()
|
||
content = json.loads(path.read_text())
|
||
assert content["report_type"] == "build"
|
||
|
||
|
||
def test_save_all_report_types(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
for rtype in ("validation", "build", "import", "drift"):
|
||
store.save_report(run_id, rtype, {"status": "ok", "warnings": [], "errors": []})
|
||
reports = store.list_reports(run_id)
|
||
types = {r.report_type for r in reports}
|
||
assert types == {"validation", "build", "import", "drift"}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# List and retrieve (FR-1409)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_list_runs_empty(store: EvidenceStore) -> None:
|
||
assert store.list_runs() == []
|
||
|
||
|
||
def test_list_runs_after_save(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "validation", {"status": "ok", "warnings": [], "errors": []})
|
||
assert run_id in store.list_runs()
|
||
|
||
|
||
def test_list_reports_empty_for_unknown_run(store: EvidenceStore) -> None:
|
||
assert store.list_reports("no-such-run") == []
|
||
|
||
|
||
def test_list_reports_returns_all(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "validation", {"status": "ok", "warnings": [], "errors": []})
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
reports = store.list_reports(run_id)
|
||
assert len(reports) == 2
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Traceability fields (FR-1410)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_report_context_stored_and_retrieved(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
ctx = ReportContext(project="TestDoc", family="article", feature_level="level1", workflow="single-file-roundtrip")
|
||
store.save_report(run_id, "validation", {"status": "ok", "warnings": [], "errors": []}, context=ctx)
|
||
report = store.get_report(run_id, "validation")
|
||
assert report is not None
|
||
assert report.context.project == "TestDoc"
|
||
assert report.context.family == "article"
|
||
assert report.context.feature_level == "level1"
|
||
assert report.context.workflow == "single-file-roundtrip"
|
||
|
||
|
||
def test_report_has_timestamp(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
report = store.get_report(run_id, "build")
|
||
assert report is not None
|
||
assert report.created_at # non-empty ISO timestamp
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# EvidenceSet assembly (FR-1406–FR-1408)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_assemble_set(store: EvidenceStore) -> None:
|
||
run1 = store.new_run_id()
|
||
run2 = store.new_run_id()
|
||
store.save_report(run1, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
store.save_report(run2, "import", {"status": "ok", "warnings": [], "errors": []})
|
||
ev_set = store.assemble_set([run1, run2])
|
||
assert len(ev_set.reports) == 2
|
||
assert set(ev_set.run_ids) == {run1, run2}
|
||
|
||
|
||
def test_evidence_set_classification_pass(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
ev_set = store.assemble_set([run_id])
|
||
assert ev_set.classification == "pass"
|
||
|
||
|
||
def test_evidence_set_classification_pass_with_warnings(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": ["some warning"], "errors": []})
|
||
ev_set = store.assemble_set([run_id])
|
||
assert ev_set.classification == "pass-with-warnings"
|
||
|
||
|
||
def test_evidence_set_classification_failed(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "error", "warnings": [], "errors": ["oops"]})
|
||
ev_set = store.assemble_set([run_id])
|
||
assert ev_set.classification == "failed"
|
||
|
||
|
||
def test_evidence_set_composition(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "validation", {"status": "ok", "warnings": [], "errors": []})
|
||
ev_set = store.assemble_set([run_id])
|
||
comp = ev_set.composition
|
||
assert len(comp) == 1
|
||
assert comp[0]["run_id"] == run_id
|
||
assert comp[0]["type"] == "validation"
|
||
|
||
|
||
def test_evidence_set_summary_keys(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
ev_set = store.assemble_set([run_id])
|
||
summary = ev_set.summary()
|
||
assert "classification" in summary
|
||
assert "run_count" in summary
|
||
assert "report_count" in summary
|
||
assert "complete" in summary
|
||
assert "warnings_count" in summary
|
||
assert "composition" in summary
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Human-readable and machine-readable output (FR-1411, FR-1412)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_to_markdown(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
md = store.to_markdown(run_id)
|
||
assert run_id in md
|
||
assert "Build" in md
|
||
|
||
|
||
def test_to_json(store: EvidenceStore) -> None:
|
||
run_id = store.new_run_id()
|
||
store.save_report(run_id, "build", {"status": "ok", "warnings": [], "errors": []})
|
||
raw = store.to_json(run_id)
|
||
parsed = json.loads(raw)
|
||
assert parsed["run_id"] == run_id
|
||
assert len(parsed["reports"]) == 1
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# RunReport round-trip serialisation
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_run_report_to_from_dict() -> None:
|
||
ctx = ReportContext(project="P", family="article")
|
||
r = RunReport(
|
||
run_id="abc",
|
||
report_type="build",
|
||
data={"status": "ok", "warnings": [], "errors": []},
|
||
created_at="2026-01-01T00:00:00+00:00",
|
||
context=ctx,
|
||
)
|
||
d = r.to_dict()
|
||
r2 = RunReport.from_dict(d)
|
||
assert r2.run_id == "abc"
|
||
assert r2.context.project == "P"
|
||
assert r2.context.family == "article"
|