"""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"