[DTP-Worker 20260813_020316] Auto exec · session 20260813_020316
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
"""Suite Phase 1 · Master Project Intake / Master Data Model V18.
|
||||
|
||||
Vérifie le CONTRAT (pas un moteur métier — il n'y en a pas encore, audit §11) :
|
||||
statuts/catégories canoniques · DataPoint traçable · garde canonique #9/#10 ·
|
||||
intégrité de l'Evidence Register · rétro-compat STRICTE avec brief.json V12
|
||||
(D-08) · cohérence schéma ↔ code. stdlib pur ; l'oracle jsonschema est utilisé
|
||||
s'il est présent, sinon ignoré (comme les 17 skips « par design » du dépôt).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_MODULE = os.path.dirname(_HERE)
|
||||
for p in (_MODULE,):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from mdmlib import ( # noqa: E402
|
||||
CANONICAL,
|
||||
CATEGORIES,
|
||||
STATUSES,
|
||||
DataPoint,
|
||||
Document,
|
||||
DocumentRegister,
|
||||
EvidenceRegister,
|
||||
MasterDataError,
|
||||
assert_not_canonical,
|
||||
is_present,
|
||||
)
|
||||
import master_intake_gen as cli # noqa: E402
|
||||
|
||||
SCHEMA_PATH = os.path.join(_MODULE, "master_intake.schema.json")
|
||||
BRIEF_SCHEMA_PATH = os.path.join(_MODULE, "..", "generator", "brief.schema.json")
|
||||
FIXTURE = os.path.join(_MODULE, "fixtures", "intake_P01_coralis.json")
|
||||
GEN_CANONICAL_PATH = os.path.join(
|
||||
_MODULE, "..", "generator", "genlib", "model.py"
|
||||
)
|
||||
|
||||
|
||||
def _load(path: str) -> dict:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class TestStatusesCategories(unittest.TestCase):
|
||||
def test_exactly_11_statuses_in_canonical_order(self):
|
||||
self.assertEqual(len(STATUSES), 11)
|
||||
self.assertEqual(
|
||||
STATUSES,
|
||||
(
|
||||
"VERIFIED", "CONFIRMED", "SOURCE_BASED", "CALCULATED",
|
||||
"ESTIMATED", "ASSUMPTION", "TARGET", "BANK_REQUIREMENT",
|
||||
"PENDING", "MISSING", "NOT_APPLICABLE",
|
||||
),
|
||||
)
|
||||
|
||||
def test_exactly_5_categories(self):
|
||||
self.assertEqual(
|
||||
CATEGORIES,
|
||||
("INPUT", "TARGET", "ASSUMPTION", "BANK_REQUIREMENT", "CALCULATED"),
|
||||
)
|
||||
|
||||
def test_is_present_only_false_for_pending_and_missing(self):
|
||||
absent = {s for s in STATUSES if not is_present(s)}
|
||||
self.assertEqual(absent, {"PENDING", "MISSING"})
|
||||
|
||||
|
||||
class TestSchemaMirrorsCode(unittest.TestCase):
|
||||
"""Le schéma machine et le code Python doivent porter les MÊMES enums (pas
|
||||
de dérive silencieuse entre les deux surfaces du contrat)."""
|
||||
|
||||
def setUp(self):
|
||||
self.schema = _load(SCHEMA_PATH)
|
||||
|
||||
def test_schema_is_draft07(self):
|
||||
self.assertEqual(
|
||||
self.schema["$schema"], "http://json-schema.org/draft-07/schema#"
|
||||
)
|
||||
|
||||
def test_status_enum_matches_code(self):
|
||||
enum = self.schema["definitions"]["data_status"]["enum"]
|
||||
self.assertEqual(tuple(enum), STATUSES)
|
||||
|
||||
def test_category_enum_matches_code(self):
|
||||
enum = self.schema["definitions"]["data_category"]["enum"]
|
||||
self.assertEqual(tuple(enum), CATEGORIES)
|
||||
|
||||
def test_dataset_version_enum(self):
|
||||
self.assertEqual(
|
||||
self.schema["properties"]["dataset_version"]["enum"], ["V12", "V18"]
|
||||
)
|
||||
|
||||
|
||||
class TestRetroCompatV12(unittest.TestCase):
|
||||
"""D-08 : master_intake = SUR-ENSEMBLE STRICT du brief.json V12."""
|
||||
|
||||
def setUp(self):
|
||||
self.master = _load(SCHEMA_PATH)
|
||||
self.brief = _load(BRIEF_SCHEMA_PATH)
|
||||
|
||||
def test_master_includes_brief_by_ref(self):
|
||||
refs = [a.get("$ref") for a in self.master.get("allOf", [])]
|
||||
self.assertIn(self.brief["$id"], refs,
|
||||
"brief.schema.json V12 doit être inclus via allOf/$ref (D-08 règle 2)")
|
||||
|
||||
def test_no_v12_key_shadowed_with_conflicting_type(self):
|
||||
# Toute clé V12 réintroduite dans les properties V18 doit garder un type
|
||||
# compatible (rétro-compat parser Publiciste · D-08 règle 1). Ici V18
|
||||
# n'en réintroduit AUCUNE (extension pure) — on le prouve.
|
||||
v12_keys = set(self.brief.get("properties", {}))
|
||||
v18_keys = set(self.master.get("properties", {}))
|
||||
self.assertEqual(
|
||||
v12_keys & v18_keys, set(),
|
||||
"les champs V18 sont ajoutés en EXTENSION, jamais en remplacement (D-08 règle 3)",
|
||||
)
|
||||
|
||||
def test_dataset_version_field_present(self):
|
||||
self.assertIn("dataset_version", self.master["properties"])
|
||||
|
||||
|
||||
class TestDataPoint(unittest.TestCase):
|
||||
def test_valid_point(self):
|
||||
dp = DataPoint(key="k", value=1, category="INPUT", status="VERIFIED")
|
||||
self.assertTrue(dp.present)
|
||||
self.assertEqual(dp.as_dict()["key"], "k")
|
||||
|
||||
def test_unknown_status_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key="k", value=1, category="INPUT", status="NOPE")
|
||||
|
||||
def test_unknown_category_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key="k", value=1, category="NOPE", status="VERIFIED")
|
||||
|
||||
def test_empty_key_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key=" ", value=1, category="INPUT", status="VERIFIED")
|
||||
|
||||
def test_confidence_out_of_range_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
|
||||
confidence=1.5)
|
||||
|
||||
def test_confidence_bool_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
|
||||
confidence=True)
|
||||
|
||||
def test_source_based_constructs_without_inline_doc(self):
|
||||
# La provenance d'un SOURCE_BASED peut venir d'un lien Evidence : le
|
||||
# point seul se construit (la complétude est vérifiée au niveau dataset,
|
||||
# cf. TestEvidenceRegister.test_dangling_evidence_detected).
|
||||
dp = DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED")
|
||||
self.assertTrue(dp.present)
|
||||
DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED",
|
||||
source_document="DOC-1")
|
||||
|
||||
def test_absent_status_cannot_carry_value(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key="k", value=42, category="INPUT", status="PENDING")
|
||||
# None est OK pour une lacune
|
||||
DataPoint(key="k", value=None, category="INPUT", status="MISSING")
|
||||
|
||||
|
||||
class TestCanonicalGuard(unittest.TestCase):
|
||||
"""Les 6 canoniques #9/#10 ne peuvent jamais devenir un intake saisissable."""
|
||||
|
||||
def test_each_canonical_key_rejected(self):
|
||||
for key in CANONICAL:
|
||||
with self.assertRaises(MasterDataError):
|
||||
assert_not_canonical(key)
|
||||
with self.assertRaises(MasterDataError):
|
||||
DataPoint(key=key, value="x", category="INPUT", status="VERIFIED")
|
||||
|
||||
def test_canonical_values_match_generator_v12(self):
|
||||
# Single source of truth : les valeurs canoniques V18 sont IDENTIQUES à
|
||||
# celles du générateur V12 (audit §8 « réimposer identiques »). On les
|
||||
# relit dans generator/genlib/model.py sans l'importer (évite tout effet
|
||||
# de bord d'import du module de prod) et on exige l'égalité stricte.
|
||||
ns: dict = {}
|
||||
with open(GEN_CANONICAL_PATH, encoding="utf-8") as fh:
|
||||
src = fh.read()
|
||||
# extrait le littéral CANONICAL = {...}
|
||||
start = src.index("CANONICAL = {")
|
||||
end = src.index("}", start) + 1
|
||||
exec(src[start:end], ns) # noqa: S102 — littéral de constantes contrôlé
|
||||
self.assertEqual(CANONICAL, ns["CANONICAL"])
|
||||
|
||||
|
||||
class TestEvidenceRegister(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.docs = DocumentRegister()
|
||||
self.docs.add(Document(doc_id="D1", title="Pièce 1"))
|
||||
|
||||
def test_duplicate_doc_id_rejected(self):
|
||||
with self.assertRaises(MasterDataError):
|
||||
self.docs.add(Document(doc_id="D1", title="doublon"))
|
||||
|
||||
def test_link_to_unknown_doc_rejected(self):
|
||||
ev = EvidenceRegister(self.docs)
|
||||
with self.assertRaises(MasterDataError):
|
||||
ev.link("k", "GHOST")
|
||||
|
||||
def test_dangling_evidence_detected(self):
|
||||
ev = EvidenceRegister(self.docs)
|
||||
# SOURCE_BASED dont la pièce n'est pas cataloguée → pendouillant
|
||||
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||
status="SOURCE_BASED", source_document="GHOST")
|
||||
self.assertEqual(ev.dangling_evidence([dp]), ["k"])
|
||||
|
||||
def test_resolved_evidence_not_dangling(self):
|
||||
ev = EvidenceRegister(self.docs)
|
||||
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||
status="SOURCE_BASED", source_document="D1")
|
||||
self.assertEqual(ev.dangling_evidence([dp]), [])
|
||||
|
||||
def test_link_resolves_evidence(self):
|
||||
ev = EvidenceRegister(self.docs)
|
||||
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||
status="SOURCE_BASED", source_document=None)
|
||||
# pas de source_document mais un lien Evidence vers D1 → résout
|
||||
ev.link("k", "D1")
|
||||
self.assertEqual(ev.dangling_evidence([dp]), [])
|
||||
|
||||
|
||||
class TestPilotFixture(unittest.TestCase):
|
||||
"""Le fixture pilote P01 respecte le contrat et est un vrai sur-ensemble V12."""
|
||||
|
||||
def setUp(self):
|
||||
self.intake = _load(FIXTURE)
|
||||
|
||||
def test_is_superset_of_v12_brief(self):
|
||||
# champ requis V12 présent + version V18 + champs V18 d'extension
|
||||
self.assertEqual(self.intake["projet"], "P01")
|
||||
self.assertEqual(self.intake["dataset_version"], "V18")
|
||||
self.assertIn("data_register", self.intake)
|
||||
|
||||
def test_synthetique_flagged(self):
|
||||
self.assertTrue(self.intake.get("synthetique"),
|
||||
"un fixture de test doit être marqué synthetique (jamais publiable)")
|
||||
|
||||
def test_contract_holds_and_no_dangling(self):
|
||||
points, ev = cli.build_registers(self.intake)
|
||||
self.assertEqual(ev.dangling_evidence(points), [])
|
||||
# honnêteté : la majorité des données sont des lacunes au démarrage
|
||||
present = [p for p in points if p.present]
|
||||
self.assertTrue(len(present) >= 1)
|
||||
|
||||
def test_cli_validate_returns_zero(self):
|
||||
self.assertEqual(cli.cmd_validate(FIXTURE), 0)
|
||||
|
||||
def test_cli_maturity_returns_zero(self):
|
||||
self.assertEqual(cli.cmd_maturity(FIXTURE), 0)
|
||||
|
||||
def test_jsonschema_oracle_if_present(self):
|
||||
try:
|
||||
import jsonschema # noqa: F401
|
||||
except Exception:
|
||||
self.skipTest("jsonschema absent — oracle structurel ignoré (par design)")
|
||||
errors = cli._schema_validate(self.intake)
|
||||
self.assertEqual(errors, [], f"fixture non conforme : {errors}")
|
||||
|
||||
|
||||
class TestCliContractViolations(unittest.TestCase):
|
||||
def test_bad_status_fixture_fails_validate(self):
|
||||
import tempfile
|
||||
bad = {
|
||||
"projet": "P02",
|
||||
"dataset_version": "V18",
|
||||
"data_register": [
|
||||
{"key": "x", "value": 1, "category": "INPUT", "status": "BOGUS"}
|
||||
],
|
||||
}
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False,
|
||||
encoding="utf-8") as fh:
|
||||
json.dump(bad, fh)
|
||||
path = fh.name
|
||||
try:
|
||||
self.assertEqual(cli.cmd_validate(path), 1)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user