PD-295 — Tests (cycle 3 v3)¶
1. Références¶
- Spécification :
PD-295-specification.md(cycle 3 v3) - Epic :
tooling - Scripts ciblés :
b2-sanitizer.py,compute-learning-scores.py,promote-learnings.py,gov-learnings-inject,rotate-audit-hmac.sh,sign-existing-state.py,lib/write-signed-state.py,check-no-verbatim-persisted.sh,purge-backups.sh,lib/story-hooks.sh
2. Matrice de couverture¶
| Invariant / CA | Test(s) |
|---|---|
| INV-295-01, CA-295-02 | TC-NOM-02, TC-ERR-14, TC-NEG-08 |
| INV-295-02 (code invariant) | TC-INV-02 |
| INV-295-03, CA-295-03 | TC-NOM-03 |
| INV-295-04..05, CA-295-04 | TC-NOM-04, TC-NOM-05, TC-NOM-27, TC-ERR-16 |
| INV-295-06..08, CA-295-05 | TC-NOM-06, TC-NOM-06-bis |
| INV-295-09, CA-295-06 | TC-ERR-10, TC-ERR-11, TC-ERR-20, TC-STATE-02 |
| INV-295-10, CA-295-07 | TC-NOM-08, TC-ERR-13 |
| INV-295-11, CA-295-08 | TC-NOM-08, TC-NEG-15 |
| INV-295-12, CA-295-14 | TC-NOM-09 |
| INV-295-13, CA-295-09 | TC-NOM-07, TC-NOM-10 |
| INV-295-14, CA-295-12 | TC-NOM-15, TC-NOM-17, TC-ERR-08, TC-ERR-09 |
| INV-295-15, CA-295-11 | TC-ERR-15, TC-NEG-06, TC-NEG-07 |
| INV-295-16, CA-295-15 (meta) | TC-META-01..04, TC-ERR-17 |
| INV-295-17 | TC-NEG-17 |
| INV-295-RUNTIME-01..03, CA-295-RUNTIME-01 | TC-RUNTIME-01, TC-RUNTIME-02, TC-RUNTIME-03 |
| INV-295-RUNTIME-04 | TC-NOM-24 |
| INV-295-STATE-01, CA-295-STATE-01 | TC-STATE-01 |
| INV-295-STATE-02 | TC-STATE-01, TC-STATE-03 |
| INV-295-STATE-03 | TC-STATE-04 |
| INV-295-STATE-04 | TC-NOM-22, TC-STATE-02 |
| INV-295-BACKUP-01, CA-295-BACKUP-01 | TC-NOM-21 |
| INV-295-LS-01..12, CA-295-10/11 | TC-NOM-11, TC-NOM-12, TC-NOM-13, TC-ERR-15 |
| INV-295-CL-01..08 | TC-NOM-14, TC-NOM-03, TC-NOM-04, TC-NOM-05, TC-NEG-07 |
| CA-295-01 | TC-NOM-01 |
| CA-295-13 | TC-NOM-10 |
| CA-295-16..19 (post-merge) | TC-NOM-18-A/B/C/D |
| CA-295-20 | TC-NOM-19 |
3. Scénarios nominaux¶
| TEST-ID | GIVEN | WHEN | THEN |
|---|---|---|---|
| TC-NOM-01 | Corpus veille valide + invalide | B1 collecte/indexe | veille.jsonl valide, filtres corrects, invalides ignorés tracés |
| TC-NOM-02 | 4 réponses PO avec PII test | B2 synthèse + validation PO=oui | Résumé non-PII persistant, verbatim absent |
| TC-NOM-03 | Résumé en attente | PO=non | REJECTED, aucune écriture |
| TC-NOM-04 | Story avec clarifications indexées | purge --story --force | 6 artefacts supprimés (dont docs/epics/**/PD-XX-*/PD-XX-clarifications.md) + traces query supprimées + état PURGED |
| TC-NOM-05 | Cas frontière retention_until | purge rétention | date > retention_until seul expire/purge |
| TC-NOM-06 | Vecteurs V1/V4/V6/V8 | vérification HMAC JCS | signatures exactes, V6 validée avec clé v2 |
| TC-NOM-06-bis | Objet arbitraire schéma v3 | HMAC(key, JCS(objet)) | signature attendue 2afd2cd6dcedc730e1abc41203bf40a67764d69704e19cfd032691879d0a3ab0 |
| TC-NOM-07 | Jeux de données saturants | calcul reuse_score | aucune valeur 1.0000, écrêtage 0.9999 si pré-arrondi >=0.99995 |
| TC-NOM-08 | Corpus nominal 3 sources + clé/lock OK | exécution B5 | section learnings/veille/clarifications avec count_configured, count_effective, under_corpus corrects + trace signée avant retour |
| TC-NOM-09 | Index clarifications multi-domain/project | appel sans puis avec flags | rejet sans flags, résultat cloisonné avec flags |
| TC-NOM-10 | Scores disponibles | /morning | exactement 3 lignes score=X.XXXX, tri stable |
| TC-NOM-11 | Learning A avec seuils atteints + learning B jamais injecté (nb_domains=0) | promote B4 | A: story->domain puis domain->global si seuil global; B: promotion bloquée, reste scope:story |
| TC-NOM-12 | learnings stale et témoins | archive B4 | stale -> ARCHIVED, témoins inchangés |
| TC-NOM-13 | learning ARCHIVED | restore manuel audité | retour STORY_ACTIVE uniquement |
| TC-NOM-14 | clarification VERBATIM_IN_MEMORY | 2 runs (PO oui/non) | transitions CL exactes |
| TC-NOM-15 | concurrence + lock stale + lock occupé | B3/B4/B5 en parallèle | stale récupéré 1 fois, timeout >30s => erreur |
| TC-NOM-16 | fenêtre 5 min + idempotence | 4e requête + replay identique | rate-limit rejeté, replay identique renvoie même résultat |
| TC-NOM-17 | 4 fichiers d’état + sessions | acquisition lock read/write | lockfiles dédiés créés/utilisés (learnings, scores, injections, sessions/current.lock), granularité par fichier respectée |
| TC-NOM-18-A | T0+30/60/90 | measure-cs1.sh | 3 rapports horodatés |
| TC-NOM-18-B | historique 90j | measure-cs2.sh | mesure + comparaison cible >=30% |
| TC-NOM-18-C | fenêtre glissante 30j | measure-cs3.sh | fréquence + comparaison cible |
| TC-NOM-18-D | 5 premières stories post-B5 | measure-cs4.sh | ratio + comparaison cible |
| TC-NOM-19 | baseline SQL connue | inspection livrables PD-295 | aucun artefact DDL SQL |
| TC-NOM-20 | story close REJECTED/DONE_WITH_ANOMALY/DONE | hook on_story_close | scripts/purge-clarifications.py --story {story_id} --mode eligible-only déclenché, idempotent |
| TC-NOM-21 | exclusion backup configurée | check exclusion + purge backup historique | artefacts PD-295 exclus (tmutil isexcluded/marker), purge-backups.sh effectif |
| TC-NOM-22 | clarifications mixtes (SUMMARY_INDEXED non expirée + EXPIRED) | hook auto puis manuel --force | hook: non expirée conservée, EXPIRED purgée; manuel --force: purge toutes clarifications story + chemin retry/alerte si échec |
| TC-NOM-24 | pii_ruleset_v1.yaml présent avec validation DPO signée | initialisation B2 | ruleset chargé (hash/version tracés), refus d’exécution B2 si validation DPO absente |
| TC-NOM-27 | clarification datée J0 | purge à J+540 puis J+541 | J+540 conservée (date == retention_until), J+541 purgée (date > retention_until) |
4. Cas d’erreur¶
| TEST-ID | Référence | Attendu |
|---|---|---|
| TC-ERR-01 | ERR-295-INVALID_STORY_ID | rejet immédiat, pas d’effet de bord |
| TC-ERR-02 | ERR-295-INVALID_DOMAIN | rejet immédiat |
| TC-ERR-03 | ERR-295-INVALID_PROJECT | rejet immédiat |
| TC-ERR-04 | ERR-295-INVALID_QUERY_LENGTH | rejet, pas de recherche aval |
| TC-ERR-05 | ERR-295-INVALID_QUERY_HASH | rejet trace invalide |
| TC-ERR-06 | ERR-295-RATE_LIMIT_EXCEEDED | 4e requête rejetée |
| TC-ERR-07 | ERR-295-IDEMPOTENCY_CONFLICT | rejet, état inchangé |
| TC-ERR-08 | ERR-295-LOCK_TIMEOUT | fail-closed sans écriture partielle |
| TC-ERR-09 | ERR-295-LOCK_STALE_RECOVERY_FAILED | fail-closed |
| TC-ERR-10 | ERR-295-AUDIT_KEY_UNAVAILABLE | arrêt /gov (SystemExit) |
| TC-ERR-11 | ERR-295-AUDIT_KEY_FORMAT_INVALID | SystemExit immédiat, aucun code retour au caller step 0 |
| TC-ERR-12 | ERR-295-HMAC_VERIFICATION_FAILED | trace rejetée + alerte |
| TC-ERR-13 | ERR-295-TRACE_WRITE_FAILED | EMPTY_BLOCK, step0 continue |
| TC-ERR-14 | ERR-295-PII_DETECTED | rejet résumé, aucune persistance |
| TC-ERR-15 | ERR-295-STATE_TRANSITION_FORBIDDEN | état inchangé |
| TC-ERR-16 | ERR-295-PURGE_VERIFICATION_FAILED | clôture purge rejetée + alerte |
| TC-ERR-17 | ERR-295-MEASURE_SCRIPT_MISSING | Gate 8 NON_CONFORME |
| TC-ERR-18 | ERR-295-UNSIGNED_ENTRY | compteur incrémenté, ligne ignorée, exécution continue |
| TC-ERR-19 | ERR-295-FAIL_CLOSED_RECURSION | arrêt process + alerte sécurité |
| TC-ERR-20 | ERR-295-ROTATION_BLOCKED | rotation refusée tant que lock session active (mtime <2h) |
5. Tests runtime B5 (subprocess verbatim)¶
TEST-ID: TC-RUNTIME-01
Référence: INV-295-RUNTIME-01, INV-295-RUNTIME-02, CA-295-RUNTIME-01
GIVEN
- `scripts/b2-sanitizer.py` actif
- verbatim fake contenant UUIDv4 unique
- session Claude Code active simulée (`CLAUDECODE=1`)
WHEN
- `CLAUDECODE=1 scripts/b2-sanitizer.py < verbatim.txt` est exécuté jusqu'à production du résumé
THEN
- `grep -R <UUIDv4> data/` retourne 0 occurrence
- aucun argument process (`ps`) ne contient l’UUID
- aucun fichier temporaire disque n’est créé
- le sous-subprocess `claude -p` n’hérite pas de `CLAUDECODE=1`
TEST-ID: TC-RUNTIME-02
Référence: INV-295-RUNTIME-01
GIVEN
- chaîne 2 niveaux active: parent -> `b2-sanitizer.py` -> `claude -p`
WHEN
- exécution avec `CLAUDE_DISABLE_SESSION_LOG=1` et `TERM=dumb`
THEN
- aucun nouvel enregistrement verbatim dans `data/sessions/*.jsonl`
- aucun passage verbatim via `argv`
TEST-ID: TC-RUNTIME-03
Référence: INV-295-RUNTIME-03
GIVEN
- verbatim fake avec termes sentinelles
WHEN
- subprocess B2 est invoqué
THEN
- événement signé `verbatim_subprocess_invoked` présent
- payload contient `{story_id,subprocess_name,duration_ms,success}`
- regex négative sur payload: aucune occurrence des sentinelles verbatim
6. Tests signature d’état B7¶
TEST-ID: TC-STATE-01
Référence: INV-295-STATE-01, INV-295-STATE-02, CA-295-STATE-01
GIVEN
- ligne manuelle non signée injectée:
echo '{"story":"PD-ATTACK","nb_injections":9999}' >> data/learnings-injections.jsonl
WHEN
- `scripts/compute-learning-scores.py` est exécuté
THEN
- ligne malveillante ignorée
- score de `PD-ATTACK` reste 0.0 / absent
- compteur `ERR-295-UNSIGNED_ENTRY` incrémenté
- événement signé `unsigned_entry_detected` émis
TEST-ID: TC-STATE-02
Référence: INV-295-09, INV-295-STATE-04
GIVEN
- fichiers d’état signés avec clé version N
- lock session `/gov` absent ou stale (`mtime >2h`)
WHEN
- `scripts/rotate-audit-hmac.sh` est exécuté hors session `/gov`
THEN
- lignes existantes re-signées version N+1
- événement V6 signé avec la clé N+1 (v2)
- vérification historique possible avec clé archivée N
TEST-ID: TC-STATE-03
Référence: INV-295-STATE-02
GIVEN
- lignes non signées injectées dans `data/learnings-injections.jsonl` et `data/learnings-scores.jsonl`
WHEN
- `scripts/promote-learnings.py` puis `gov-learnings-inject` sont exécutés
THEN
- lignes non signées ignorées au calcul de promotion et à l’injection
- compteur `ERR-295-UNSIGNED_ENTRY` incrémenté
TEST-ID: TC-STATE-04
Référence: INV-295-STATE-03
GIVEN
- tentative d’écriture manuelle directe (`echo >> data/learnings-scores.jsonl`)
WHEN
- lecture par `compute-learning-scores.py` ou `gov-learnings-inject`
THEN
- entrée ignorée; seules les écritures via `scripts/lib/write-signed-state.py` sont prises en compte
7. Tests méta Gate 8¶
8. Tests d’invariants¶
| Invariant | Test dédié | Observable |
|---|---|---|
| INV-295-02 | TC-INV-02 | lint statique passe |
| INV-295-17 | TC-NEG-17 | profondeur bornée |
| INV-295-BACKUP-01 | TC-NOM-21 | exclusions actives |
| INV-295-RUNTIME-01..03 | TC-RUNTIME-01..03 | aucune fuite verbatim |
| INV-295-RUNTIME-04 | TC-NOM-24 | ruleset DPO validé et tracé |
| INV-295-STATE-01..04 | TC-STATE-01..04, TC-NOM-22 | lignes non signées neutralisées + hook eligible-only |
TEST-ID: TC-INV-02
Référence: INV-295-02
GIVEN
- code source B2
WHEN
- `scripts/check-no-verbatim-persisted.sh` est exécuté
THEN
- aucune écriture disque liée au verbatim PO n’est détectée
- toute violation produit exit code non nul
9. Tests négatifs et adversariaux¶
| TEST-ID | Entrée abusive | Attendu |
|---|---|---|
| TC-NEG-01 | learning_id invalide dans result_ids[] | trace rejetée |
| TC-NEG-02 | source hors enum | trace rejetée |
| TC-NEG-03 | count non numérique/>999 | trace rejetée |
| TC-NEG-04 | timestamp non UTC conforme | trace rejetée |
| TC-NEG-05 | reuse_score ⅗ décimales | rejet format |
| TC-NEG-06 | transition STORY_ACTIVE->GLOBAL_ACTIVE | ERR-295-STATE_TRANSITION_FORBIDDEN |
| TC-NEG-07 | sortie de REJECTED/PURGED | ERR-295-STATE_TRANSITION_FORBIDDEN |
| TC-NEG-08 | résumé avec PII (6 cas fake: nom propre, email, téléphone, IBAN/BIC, adresse postale, identifiant externe) | chaque marqueur est remplacé par <REDACTED_FAMILY_N> avant écriture disque; sinon ERR-295-PII_DETECTED |
| TC-NEG-09 | query 0 et 501 chars | ERR-295-INVALID_QUERY_LENGTH |
| TC-NEG-10 | sntp -t 5 pool.ntp.org indisponible + cache /var/db/timed/... indisponible, ou dérive >500ms | ERR-295-NTP_UNREACHABLE ou ERR-295-CLOCK_DRIFT_EXCEEDED, fail-closed strict, abort /gov |
| TC-NEG-11 | idempotence divergente <5min | ERR-295-IDEMPOTENCY_CONFLICT |
| TC-NEG-12 | 4e requête <5min | ERR-295-RATE_LIMIT_EXCEEDED |
| TC-NEG-13 | frontière stale lock mtime=60s | non stale (strict >60) |
| TC-NEG-14 | key_version=0 | trace rejetée |
| TC-NEG-15 | corpus vide sur 3 sources B5 | count_effective=0, under_corpus=true, pas de non-conformité |
| TC-NEG-16 | bypass guard (10 cas G-295-01..10) | chaque tentative bloque fail-closed |
| TC-NEG-17 | chaîne fail-closed récursive | profondeur 3 acceptée; tentative d’incrément à 4 => ERR-295-FAIL_CLOSED_RECURSION + arrêt process |
10. Non-régression¶
| Test ID | Objet | Attendu |
|---|---|---|
| TC-NR-01 | B1 veille | index/recherche existants non cassés |
| TC-NR-02 | step0 sans mémoire dispo | continue sans crash |
| TC-NR-03 | interdiction mode dégradé non tracé | aucun bloc non vide sans trace |
| TC-NR-04 | intégrité learnings.jsonl | B3 n’altère pas source |
| TC-NR-05 | exclusion archived | pas de sortie recherche active |
| TC-NR-06 | guards scope limité | seulement commandes listées |
| TC-NR-07 | compatibilité stack | aucun composant hors scope |
| TC-NR-08 | absence DDL SQL | confirmé |
11. Observabilité minimale requise¶
- États:
scope,lifecycle_state,MEMORY_DEGRADED,MEMORY_HEALTHY,fail_closed_depth. - Logs: événements signés schéma v3 (
schema_version,event_type,brick,key_version,sig_hmac_sha256) incluantpurge_clarifications_failed. - Compteurs:
ERR-295-UNSIGNED_ENTRY. - Preuves: rapports purge, rapports scripts CS post-merge, résultat
test -xméta Gate 8. - Vérifications regex anti-fuite verbatim sur
data/etdata/sessions/*.jsonl.
12. Verdict QA¶
- ✅ Testable (avec instrumentation CI standard + scripts contractuels fournis)
- Réserve mineure: campagnes CA-295-16..19 différées par calendrier post-merge (normal, non bloquant Gate 8).
Changelog cycle3-v2 → cycle3-v3¶
- F-01 : TC-RUNTIME-01 teste CLAUDECODE=1 non hérité par sous-subprocess
- F-02 : TC-NOM-22 teste SUMMARY_INDEXED non purgée + EXPIRED purgée + --force purge tout
- F-03 : TC-NEG-10 utilise sntp comme source NTP
- F-04 : TC nouveau teste rotation bloquée si session active
- F-05 : TC-NOM-17 teste les 4 lockfiles
- F-06 : TC-NOM-06 vérifie V6 avec clé v2
- F-07 : matrice ST↔TC complétée