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