[DTP-Worker] Sprint 5 · Générateur Audit 5D conformité (QA · roadmap L58)

Audit de second niveau : lit les hand-off out/ des livrables (workflow vente,
Dossier Vente, commissions, e-CF DGII, CONFOTUR) et vérifie 17 contrôles en
5 dimensions (D1 Traçabilité/ISA 500 · D2 AML-UAF/Ley 155-17 · D3 Fiscal
e-CF/Ley 32-23 · D4 Intégrité/IFRS · D5 Gouvernance-SoD/ISA 315).

Anti-invention #6 : paramètre réglementaire non confirmé → A_CONFIRMER (open
item assigné au métier), jamais fabriqué. Verdict PASS_WITH_OPEN_ITEMS (13 PASS,
0 FAIL, 4 à confirmer). Réutilise validateur Publiciste + RoleResolver CRM +
roles_targeting CONFOTUR (zéro duplication).

37 tests · 15 invariants · build déterministe · régression 341 tests verts.
Job CI qa-audit-5d-tests + gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-30 08:41:57 +00:00
parent 35a20247f8
commit c06e15c058
16 changed files with 1699 additions and 1 deletions
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""Tests du générateur d'audit 5D de conformité (QA · Sprint 5).
Stdlib pur (`unittest`) → aucune installation pip requise sur le runner Gitea
(CLAUDE.md #2). Couvre : chargement des artefacts, chaque contrôle (positif +
injection négative sur copie profonde de l'artefact), synthèse par dimension,
verdict, open items, invariants du générateur, déterminisme et schéma.
Axe central : l'audit LIT les hand-off `out/` des livrables et remonte les
paramètres réglementaires non confirmés en A_CONFIRMER — jamais en valeur
fabriquée (#6). Un FAIL ne doit jamais survenir sur les livrables réels.
"""
from __future__ import annotations
import copy
import json
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)
from qalib import artifacts, builder, controls # noqa: E402
import audit_5d_gen as gen # noqa: E402
PASS, FAIL, AC = controls.PASS, controls.FAIL, controls.A_CONFIRMER
_FN = {c["id"]: c["fn"] for c in controls.CONTROLS}
def _spec():
with open(os.path.join(_MOD, "audit_spec.json"), encoding="utf-8") as fh:
return json.load(fh)
def _art():
return artifacts.load_artifacts()
def _run(cid, art, spec):
return _FN[cid](art, spec)
class ArtifactsTest(unittest.TestCase):
def test_load_all_present(self):
art = _art()
self.assertEqual(set(art), artifacts.known_ids())
self.assertEqual(art["workflow"]["doctype"], "Workflow") # liste dépliée
def test_missing_artifact_raises(self):
orig = artifacts.ARTIFACT_PATHS.copy()
artifacts.ARTIFACT_PATHS["ecf"] = os.path.join("fiscal", "n_existe_pas.json")
try:
self.assertRaises(FileNotFoundError, artifacts.load_artifacts)
finally:
artifacts.ARTIFACT_PATHS.clear()
artifacts.ARTIFACT_PATHS.update(orig)
class ControlsHappyPathTest(unittest.TestCase):
def test_no_fail_on_real_artifacts(self):
art, spec = _art(), _spec()
for c in controls.CONTROLS:
statut, detail = c["fn"](art, spec)
self.assertIn(statut, (PASS, AC), f"{c['id']} inattendu {statut}")
self.assertTrue(detail, f"{c['id']} sans détail")
def test_open_items_are_the_four_regulatory_params(self):
art, spec = _art(), _spec()
ac = {c["id"] for c in controls.CONTROLS if c["fn"](art, spec)[0] == AC}
self.assertEqual(ac, {"D1.1", "D1.2", "D1.3", "D2.3"})
class D1TracabiliteTest(unittest.TestCase):
def test_d1_1_fabricated_rate_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["commissions"]["evenements"][0]["taux_pct"] = 5.0
art["commissions"]["evenements"][0]["a_confirmer"] = False
self.assertEqual(_run("D1.1", art, spec)[0], FAIL)
def test_d1_1_rate_with_source_passes(self):
art, spec = copy.deepcopy(_art()), _spec()
for e in art["commissions"]["evenements"]:
e["taux_pct"], e["source"], e["a_confirmer"] = 3.0, "barème Direction 2026", False
self.assertEqual(_run("D1.1", art, spec)[0], PASS)
def test_d1_2_fabricated_rnc_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["ecf"]["emisor"]["rnc_emisor"] = "131000000"
self.assertEqual(_run("D1.2", art, spec)[0], FAIL)
def test_d1_3_fabricated_itbis_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["ecf"]["taxes"][0]["taux_pct"] = 18.0
self.assertEqual(_run("D1.3", art, spec)[0], FAIL)
def test_d1_4_default_on_data_field_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
for f in art["confotur"]["fields"]:
if f["fieldname"] == "referencia_autoridad":
f["default"] = "RES-CONFOTUR-0000"
self.assertEqual(_run("D1.4", art, spec)[0], FAIL)
def test_d1_4_passes_on_real(self):
self.assertEqual(_run("D1.4", _art(), _spec())[0], PASS)
class D2AmlUafTest(unittest.TestCase):
def test_d2_1_missing_client_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["dossier"]["fields"] = [f for f in art["dossier"]["fields"]
if f["fieldname"] != "client"]
self.assertEqual(_run("D2.1", art, spec)[0], FAIL)
def test_d2_2_missing_identity_piece_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["confotur"]["field_order"] = [x for x in art["confotur"]["field_order"]
if x != "piece_identidad_cliente"]
self.assertEqual(_run("D2.2", art, spec)[0], FAIL)
def test_d2_3_threshold_is_open_item(self):
self.assertEqual(_run("D2.3", _art(), _spec())[0], AC)
def test_d2_3_fabricated_threshold_fails(self):
spec = _spec()
spec["uaf"]["seuil_operacion"] = 1000000
self.assertEqual(_run("D2.3", _art(), spec)[0], FAIL)
class D3FiscalTest(unittest.TestCase):
def test_d3_1_devise_mismatch_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["ecf"]["devise_field"] = "champ_inexistant"
self.assertEqual(_run("D3.1", art, spec)[0], FAIL)
def test_d3_2_emission_on_draft_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
for s in art["workflow"]["states"]:
if s["update_value"] == "contrat":
s["doc_status"] = "0"
self.assertEqual(_run("D3.2", art, spec)[0], FAIL)
def test_d3_3_wrong_formapago_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["ecf"]["forma_pago_defaut"]["code"] = "1"
self.assertEqual(_run("D3.3", art, spec)[0], FAIL)
class D4IntegriteTest(unittest.TestCase):
def test_d4_1_states_diverge_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
for f in art["dossier"]["fields"]:
if f["fieldname"] == "workflow_state":
f["options"] = "Lead\nÉtat fantôme"
self.assertEqual(_run("D4.1", art, spec)[0], FAIL)
def test_d4_2_base_not_currency_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["commissions"]["evenements"][0]["base_field"] = "prospect"
self.assertEqual(_run("D4.2", art, spec)[0], FAIL)
def test_d4_3_link_wrong_target_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
for f in art["confotur"]["fields"]:
if f["fieldname"] == "dossier_vente":
f["options"] = "Autre DocType"
self.assertEqual(_run("D4.3", art, spec)[0], FAIL)
def test_d4_4_unknown_update_value_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["commissions"]["evenements"][0]["update_value"] = "etat_inconnu"
self.assertEqual(_run("D4.4", art, spec)[0], FAIL)
class D5GouvernanceTest(unittest.TestCase):
def test_d5_1_unknown_role_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["commissions"]["evenements"][0]["role_id"] = "role-fantome-999"
self.assertEqual(_run("D5.1", art, spec)[0], FAIL)
def test_d5_1_passes_on_real(self):
self.assertEqual(_run("D5.1", _art(), _spec())[0], PASS)
def test_d5_2_segregation_breach_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
for ev in art["ecf"]["emission_events"]:
ev["role_id"] = "ventes-reservations"
self.assertEqual(_run("D5.2", art, spec)[0], FAIL)
def test_d5_3_perms_mismatch_fails(self):
art, spec = copy.deepcopy(_art()), _spec()
art["confotur"]["permissions"][0]["submit"] = 0
self.assertEqual(_run("D5.3", art, spec)[0], FAIL)
def test_d5_3_passes_on_real(self):
self.assertEqual(_run("D5.3", _art(), _spec())[0], PASS)
class BuilderTest(unittest.TestCase):
def test_verdict_pass_with_open_items(self):
r = builder.build(_spec())
self.assertEqual(r["verdict"], "PASS_WITH_OPEN_ITEMS")
self.assertEqual(r["totals"], {"controls": 17, "pass": 13, "fail": 0, "a_confirmer": 4})
def test_deterministic(self):
self.assertEqual(builder.build(_spec()), builder.build(_spec()))
def test_open_items_match_a_confirmer(self):
r = builder.build(_spec())
ac = {c["id"] for c in r["controls"] if c["statut"] == AC}
oi = {o["control"] for o in r["open_items"]}
self.assertEqual(ac, oi)
self.assertTrue(all(o["owner"] for o in r["open_items"]))
def test_dimensions_cover_all_controls(self):
r = builder.build(_spec())
self.assertEqual(sum(d["controls_total"] for d in r["dimensions"]), 17)
self.assertEqual(len(r["dimensions"]), 5)
def test_controls_sorted(self):
ids = [c["id"] for c in builder.build(_spec())["controls"]]
self.assertEqual(ids, sorted(ids))
class GeneratorTest(unittest.TestCase):
def test_validate_clean(self):
spec = _spec()
self.assertEqual(gen._validate(spec, builder.build(spec)), [])
def test_spec_code_ids_match(self):
self.assertEqual(sorted(c["id"] for c in _spec()["controls"]),
controls.control_ids())
def test_report_validates_schema(self):
from qalib.deps import validate as maison
with open(os.path.join(_MOD, "audit.schema.json"), encoding="utf-8") as fh:
schema = json.load(fh)
self.assertEqual(list(maison(builder.build(_spec()), schema)), [])
def test_validate_detects_injected_fail(self):
spec = _spec()
r = copy.deepcopy(builder.build(spec))
r["controls"][0]["statut"] = FAIL
self.assertTrue(any("FAIL" in e for e in gen._validate(spec, r)))
def test_validate_detects_id_drift(self):
spec = _spec()
r = builder.build(spec)
spec2 = copy.deepcopy(spec)
spec2["controls"].pop()
self.assertNotEqual(gen._validate(spec2, r), [])
def test_cli_validate_returns_zero(self):
self.assertEqual(gen.main(["validate"]), 0)
if __name__ == "__main__":
unittest.main()