[DTP-Worker] Sprint 8 · Générateur Matrice d'acceptation / traçabilité MVP (15 promesses roadmap · 21 modules gated tracés bijectif · partition sprint exacte · 12 hors-périmètre sourcés) (QA · roadmap L75/L80-87)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip).
|
||||
|
||||
Couvre : le build réel (couverture bijective vs CI Gitea + registre 4Big), la
|
||||
conformité au schéma, les 10 familles d'invariants, le déterminisme, et une
|
||||
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole).
|
||||
|
||||
Les négatifs s'appuient sur une fixture SYNTHÉTIQUE minimale (CI + registre +
|
||||
spec factices) pour contrôler exactement l'écart injecté, sans dépendre de la
|
||||
forme du dépôt réel.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_MOD = os.path.normpath(os.path.join(_HERE, ".."))
|
||||
sys.path.insert(0, _MOD)
|
||||
|
||||
import acceptance_gen as gen # noqa: E402
|
||||
from acclib import builder, deps # noqa: E402
|
||||
|
||||
|
||||
def _spec() -> dict:
|
||||
return gen._load(gen._SPEC_PATH)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixture synthétique : deux modules gated (a/one=S2, b/two=S3) + la matrice. #
|
||||
# --------------------------------------------------------------------------- #
|
||||
_SELF = "qa/acceptance"
|
||||
|
||||
|
||||
def _mini_ci(paths, self_module=_SELF) -> dict:
|
||||
j2p = {f"{p.replace('/', '-')}-tests": p for p in paths}
|
||||
j2p["qa-acceptance-tests"] = self_module
|
||||
return {"job_to_path": j2p, "gate_needs": list(j2p)}
|
||||
|
||||
|
||||
def _mini_audit() -> dict:
|
||||
return {"a/one": "S2", "b/two": "S3"}
|
||||
|
||||
|
||||
def _mini_spec() -> dict:
|
||||
return {
|
||||
"version": "test",
|
||||
"reference_cadre": "fixture",
|
||||
"roadmap_ref": "ROADMAP.md",
|
||||
"self_module": _SELF,
|
||||
"cible_portage": "recette",
|
||||
"extra_module_sprint": {},
|
||||
"sprint_deliverables": [
|
||||
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
|
||||
"roadmap_line": 39, "statement": "livrable S2", "status": "in_repo",
|
||||
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||
"out_of_scope": []},
|
||||
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
|
||||
"roadmap_line": 46, "statement": "livrable S3", "status": "in_repo",
|
||||
"evidence_modules": ["b/two"], "evidence_artifacts": [],
|
||||
"out_of_scope": []},
|
||||
],
|
||||
"mvp_metrics": [
|
||||
{"id": "M1", "kind": "mvp_metric", "sprint": None,
|
||||
"roadmap_line": 81, "statement": "métrique M1", "status": "in_repo",
|
||||
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||
"out_of_scope": []},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _mini_env():
|
||||
"""(spec, ci, audit) — la fixture minimale valide."""
|
||||
return _mini_spec(), _mini_ci(["a/one", "b/two"]), _mini_audit()
|
||||
|
||||
|
||||
def _mini_errors(spec, ci=None, audit=None) -> list:
|
||||
ci = ci or _mini_ci(["a/one", "b/two"])
|
||||
audit = audit or _mini_audit()
|
||||
bundle = builder.build_matrix(spec, ci, audit, deps)
|
||||
return gen.validate_bundle(bundle, spec, ci, audit)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
class TestMiniClean(unittest.TestCase):
|
||||
"""La fixture synthétique doit valider proprement (base des négatifs)."""
|
||||
|
||||
def test_mini_validates_clean(self):
|
||||
self.assertEqual(_mini_errors(*(_mini_env()[:1]),), [],
|
||||
"la fixture de base doit être verte")
|
||||
|
||||
def test_mini_bijective(self):
|
||||
spec, ci, audit = _mini_env()
|
||||
b = builder.build_matrix(spec, ci, audit, deps)
|
||||
cov = b["manifest"]["coverage"]
|
||||
self.assertTrue(cov["bijective"])
|
||||
self.assertTrue(cov["partition_ok"])
|
||||
self.assertTrue(b["verdict"])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
class TestRealBuild(unittest.TestCase):
|
||||
"""Build réel contre le CI, le registre 4Big et le spec du dépôt."""
|
||||
|
||||
def setUp(self):
|
||||
self.spec = _spec()
|
||||
self.ci = deps.parse_ci()
|
||||
self.audit = deps.audit_sprints()
|
||||
self.bundle = builder.build_matrix(self.spec, self.ci, self.audit, deps)
|
||||
|
||||
def test_build_validates_clean(self):
|
||||
errs = gen.validate_bundle(self.bundle, self.spec, self.ci, self.audit)
|
||||
self.assertEqual(errs, [], f"invariants rouges: {errs}")
|
||||
|
||||
def test_schema_conformance(self):
|
||||
schema = gen._load(gen._SCHEMA_PATH)
|
||||
self.assertEqual(list(deps.validate(self.bundle, schema)), [])
|
||||
|
||||
def test_coverage_bijective(self):
|
||||
cov = self.bundle["manifest"]["coverage"]
|
||||
self.assertTrue(cov["bijective"])
|
||||
self.assertEqual(cov["missing_in_matrix"], [])
|
||||
self.assertEqual(cov["phantom_evidence"], [])
|
||||
|
||||
def test_partition_exact_all_sprints(self):
|
||||
cov = self.bundle["manifest"]["coverage"]
|
||||
self.assertTrue(cov["partition_ok"])
|
||||
for p in cov["sprint_partition"]:
|
||||
self.assertTrue(p["exact"], f"partition {p['sprint']} non exacte")
|
||||
self.assertEqual(cov["unknown_sprint"], [])
|
||||
|
||||
def test_verdict_pass(self):
|
||||
self.assertTrue(self.bundle["verdict"])
|
||||
|
||||
def test_counts_match_roadmap(self):
|
||||
c = self.bundle["manifest"]["counts"]
|
||||
self.assertEqual(c["sprint_deliverables"], 8) # S1..S8
|
||||
self.assertEqual(c["mvp_metrics"], 7) # M1..M7
|
||||
self.assertEqual(c["gated_modules"], c["cited_modules"])
|
||||
|
||||
def test_every_gated_module_cited_once(self):
|
||||
gated = set(builder.gated_modules(self.ci, self.spec["self_module"]))
|
||||
cited = set()
|
||||
for d in builder.all_deliverables(self.spec):
|
||||
if d["status"] == "in_repo":
|
||||
cited.update(d["evidence_modules"])
|
||||
self.assertEqual(cited, gated)
|
||||
|
||||
def test_self_module_not_cited(self):
|
||||
for d in builder.all_deliverables(self.spec):
|
||||
self.assertNotIn(self.spec["self_module"], d["evidence_modules"])
|
||||
|
||||
def test_out_of_scope_all_sourced(self):
|
||||
for d in builder.all_deliverables(self.spec):
|
||||
for o in d.get("out_of_scope", []):
|
||||
self.assertTrue(o.get("source", "").strip(),
|
||||
f"{d['id']}: hors-périmètre sans source")
|
||||
|
||||
def test_evidence_artifacts_exist(self):
|
||||
for d in builder.all_deliverables(self.spec):
|
||||
for a in d.get("evidence_artifacts", []):
|
||||
self.assertTrue(deps.artifact_exists(a), f"{a} absent")
|
||||
|
||||
def test_determinism(self):
|
||||
b2 = builder.build_matrix(self.spec, self.ci, self.audit, deps)
|
||||
self.assertEqual(self.bundle, b2)
|
||||
|
||||
def test_sprint_windows_from_audit_registry(self):
|
||||
# Toute fenêtre de sprint vient du registre 4Big (sauf l'auditeur, via
|
||||
# extra) — jamais re-déclarée dans le spec d'acceptation.
|
||||
ms = builder.module_sprint_map(self.spec, self.audit)
|
||||
gated = builder.gated_modules(self.ci, self.spec["self_module"])
|
||||
for m in gated:
|
||||
self.assertIn(m, ms, f"{m} sans fenêtre de sprint")
|
||||
|
||||
def test_audit_4big_declared_via_extra(self):
|
||||
self.assertIn("qa/audit_4big", self.spec["extra_module_sprint"])
|
||||
self.assertTrue(
|
||||
self.spec["extra_module_sprint"]["qa/audit_4big"]["source"].strip())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
class TestNegativeInjections(unittest.TestCase):
|
||||
"""Chaque invariant DOIT rougir quand on injecte l'écart correspondant."""
|
||||
|
||||
def test_inv2_phantom_module_flagged(self):
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][0]["evidence_modules"].append("z/ghost")
|
||||
self.assertTrue(any("non gated" in e or "fantôme" in e
|
||||
for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv3_missing_gated_module(self):
|
||||
# b/two gated mais retiré de son livrable → couverture non bijective.
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||
# laisser un artefact pour ne pas déclencher AUSSI INV7
|
||||
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||
errs = _mini_errors(spec)
|
||||
self.assertTrue(any("non tracés" in e for e in errs), errs)
|
||||
|
||||
def test_inv5_partition_wrong_sprint(self):
|
||||
# Placer b/two (fenêtre S3) dans le livrable S2 → partition fausse.
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", "b/two"]
|
||||
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||
self.assertTrue(any("partition" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv4_module_without_sprint(self):
|
||||
# Registre 4Big ne connaît pas b/two → module gated sans fenêtre.
|
||||
spec = _mini_spec()
|
||||
bad_audit = {"a/one": "S2"} # b/two manquant
|
||||
errs = _mini_errors(spec, audit=bad_audit)
|
||||
self.assertTrue(any("sans fenêtre" in e for e in errs), errs)
|
||||
|
||||
def test_inv4_extra_conflicts_with_registry(self):
|
||||
spec = _mini_spec()
|
||||
spec["extra_module_sprint"] = {"a/one": {"sprint": "S9", "source": "x"}}
|
||||
self.assertTrue(any("conflit" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv6_out_of_scope_without_source(self):
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][0]["out_of_scope"] = [{"item": "x", "source": " "}]
|
||||
self.assertTrue(any("sans source" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv7_in_repo_without_evidence(self):
|
||||
spec = _mini_spec()
|
||||
spec["mvp_metrics"][0]["evidence_modules"] = []
|
||||
self.assertTrue(any("sans aucune preuve" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv7_out_of_scope_with_module(self):
|
||||
spec = _mini_spec()
|
||||
spec["mvp_metrics"].append({
|
||||
"id": "M2", "kind": "mvp_metric", "sprint": None, "roadmap_line": 82,
|
||||
"statement": "m2", "status": "out_of_scope",
|
||||
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||
"out_of_scope": [{"item": "x", "source": "s"}]})
|
||||
self.assertTrue(any("out_of_scope avec preuve" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv7_missing_artifact(self):
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][0]["evidence_artifacts"] = ["does/not/exist.md"]
|
||||
self.assertTrue(any("inexistant" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv8_metric_with_sprint(self):
|
||||
spec = _mini_spec()
|
||||
spec["mvp_metrics"][0]["sprint"] = "S4"
|
||||
self.assertTrue(any("sprint non nul" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv8_sprint_deliverable_without_sprint(self):
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][0]["sprint"] = None
|
||||
self.assertTrue(any("sans sprint" in e or "sprint" in e
|
||||
for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv9_self_cited(self):
|
||||
spec = _mini_spec()
|
||||
# citer la matrice elle-même comme preuve (et la rendre gated)
|
||||
ci = _mini_ci(["a/one", "b/two"])
|
||||
ci["job_to_path"]["extra-job"] = _SELF # reste self → exclu du gated
|
||||
spec["sprint_deliverables"][0]["evidence_modules"] = ["a/one", _SELF]
|
||||
self.assertTrue(any("elle-même" in e or "non gated" in e
|
||||
for e in _mini_errors(spec, ci=ci)))
|
||||
|
||||
def test_inv1_duplicate_id(self):
|
||||
spec = _mini_spec()
|
||||
spec["mvp_metrics"][0]["id"] = "S2" # collision avec un sprint
|
||||
self.assertTrue(any("double" in e for e in _mini_errors(spec)))
|
||||
|
||||
def test_inv3_flags_bijective_false_consistency(self):
|
||||
# Retirer un module ET son artefact → bijective doit passer à False.
|
||||
spec = _mini_spec()
|
||||
spec["sprint_deliverables"][1]["evidence_modules"] = []
|
||||
spec["sprint_deliverables"][1]["evidence_artifacts"] = ["GAP_ANALYSIS_SPRINT1.md"]
|
||||
b = builder.build_matrix(spec, _mini_ci(["a/one", "b/two"]), _mini_audit(), deps)
|
||||
self.assertFalse(b["manifest"]["coverage"]["bijective"])
|
||||
self.assertFalse(b["verdict"])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
class TestCLI(unittest.TestCase):
|
||||
"""Le CLI build/validate produit les artefacts et sort proprement."""
|
||||
|
||||
def test_build_writes_outputs(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
gen.main(["build", "-o", tmp])
|
||||
self.assertTrue(os.path.exists(os.path.join(tmp, "acceptance_matrix.json")))
|
||||
self.assertTrue(os.path.exists(os.path.join(tmp, "MANIFEST.json")))
|
||||
|
||||
def test_validate_exits_zero(self):
|
||||
# cmd_validate ne lève pas sur le dépôt réel (sinon SystemExit).
|
||||
gen.main(["validate"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user