Files
marki-docx/tests/test_evidence.py
Bernd Worsch 1f3dddf7d6 feat: WP-0001 + WP-0002 complete — LEVEL1 core + service interfaces
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>
2026-03-16 07:46:31 +00:00

213 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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-1401FR-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-1406FR-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"