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+ marqueurstory). Principe directeur :s43est 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% surci_low/ci_high;M_obsest 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
Mponctuel 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 deM(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_highidentiques 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 == 0sur un pin HIT (warm = pas de re-train du cache). - I5 cost_bps<0 valide : funding carry positif →
cost_bps_per_leg<0accepté (loggués43_cost_negative), pasINCONCLUSIVE_TOOLING.
6. Cas de tests — I/O fail-loud¶
- IO1 clé coût PG absente →
s43_io_cost_resolve_failed severity=error→INCONCLUSIVE_TOOLING, pas de défaut 0 silencieux (ADR-90). - IO2 parquet capturé absent / pin MISS sans capture →
s43_io_parquet_load_failed→INCONCLUSIVE_TOOLING. - IO3 anti look-ahead
ATR/price(committee rec #2) : le ratio à l'indextn'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_dirsous/tmp.
7. Cas de tests — DAG smoke (medium)¶
- DS1 :
import dags.dag_diagnostic__s43sans 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_mdbanner, tagbuild=<sha>,event=dag_loadeden 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 | OUI — gated 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-unitvert : 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 siNOT_TRADEABLEstrict 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_parityreste 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.