Test strategy (r1 — for review)
Comment on le valide. Stratégie de tests de S09 (ADR-0095 artefact 5/5). Le cœur est le test d'exhaustivité sur le vrai produit cartésien 5-voies (architecture §5bis) — toutes les branches sur papier avant le code. Réalise le plan r4 + architecture r2.
1. Objectifs & périmètre¶
Valider que la règle de décision pré-enregistrée (plan §1, ordre H1 → P0 → H2) est implémentée
exactement : chaque combinaison d'entrées → exactement un verdict + cause/reason, aucun
fall-through, aucun crash, aucune valeur décisionnelle inventée hors plan. Hors périmètre : valider la
justesse scientifique du verdict réel (ce n'est pas un test CI — §12).
2. Pyramide de tests + tiers ADR-83¶
| Tier | Markers | Quoi (S09) |
|---|---|---|
| fast (PR-blocking) | unit, property, contract |
couche décision pure (§3-§4), invariants (§5), I/O fail-loud mockée (§6) |
| medium (PR + nightly) | dag_smoke |
import + task discovery + 1-step stub des 2 DAGs (§7) |
| nightly | ml_behaviour |
le run de fidélité réel sur defi_top5/fold-3 (caractérisation, pas assertion de valeur) |
| operator-driven | post_deploy_smoke |
smoke post-deploy in-pod (machinerie verte ≠ verdict — leçon S03) |
pytestmark = pytest.mark.unit (+ @pytest.mark.story("CVN-N001-EI-S09")) en tête de chaque fichier.
3. Couche DÉCISION — _decide_verdict (le cœur)¶
Entrées (5) : determinism{PASS,FAIL,ERROR} (+ det_cause), parity{equal,differs,unverifiable},
divergence{≤ε,>ε}, canonical_present{T,F}, eps_prod_measured{T,F}. Ordre H1 → P0 → H2 (arch §5bis).
| # | Entrée | Verdict attendu | cause/reason |
|---|---|---|---|
| D1 | H1=ERROR | INCONCLUSIVE_TOOLING |
replay_error |
| D2 | H1=FAIL, det_delta≤ε_num | NON_DETERMINISTIC |
numeric_residue |
| D3 | H1=FAIL, det_delta>ε_num | NON_DETERMINISTIC |
real_instability |
| D4 | H1=PASS, canonical_present=F | INCONCLUSIVE_TOOLING |
canonical_absent |
| D5 | H1=PASS, present, eps_prod_measured=F | INCONCLUSIVE_TOOLING |
epsilon_prod_unmeasured |
| D6 | H1=PASS, present, measured, P0=unverifiable | INCONCLUSIVE_TOOLING |
env_parity_unverified |
| D7 | H1=PASS, present, measured, P0=differs, H2 quelconque | CANONICAL_DIVERGENCE |
env_parity_gap |
| D8 | H1=PASS, present, measured, P0=equal, H2≤ε | FIDELITY_OK |
— |
| D9 | H1=PASS, present, measured, P0=equal, H2>ε | CANONICAL_DIVERGENCE |
logic_fidelity_gap |
Pièges à asserter explicitement (les trous logiques sur papier) :
- H1=FAIL court-circuite P0/H2 : D2/D3 ne consultent pas le canonique (plan tie-break #1 ; arch #1).
- P0 AVANT H2 : un >ε sous P0=differs est env_parity_gap, jamais logic_fidelity_gap (D7≠D9).
- differs ≠ unverifiable : D7 (env_parity_gap, divergence réelle attribuée à l'env) vs D6
(env_parity_unverified, on ne sait pas) — le footgun que le tri-état (#7) évite.
- Gates avant P0 : canonical_absent (D4) et epsilon_prod_unmeasured (D5) priment sur l'éval P0.
4. Test d'EXHAUSTIVITÉ (le pendant de « jamais None »)¶
Couvre le produit cartésien complet, pas un sous-ensemble — c'est ce qui attrape les fall-through que la couverture-de-branche rate (arch §5bis).
import itertools
H1 = ["PASS", "FAIL", "ERROR"]
P0 = ["equal", "differs", "unverifiable"]
H2 = ["le", "gt"] # ≤ε / >ε
CP = [True, False] # canonical_present
EM = [True, False] # eps_prod_measured
DC = ["numeric_residue", "real_instability"] # det_cause (only meaningful on FAIL)
def test_decision_is_total():
seen = set()
for h1, p0, h2, cp, em, dc in itertools.product(H1, P0, H2, CP, EM, DC):
v = decide_verdict(h1, p0, h2, cp, em, dc)
assert v.verdict in {"FIDELITY_OK", "NON_DETERMINISTIC",
"CANONICAL_DIVERGENCE", "INCONCLUSIVE_TOOLING"} # exactly one, never None
assert (v.cause is not None) or (v.reason is not None) or v.verdict == "FIDELITY_OK"
seen.add((h1, p0, h2, cp, em, dc))
assert len(seen) == len(H1)*len(P0)*len(H2)*len(CP)*len(EM)*len(DC) # full product visited
Plus un mapping explicite (paramétrisé sur D1–D9) qui assert verdict + cause/reason exacts par classe d'équivalence — pour que la table §3 et le code ne puissent pas diverger.
5. INVARIANTS (property / contract)¶
| Inv | Énoncé | Type |
|---|---|---|
| I1 | Déterminisme = égalité IEEE-754 exacte : det_status==PASS ⟺ f1_buy(a)==f1_buy(b) (pas de tolérance) |
property |
| I2 | H1 canonical-independent : _probe_determinism ne prend aucun argument canonique ; muter le canonique ne change pas H1 (arch #1) |
contract |
| I3 | Ordre H1→P0→H2 : aucune entrée ne produit logic_fidelity_gap quand P0≠equal |
property |
| I4 | Tri-état parité : _probe_parity ∈ {equal, differs, unverifiable} ; une dimension non enregistrable ⇒ unverifiable (jamais differs) |
contract |
| I5 | env_fingerprint vient du subprocess : le fingerprint comparé en P0 est celui retourné par le replay, pas l'env de l'orchestrateur (arch #3) | contract |
| I6 | No-crash : tout input (NaN, None, exception replay) → un Verdict structuré, jamais une exception propagée |
property |
| I7 | ε résolu, pas codé en dur : epsilon == max(ε_num, ε_prod) ; ε_num = valeur figée du plan §3.E |
contract |
| I8 | Verdict auto-auditable : l'artefact porte provenance des deux côtés + ε_prod + son SHA image + ε_num (arch §6) |
contract |
6. I/O fail-loud (mockée, fast)¶
query_canonical_from_pglève / retourne None →canonical_absent(pas de crash).- store
ε_prodvide pour le SHA prod →epsilon_prod_unmeasured. - subprocess replay exit≠0 / timeout →
replay_error(ERROR, pas raise). - provenance canonique colonne absente →
parity_state=unverifiable→env_parity_unverified(arch #2). - Aucun
print(ADR-31) ; eventsevent=key=value(ADR-31) ; pas de NaN propagé (ADR-25).
7. DAG smoke (medium)¶
diagnostic__s18_replay_fidelity: import + task discovery +dag.test()1-step sous stub (replays mockés, canonique stub, ε_prod stub) → verdict émis, no-crash.diagnostic__s18_eps_prod: import + task discovery + 1-step stub → écrit unε_prodstub keyé SHA.- ADR-92 : bannière
build=<sha>+event=dag_loadedprésents.
8. Les 3 niveaux d'exécution (≠ tests unitaires)¶
| Niveau | Quoi | Coûts/provenance |
|---|---|---|
| unit (CI, fast) | §3–§6 — décision + invariants + fail-loud, tout mické | aucun ; placeholders OK |
| smoke (post-deploy, in-pod) | machinerie réelle, 1 cellule, replays réels mais non décisionnel | placeholders OK (câblage) — mock vert ≠ validé (S03) |
| full gated (nightly / operator) | les 5 cellules, ε_prod mesuré, provenance P0 présente → verdict décisionnel |
gaté : provenance P0 + ε_prod frais + config §3.C au spawn |
9. Mapping tests ↔ hypothèses ↔ user stories¶
| Test | Hypothèse (plan §3) | US (plan A2) |
|---|---|---|
| I1, I2, D2/D3 | H1 (déterminisme cross-process) | US1 |
| D4–D9, I3, I7 | H2 (fidélité au canonique) | US2 |
| D6/D7, I4, I5, I8 | P0 (parité env) + WA1 | US2, US4 |
| §4 exhaustivité | la règle figée (plan §1) | US1–US4 |
| §7 dag_smoke, §8 smoke | machinerie opérable | US3 |
| §8 full gated + §12 | cause classification (5 cellules) | US4, US5 |
10. Données de test & fixtures (ADR-88)¶
Verdict/DeterminismResult/ParityResult/DivergenceResultconstruits en mémoire (pas de PG) pour §3–§5.- fixtures replay : deux
ReplayResultà f1_buy identiques (H1 PASS) / différents (FAIL, deux deltas). - fixture canonique : ligne stub avec/sans colonnes de provenance (pour P0 equal/differs/unverifiable).
- fixture
ε_prodstore : présent (frais / stale SHA) / absent.
11. Critères de done (gate PR)¶
-
make test-unitvert ; §4 exhaustivité 5-voies vert (produit complet visité, exactly-one-verdict). - Table §3 (D1–D9) paramétrée, verdict + cause/reason assertés.
- Invariants I1–I8 verts (property/contract).
- dag_smoke des 2 DAGs vert + ADR-92.
- no-crash sur tous les chemins d'erreur (§6) ; aucun
print. - couverture des modules nouveaux ; aucun TODO de valeur décisionnelle hors plan.
12. Hors-test (validation système, post-merge)¶
Le verdict réel (FIDELITY_OK / NON_DETERMINISTIC / CANONICAL_DIVERGENCE) sur les 5 cellules est une
validation système, pas un test CI : déclencher §3.B post-deploy, lire s18_replay_verdict, et —
attendu (prior plan §1) — classer la divergence A6. C'est l'acceptance de la Story, pas une assertion
figée en CI.
13. Validations hors-CI + dépendances¶
- Provenance canonique (arch #2) : précondition schéma — un test d'intégration vérifie que la persistance porte les colonnes P0 avant de déclarer le full gated exécutable.
ε_prodfraîcheur : le verdict job assert queeps_prod_image_sha == SHA image prod courant(sinonepsilon_prod_unmeasured) — testé en unit (I7-adjacent) + vérifié en système.- Config §3.C au spawn : un test (ou un check in-pod) confirme que
PYTHONHASHSEED+ caps sont dans l'env du child, pas posés tard (arch §2bis) — sinon I1 teste un chemin non épinglé.