Skip to content

Stratégie de tests — CVN-N001-EI-S05 (diagnostic s43)

Réfs : plan dossier r2.6 · architecture r1.1 · runbook. Taxonomie : ADR-83 (tiers fast/medium/nightly/operator-driven + marqueur story). Principe directeur : s43 est un diagnostic dont la logique décisionnelle est pré-enregistrée (plan §1). Donc chaque branche du verdict est testable unitairement et de façon déterministe, indépendamment d'un vrai run cluster. Le verdict scientifique lui-même (avec coûts réels) relève de la validation système, pas du test.


1. Objectifs & périmètre

Tester : (a) la correction de chaque règle décisionnelle (family_verdict, combinaison, cohort coverage, priorité inconclusif) ; (b) les invariants (no-crash → INCONCLUSIVE_TOOLING, déterminisme bit-identical, pas de NaN propagé, anti look-ahead, capture-spy) ; (c) le câblage (DAG compile, graphe Hamilton build, provenance ADR-92).

Ne PAS tester (relève d'autre chose) : la valeur du verdict scientifique (= validation système, full run avec coûts réels mesurés) ; la justesse des coûts (= mesure opérateur, gated) ; le re-fit des modèles (= harness, ADR-89, déjà testé).

2. Pyramide de tests + tiers ADR-83

Niveau Marqueur Tier Ce qui est testé Fichier(s)
Décision (pur) unit fast (PR-blocking) family_verdict (6 branches), combinaison cross-famille, cohort coverage, priorité, per_asset, route tests/unit/test_s43_decide.py
Stats primitives (pur) unit fast envelope M, bootstrap CI percentile, micro-average, rate_buy/precision_buy, breakeven, tie-break tests/unit/test_s43_stats.py
Invariants property / contract fast no-crash, déterminisme bit-identical, pas de NaN, ADR-0094 Inv 7 tests/unit/test_s43_parity.py (scaffold) + test_s43_invariants.py
I/O fail-loud unit fast coût manquant, parquet absent, anti look-ahead ATR/price tests/unit/test_s43_io.py
DAG smoke dag_smoke medium import DAG, Hamilton graph build, provenance ADR-92, fan-out structure tests/integration/test_s43_dag_smoke.py
Oracle parity unit fast replay probe == oracle (skippé jusqu'au hand-author des probes) tests/unit/test_s43_parity.py::test_full_parity

Tous portent @pytest.mark.story("CVN-N001-EI-S05") (ADR-87, filtre pytest --story).

3. Cas de tests — couche DÉCISION (le cœur)

3.1 _decide_family — décision keyée sur l'IC, jamais M_obs (plan §1, fix r2.7)

Bug corrigé r2.7 : D5 keyait sur M_obs>0 (point-estimate) → un straddle non-significatif à bas rate était misclassé NOT_TRADEABLE. La règle keye désormais 100% sur ci_low/ci_high ; M_obs est reporting-only.

# Entrée (ci_low, ci_high, rate) Verdict attendu reason
D1 tooling_error=True INCONCLUSIVE_TOOLING
D2 cost_sensitive=True INCONCLUSIVE_COST_SENSITIVE cost_sensitive
D3 ci_low>0, rate≥rate_min C_REFUTED_TRADEABLE
D5 ci_low>0, rate<rate_min C_GENERALISED_NOT_TRADEABLE non_degenerate_rate_failed
D4 ci_high≤0 C_GENERALISED_NOT_TRADEABLE
D6 ci_low≤0<ci_high (straddle), tout M_obs INCONCLUSIVE_UNDERPOWERED

Cas qui auraient fall-through / misclassifié sous l'ancienne règle (régression r2.7 — à tester explicitement) : | # | Entrée | Attendu | ancien comportement (bug) | |---|---|---|---| | D6b | straddle, M_obs>0, rate≥rate_min | INCONCLUSIVE_UNDERPOWERED | fall-through (aucune branche) | | D6c | straddle, M_obs>0, rate<rate_min | INCONCLUSIVE_UNDERPOWERED | misclassé NOT_TRADEABLE |

Test de priorité d'ordre : D1 > D2 > (D3/D5) — tooling avant cost avant décision IC.

3.2 _combine_families — table cross-famille + priorité inconclusif

# (LGB, XGB, CB) c_verdict attendu
C1 REFUTED, NOT, NOT C_PER_FAMILY (n_refuted≥1 & n_not≥1)
C2 REFUTED, REFUTED, INCONCLUSIVE C_REFUTED_TRADEABLE (n_refuted≥1 & n_not==0)
C3 NOT, NOT, NOT C_GENERALISED_NOT_TRADEABLE
C4 NOT, NOT, INCONCLUSIVE C_GENERALISED_NOT_TRADEABLE (n_not≥2 & n_refuted==0)
C5 UNDERPOWERED, UNDERPOWERED, UNDERPOWERED INCONCLUSIVE_UNDERPOWERED
C6 NOT, COST_SENSITIVE, COST_SENSITIVE INCONCLUSIVE_COST_SENSITIVE (priorité : COST_SENSITIVE > UNDERPOWERED)
C7 TOOLING, COST_SENSITIVE, UNDERPOWERED INCONCLUSIVE_TOOLING (priorité : TOOLING en tête)
C8 REFUTED, INCONCLUSIVE, INCONCLUSIVE C_REFUTED_TRADEABLE (n_refuted≥1 & n_not==0) — « 1 REFUTED suffit », confirmé intentionnel (plan r2.7 : réfuter H₀ pour ≥1 famille = tradable quelque part)
C9 NOT, INCONCLUSIVE, INCONCLUSIVE INCONCLUSIVE_* (n_not=1<2, n_refuted=0 → inconclusif dominant selon priorité §3.2-priorité)
C10 REFUTED, NOT, INCONCLUSIVE C_PER_FAMILY (n_refuted≥1 & n_not≥1 — le mix-avec-inconclusive, complète C1)

Couverture des classes : l'espace {REFUTED, NOT, INCONCLUSIVE_*}³ (à symétrie près) est énuméré par C1-C10 ; le test d'exhaustivité §3.5 garantit qu'aucune classe ne fall-through.

3.3 Cohort coverage (r2.6) — échec partiel de cellules

# Cryptos OK Comportement
K1 5/5 verdict normal
K2 4/5 verdict émis + reason=partial_cohort coverage=4/5
K3 3/5 INCONCLUSIVE_TOOLING reason=cohort_coverage_below_floor

3.4 per_asset_report + _route

  • P1 : signe M ponctuel d'1 crypto diverge du groupe → per_asset_divergence=[SYM] (reporting, n'altère pas c_verdict).
  • R1-R6 : chaque c_verdict → routing attendu (Chapter 5.B) : NOT_TRADEABLE→S06 ; REFUTED→follow-up ; PER_FAMILY→par famille ; INCONCLUSIVE_*→pas de routing.

3.5 Test d'EXHAUSTIVITÉ (le pendant de « jamais None ») — attrape les fall-through que la couverture-de-branche rate

La couverture-de-branche mesure les branches exécutées, pas les manquantes (un fall-through a 0 branche → 100% de couverture tout en n'ayant pas de verdict). À la place : - EX1 _decide_family : balayer le produit cartésien des classes (sign(ci_low), sign(ci_high) sous contrainte ci_high≥ci_low, rate ≷ rate_min, tooling∈{0,1}, cost_sensitive∈{0,1}) → asserter le verdict ∈ ensemble pré-enregistré ET non-None pour toute combinaison (pas zéro, pas deux). - EX2 _combine_families : balayer le produit cartésien {REFUTED, NOT, INCONCLUSIVE_UNDERPOWERED, INCONCLUSIVE_COST_SENSITIVE, INCONCLUSIVE_TOOLING}³ → asserter c_verdict non-None ∈ ensemble pour les 5³=125 combinaisons. Test paramétré ; c'est le pendant-test du principe ValidationResult jamais-None (anti fall-through, leçon de cette revue : la table a rendu le trou visible sur papier).

4. Cas de tests — couche STATS

  • S1 envelope : M = max_θ E(θ) recomputé par resample (pas argmax-point) — vérifier sur courbe synthétique à max connu.
  • S2 tie-break (plan §1-B) : 2 θ à E égal (ε=1e-12) → θ* = plus petit.
  • S3 rate_buy(θ)=0 (plan §1) : E(θ) exclu de M (traité −∞), jamais NaN ; all-θ zéro → no_trade_points.
  • S4 micro-average : pool des opportunités (poids 1/obs) ≠ macro (poids 1/crypto) — vérifier sur 2 cryptos de tailles différentes.
  • S5 materiality (r2.5) : max/min n_scored_rows > 1.2 → déclenche macro-sensibilité ; ≤1.2 → pas de macro.
  • S6 breakeven : p* = (sl+cost)/(tp+sl) ; cost=0 → 0.25 ; cost>0 → 0.25+cost/2.
  • S7 Bonferroni : α effectif = 0.05/3 = 0.0167 par famille (IC 98.33%).

5. Cas de tests — INVARIANTS (property/contract)

  • I1 no-crash (le scaffold le garantit déjà) : toute entrée invalide / exception probe → INCONCLUSIVE_TOOLING, jamais de raise (ADR-25). Test : injecter exceptions dans chaque node → verdict structuré.
  • I2 déterminisme bit-identical : même seed + mêmes données → M, ci_low, ci_high identiques au bit sur 2 runs (leçon S30). B=2000, rng-enfants indépendants.
  • I3 pas de NaN propagé : aucun champ du output schema n'est NaN (rate=0, cost<0, IC dégénéré → valeurs définies ou verdict inconclusif).
  • I4 ADR-0094 Inv 7 : spy from_training_cache.call_count == 0 sur un pin HIT (warm = pas de re-train du cache).
  • I5 cost_bps<0 valide : funding carry positif → cost_bps_per_leg<0 accepté (loggué s43_cost_negative), pas INCONCLUSIVE_TOOLING.

6. Cas de tests — I/O fail-loud

  • IO1 clé coût PG absente → s43_io_cost_resolve_failed severity=errorINCONCLUSIVE_TOOLING, pas de défaut 0 silencieux (ADR-90).
  • IO2 parquet capturé absent / pin MISS sans capture → s43_io_parquet_load_failedINCONCLUSIVE_TOOLING.
  • IO3 anti look-ahead ATR/price (committee rec #2) : le ratio à l'index t n'utilise que des données ≤ t (ATR trailing) — test sur série synthétique avec marqueur futur.
  • IO4 path-guard ^[A-Z0-9]{2,20}$ (scaffold) + artifact_dir sous /tmp.

7. Cas de tests — DAG smoke (medium)

  • DS1 : import dags.dag_diagnostic__s43 sans erreur ; dag_id="diagnostic__s43".
  • DS2 : driver.Builder().with_modules(s43_nodes, s43_economic_tradability).build() compile le graphe (12 nodes, pas de cycle).
  • DS3 : provenance ADR-92 — dag_doc_md banner, tag build=<sha>, event=dag_loaded en 1ère task.
  • DS4 : fan-out 5 cellules + barrière synthèse trigger_rule="all_done".

8. Les 3 niveaux d'exécution (≠ tests unitaires)

Niveau Quoi Quand Décide le verdict ?
Tests unit/contract §3-§7 ci-dessus PR-blocking (CI) non (teste les règles)
Smoke dry-run 2 cryptos × 3 familles × 1 seed (agrégation) + micro-smoke 1×1×5 sub-blocks × B=20 (mécanique UQ) pré-merge cluster non (teste le câblage end-to-end ; OK sur coûts placeholder)
Full run 3 familles × 5 cryptos × (1|5 seeds) × B=2000 post-merge, opérateur OUIgated sur coûts réels mesurés (cf. runbook §2)

9. Mapping tests ↔ hypothèses ↔ user stories

Hypothèse (plan Ch.3) Tests US
H1 envelope/Bonferroni S1, S7, D3-D6, C2-C5 US-1
H2 généralisation cross-famille C1-C7 US-1
H3 net-economics S6, D-* (E pilote le verdict) US-2
H4 non-dégénérescence D5 (non_degenerate_rate_failed), S3 US-3
H5 cost-robustness D2, C6 (cost_sensitive) US-6
H6 stochasticité/UQ I2 (déterminisme), S4 US-5
H7 homogénéité cross-asset P1 (per_asset_divergence) US-7
H8 calibration XGB I/O calibration_fitted + reason code US-9
(ADR-25) I1, I3, IO1-IO2 US-10

10. Données de test & fixtures (ADR-88)

  • Synthétiques pour les stats/décision : courbes E(θ) à max connu, IC contrôlés, (y_true,p_buy) jouets — pas de dépendance cluster, déterministes.
  • Golden fixture pour l'oracle parity : un petit (y_true, p_buy) figé + M/verdict attendu, pinné par SHA (ADR-88), régénéré via procédure documentée seulement.
  • Pas de données réelles dans les tests unit (le verdict réel = validation système).

11. Critères de done (gate PR)

  • make test-unit vert : tous les cas §3-§7 passent ; family_verdict 6/6 branches, combinaison 7/7, cohort 3/3.
  • Déterminisme I2 bit-identical vérifié.
  • DAG smoke (medium) vert.
  • Oracle parity rempli (plus skippé) une fois les probes hand-authored.
  • Exhaustivité (§3.5), pas seulement couverture-de-branche : EX1/EX2 verts — toute combinaison de classe → exactement un verdict non-None (la couverture-de-branche seule raterait un fall-through, cf. le bug r2.7).
  • D6b/D6c (régression r2.7) verts ; combinaison C1-C10.

12. Hors-test (validation système, post-merge)

Le verdict status-C réel n'est PAS un test : il sort du full run sur coûts réels mesurés (runbook §2 gate). Sa validation = (a) cohérence des s43_* events Loki, (b) c_verdict + routing enregistrés, (c) sanity n_buy_at_star / ci_width. C'est le gate In testing → Tested de la Story, pas un test CI.

13. Validations hors-CI + dépendances (refinements review)

  • Calibration de l'IC (simulation offline, pas CI) : §4-S teste la mécanique + le déterminisme, pas la validité statistique. Une simulation de couverture (générer sous H₀ connu, vérifier taux de faux REFUTED ≈ budget Bonferroni 0.0167, et empiriquement si NOT_TRADEABLE strict est atteignable vu le max-bias §5.5 du plan) est rassurante mais trop lourde pour le CI* → validation statistique séparée, documentée, one-shot.
  • Déterminisme cross-process (I2) : le bit-identical in-process (2 runs) ne couvre pas la frontière pod (group bootstrap au synthèse consommant 5 cell-pods). Le déterminisme distribué (seed propagé, rng-enfants indépendants par cellule) est validé au smoke dry-run cluster, pas en unit — acté ici pour ne pas le croire couvert par I2.
  • Oracle parity — dépendance ordonnée : test_full_parity reste skipped jusqu'au hand-author des probes ; la golden fixture (§10) ne peut être figée qu'après que les probes produisent leur sortie réelle. Ordre : probes → fixture SHA-pinnée → parity. Le « done » §11 n'est complet que quand parity n'est plus skipped — ne pas croire le gate PR vert tant que ce skip subsiste.