Skip to content

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). - differsunverifiable : 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_pg lève / retourne None → canonical_absent (pas de crash).
  • store ε_prod vide pour le SHA prod → epsilon_prod_unmeasured.
  • subprocess replay exit≠0 / timeout → replay_error (ERROR, pas raise).
  • provenance canonique colonne absente → parity_state=unverifiableenv_parity_unverified (arch #2).
  • Aucun print (ADR-31) ; events event=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 ε_prod stub keyé SHA.
  • ADR-92 : bannière build=<sha> + event=dag_loaded pré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 / DivergenceResult construits 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 ε_prod store : présent (frais / stale SHA) / absent.

11. Critères de done (gate PR)

  • make test-unit vert ; §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.
  • ε_prod fraîcheur : le verdict job assert que eps_prod_image_sha == SHA image prod courant (sinon epsilon_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é.