Files
oto-enterprise-os-dtp/05_deliverables_mvp/pie/manifest/tests/test_pie_manifest.py
T
Claude Code DTP Worker 4f31d0c8c5
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 / 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
[DTP-Worker 20260803_093713] Auto exec · session 20260803_093713
2026-08-03 09:52:14 +00:00

195 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Tests du générateur de manifest de dépendances PIE · Annexe 12 · V10.1.
Stdlib pur (`unittest`) → aucune installation pip requise sur le runner Gitea.
La bibliothèque `jsonschema` sert d'*oracle* quand elle est présente.
Axes :
1. BASELINE : le contrat vanille passe schéma + 10 invariants, est déterministe,
et l'artefact `out/` commité == build frais (reproductibilité · #6).
2. MUTATION : chaque invariant attrape bien la dérive qu'il protège (on casse
le bundle en mémoire et on vérifie que `_validate` remonte une erreur).
3. ANCRAGES : codes marques ⊆ CLAUDE.md §Projets ; annexe/version verbatim dans
la directive ; modules downstream « gated » = répertoires de livrables réels.
"""
from __future__ import annotations
import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest
_HERE = os.path.dirname(os.path.abspath(__file__))
_MODULE = os.path.normpath(os.path.join(_HERE, ".."))
_DELIVERABLES = os.path.normpath(os.path.join(_MODULE, "..", ".."))
sys.path.insert(0, _MODULE)
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))
from pielib import builder, deps # noqa: E402
from lib import validator as maison # type: ignore # noqa: E402
import pie_manifest_gen as gen # noqa: E402
try:
import jsonschema # type: ignore
_HAS_JSONSCHEMA = True
except Exception: # pragma: no cover
_HAS_JSONSCHEMA = False
def _load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
class Baseline(unittest.TestCase):
"""Le contrat vanille passe schéma + 10 invariants et est déterministe."""
def setUp(self):
self.bundle = gen._build()
def test_validate_clean(self):
self.assertEqual(gen._validate(self.bundle), [])
def test_counts_match_directive(self):
c = self.bundle["traceability"]["counts"]
self.assertEqual(c["master_data_groups"], 12) # §16-29
self.assertEqual(c["sync_rules"], 4) # §82-86
self.assertEqual(c["workflow_steps"], 10) # §108-133
self.assertEqual(c["brands"], 9) # §90-102
def test_deterministic_rebuild(self):
self.assertEqual(gen._build(), gen._build())
def test_schema_oracle(self):
if not _HAS_JSONSCHEMA:
self.skipTest("jsonschema absent")
schema = _load(gen._SCHEMA_PATH)
jsonschema.validate(self.bundle["manifest"], schema) # ne lève pas
class Reproducibility(unittest.TestCase):
"""L'artefact commité `out/` == build frais (byte-identique)."""
def test_committed_matches_fresh(self):
out = os.path.join(_MODULE, "out")
if not os.path.isdir(out):
self.skipTest("out/ non commité")
with tempfile.TemporaryDirectory() as tmp:
rc = gen.main(["build", "-o", tmp])
self.assertEqual(rc, 0)
for name in ("pie_manifest.json", "MANIFEST.json"):
with open(os.path.join(out, name), encoding="utf-8") as a, \
open(os.path.join(tmp, name), encoding="utf-8") as b:
self.assertEqual(a.read(), b.read(), f"{name} a dérivé de sa source")
class Anchors(unittest.TestCase):
"""Les ancrages hors-module pointent bien vers du réel (anti-invention #6)."""
def setUp(self):
self.man = gen._build()["manifest"]
def test_brand_codes_subset_of_claude_md(self):
codes = {b["code"] for b in self.man["brands"]}
canon = deps.claude_md_project_codes()
self.assertTrue(canon, "aucun code Pxx lu dans CLAUDE.md §Projets")
self.assertTrue(codes <= canon, f"codes hors constitution : {codes - canon}")
def test_annexe_version_in_directive(self):
decl = deps.directive_declares(self.man_directive(), "Annexe 12", "V10.1")
self.assertTrue(all(decl.values()), f"ancrage directive rompu : {decl}")
def test_gated_modules_exist_on_fs(self):
for d in self.man["downstream_registry"]:
if d["statut"] == "gated":
self.assertTrue(deps.module_exists(d["module"]),
f"module gated introuvable : {d['module']}")
def man_directive(self) -> str:
return gen._build()["traceability"]["directive_source"]
class Mutations(unittest.TestCase):
"""Chaque invariant attrape la dérive qu'il protège."""
def setUp(self):
self.bundle = gen._build()
def _breaks(self, mutate) -> None:
bundle = copy.deepcopy(self.bundle)
mutate(bundle)
self.assertNotEqual(gen._validate(bundle), [],
"la mutation aurait dû être refusée")
def test_inv2_missing_group(self):
self._breaks(lambda b: b["manifest"]["master_data_groups"].pop())
def test_inv2_wrong_group_key(self):
def m(b):
b["manifest"]["master_data_groups"][0]["cle"] = "inventé"
self._breaks(m)
def test_inv3_unknown_trigger(self):
def m(b):
b["manifest"]["sync_rules"][0]["trigger"] = "couleur"
self._breaks(m)
def test_inv4_dangling_downstream(self):
def m(b):
b["manifest"]["sync_rules"][0]["downstream"].append("fantome")
self._breaks(m)
def test_inv4_dangling_trigger_group(self):
def m(b):
b["manifest"]["sync_rules"][0]["trigger_group"] = "fantome"
self._breaks(m)
def test_inv5_status_incoherent(self):
def m(b):
b["manifest"]["downstream_registry"][0]["statut"] = "gated"
b["manifest"]["downstream_registry"][0]["module"] = None
self._breaks(m)
def test_inv6_phantom_module(self):
def m(b):
for d in b["manifest"]["downstream_registry"]:
if d["module"]:
d["module"] = "faisabilite/inexistant"
return
self._breaks(m)
def test_inv7_non_contiguous_workflow(self):
def m(b):
b["manifest"]["workflow"][0]["etape"] = 99
self._breaks(m)
def test_inv8_alien_brand_code(self):
def m(b):
b["manifest"]["brands"][0]["code"] = "P99"
self._breaks(m)
def test_inv9_wrong_annexe(self):
def m(b):
b["manifest"]["annexe"] = 7 # « Annexe 7 » n'est pas dans la directive
self._breaks(m)
def test_inv10_storage_not_vps(self):
def m(b):
b["manifest"]["storage_layout"]["racine"] = "05_deliverables_mvp/pie/"
self._breaks(m)
def test_count_incoherent(self):
def m(b):
b["traceability"]["counts"]["brands"] = 999
self._breaks(m)
if __name__ == "__main__": # pragma: no cover
unittest.main()