Aller au contenu
Tests de régression Publié le Par Guillaume Fossier

Un test de régression vert ne dit pas s'il détecte le défaut

En bref

Un test vert dit une seule chose : il passe sur le code corrigé. Il ne dit pas s'il échouerait sur le défaut qu'il doit attraper. Pour le savoir, il faut le lancer aussi sur ce défaut, déclaré à l'avance, et regarder comment il échoue. Dans une démonstration livrée avec l'outil, rejouée sur un défaut réel d'une bibliothèque, un test écrit pour être faible passe aussi sur le code fautif, et AssertLedger 1.1.1 le rejette. Cela montre le mécanisme, pas la fréquence de tels tests dans une suite réelle.

AssertLedger est un outil open source dont je suis l'auteur, développé avec l'aide d'un assistant IA (Claude), comme le montrent les mentions Co-Authored-By de l'historique du dépôt hoklims/assertledger. Il répond à une seule question, posée en tête de son README : un test de régression détecte-t-il réellement le défaut ? Ce texte n'est donc pas un avis extérieur : les sorties sont reproduites et les limites citées mot pour mot, pour que vous puissiez vérifier.

Il ne contient aucune statistique sur des suites de tests réelles. Les trois tests ci-dessous viennent de l'exemple livré avec l'outil ; le mode agent (skill, serveur MCP) n'a pas été exercé. Seul le candidat du délai dépassé, plus bas, a été écrit pour l'essai, par l'assistant qui a lancé les commandes. La question vaut pour un test écrit par une personne comme par un agent ; l'essai principal, lui, n'a mis en jeu que ces trois tests.

Conditions de l'essai

Outil
assertledger 1.1.1 (sortie de assertledger --version)
Date
29 septembre 2026, de 00 h 25 à 00 h 30 UTC
Environnement
Windows 11, Node v22.23.2, Git 2.50.1.windows.1
Isolation
aucune (mode trusted-local, « UNSANDBOXED »). L'outil propose aussi un mode conteneur, non exercé dans cet essai.
Non exécuté
assertledger 1.3.0, dernière version sur npm (publiée le 25 septembre 2026 à 22 h 13 UTC)

La 1.1.1 est la version que le registre du site épingle (statut « Démontré »). npm publie depuis une 1.3.0 (lue sur le registre npm le 29 septembre 2026), qui n'a pas été exécutée dans cet essai : ce qui suit n'est établi que pour la 1.1.1.

Le cas : un défaut réel, trois tests

Le README montre d'abord le mécanisme sur une fonction isEven (exemple classé « Démontré » au registre). L'outil livre aussi un cas historique : la correction d'un défaut de la bibliothèque escape-string-regexp, dans le commit 732905d du 7 avril 2020, intitulé « Escape - in a way that’s compatible with Unicode patterns (#21) ».

Avant la correction, la fonction renvoie \- pour un tiret. Dans une expression régulière avec le drapeau u, cette forme est invalide. Après, elle renvoie \u002d. Un contrôle sans AssertLedger, sur les deux fichiers livrés avec l'exemple, le confirme.

Contrôle sans AssertLedger · Node v22.23.2 · 00 h 27 UTC · code de sortie 0
before.cjs.txt escape('-') = "\\-" -> SyntaxError: Invalid regular expression: /\-/u: Invalid escape
fixed.cjs.txt escape('-') = "\\u002d" -> compiles with u flag

Les doubles antislash sont l'affichage JSON de \- et de \u002d.

Ces deux fichiers sont identiques, octet pour octet, au fichier index.js des commits amont 5085b25 et 732905d (empreintes SHA-256 comparées le 29 septembre 2026). Le dépôt Git de démonstration, lui, est une projection : trois commits créés par un script de l'outil, avec des tests node:test écrits par l'outil. Le README de l'exemple précise qu'il ne prétend pas exécuter les tests AVA d'origine de la bibliothèque.

Les trois commits : le défaut (be437c0c), la correction (3c46e97e) et un témoin neutre (6c703f81) qui ne change que la documentation. Trois tests candidats sont écrits par le script :

Les trois tests candidats · les mêmes trois lignes d'import précèdent chaque fichier
// weak.test.mjs
test("weak check", () => assert.equal(typeof escape("-"), "string"));

// strong.test.mjs
test("Unicode hyphen regression", () => {
  assert.doesNotThrow(() => new RegExp(escape("-"), "u"));
  assert.equal(new RegExp(escape("-"), "u").test("-"), true);
});

// crash.test.mjs
test("generic throw is not evidence", () => {
  if (escape("-") === "\\-") throw new Error("generic failure");
  assert.equal(typeof escape("-"), "string");
});
  • weak : vérifie seulement que le résultat est une chaîne.
  • strong : vérifie que le motif se compile avec le drapeau u et reconnaît le tiret.
  • crash : lève une exception générique quand il reconnaît la forme fautive \-.

Un quatrième fichier, base.test.mjs, vérifie que escape("hello") renvoie hello. Il reste identique dans les trois commits.

Les observations : trois tests, trois verdicts

Le README de l'exemple annonce ces trois résultats à l'avance, dans un tableau « Expected result » : la 1.1.1 les reproduit (contrôle du 29 septembre 2026). Le test faible a été écrit pour être faible : ce cas montre le mécanisme, pas la fréquence de tels tests dans une suite réelle.

Même commande, trois fois ; seuls --test et --out changent. Sorties de assertledger check, assertledger 1.1.1, 29 septembre 2026. Certaines lignes sont retirées, marquées […]. La commande a été lancée sur une seule ligne ; elle est coupée ici pour la lecture.

Commande, ici pour weak.test.mjs
assertledger check regression-demo \
  --before be437c0ce8b155277f87d38c6519da85663a0890 \
  --after 3c46e97e26f497f7a3adcf2c209283fd424e42ca \
  --neutral 6c703f810069afafdbe7562d454b26a7ae974557 \
  --neutral-reason "Documentation-only change to the corrected tree; no additional behavioral robustness claim" \
  --test weak.test.mjs --base-test base.test.mjs \
  --out evidence-weak --allow-unsafe-execution
weak.test.mjs · 00 h 26 UTC · code de sortie 2
# AssertLedger Git regression qualification

- Verdict: **REJECTED**
- Candidate: ` weak.test.mjs `
- Known bug commit: `be437c0ce8b155277f87d38c6519da85663a0890`
- Fixed commit: `3c46e97e26f497f7a3adcf2c209283fd424e42ca`
- Known-bug observations: attempt 1: PASS, attributed=true; attempt 2: PASS, attributed=true
- Reason codes: NO_ELIGIBLE_CANDIDATE
- Candidate reasons (git-regression-candidate): TARGET_STRENGTH_INSUFFICIENT
- Neutral reason: Documentation\-only change to the corrected tree; no additional behavioral robustness claim
- Execution: **UNSANDBOXED trusted-local** (explicit operator opt-in)

NO_ELIGIBLE_CANDIDATE: No candidate passed all required evidence gates.
Next action: Read the candidate reasons and fix the first failed gate before rerunning.
TARGET_STRENGTH_INSUFFICIENT: The candidate did not detect enough declared bugs through an attributed assertion failure.
Next action: Assert the corrected behavior, then rerun the same candidate against the declared buggy and control revisions.

[…]
A REJECTED verdict only describes this declared fault model; it does not prove that the test has no value elsewhere.
strong.test.mjs · 00 h 26 UTC · code de sortie 0
# AssertLedger Git regression qualification

- Verdict: **VERIFIED**
- Candidate: ` strong.test.mjs `
[…]
- Known-bug observations: attempt 1: ASSERTION_FAILURE, attributed=true; attempt 2: ASSERTION_FAILURE, attributed=true
- Reason codes: POLICY_SATISFIED
- Candidate reasons (git-regression-candidate): POLICY_SATISFIED
[…]

POLICY_SATISFIED: The observations satisfy the declared policy.
Next action: Replay the manifest and review the declared worlds and limitations before relying on it.
[…]
crash.test.mjs · 00 h 26 UTC · code de sortie 2
# AssertLedger Git regression qualification

- Verdict: **REJECTED**
- Candidate: ` crash.test.mjs `
[…]
- Known-bug observations: attempt 1: PROCESS_CRASH, attributed=false; attempt 2: PROCESS_CRASH, attributed=false
- Reason codes: NO_ELIGIBLE_CANDIDATE
- Candidate reasons (git-regression-candidate): CANDIDATE_DISCOVERY_INVALID
[…]

CANDIDATE_DISCOVERY_INVALID: The runner did not discover and identify the expected candidate test.
Next action: Check the committed test path, title and runner selection; ensure exactly the intended test is discovered.
NO_ELIGIBLE_CANDIDATE: No candidate passed all required evidence gates.
Next action: Read the candidate reasons and fix the first failed gate before rerunning.
[…]

Les codes de sortie (0, 2) sont ceux relevés dans cet essai. Les manifestes enregistrés donnent le détail des observations, deux tentatives par cas :

Observations enregistrées pour chaque test candidat, sur le code corrigé, le défaut connu et le témoin neutre
TestCode corrigéDéfaut connuTémoin neutreVerdict
weakPASS ×2PASS ×2PASS ×2REJECTED
TARGET_STRENGTH_INSUFFICIENT
strongPASS ×2ASSERTION_FAILURE ×2, attribuéPASS ×2VERIFIED
crashPASS ×2PROCESS_CRASH ×2, non attribuéPASS ×2REJECTED
CANDIDATE_DISCOVERY_INVALID

Deux essais qui ne sont pas dans le README

Ils ont été ajoutés pour mettre à l'épreuve deux affirmations du README. Le premier porte sur le rejeu.

Rejeu du manifeste du test faible · 00 h 26 UTC · code de sortie 0
assertledger replay regression-demo/evidence-weak/manifest.json --json
{"valid":true,"schemaValid":true,"decisionDigestValid":true,"artifactDigestValid":true,"decisionSemanticsValid":true}

Les trois manifestes se rejouent valides, y compris les deux rejetés. Le rejeu confirme la cohérence du fichier, pas la qualité du test. Dans une copie du manifeste de l'exemple isEven, une observation du test faible a ensuite été modifiée directement dans le fichier JSON (PASS devenu ASSERTION_FAILURE) :

Rejeu du manifeste modifié · 00 h 25 UTC · code de sortie 4
assertledger replay manifest-tampered.json --json
{"valid":false,"schemaValid":true,"decisionDigestValid":false,"artifactDigestValid":false,"decisionSemanticsValid":false}

Une édition naïve est détectée. Cela ne montre pas qu'un producteur qui recalcule les empreintes le serait : le README dit que le rejeu n'authentifie pas celui qui a produit les observations.

Le second essai porte sur le délai dépassé. Sur l'exemple isEven, avec un délai de 1 000 ms par exécution, un candidat reste 15 secondes dans une boucle quand isEven(2) ne vaut pas true. Il « détecte » donc le défaut en se bloquant. Le rejeu de ce manifeste est valide lui aussi.

Champs extraits du manifeste JSON et mis en forme par un script de lecture, ce n'est pas la sortie brute · 00 h 26 UTC · code de sortie 3
décision  INCONCLUSIVE · aucun candidat retenu · CANDIDATE_EVIDENCE_INCONCLUSIVE
candidat  timeout-only · INCONCLUSIVE · CANDIDATE_EXECUTION_INCONCLUSIVE
portes    COMPLETENESS=PASSED STABILITY=PASSED DISCOVERY=FAILED
          REFERENCE=NOT_RUN NEUTRAL=NOT_RUN TARGET_STRENGTH=NOT_RUN

monde                        tentatives 1 et 2
reference                    PASS, attribué
neutral-bitwise-equivalence  PASS, attribué
target-parity-inversion      TIMEOUT, non attribué

Ce que ces sorties permettent de dire

Le test faible est vert partout : sur le code corrigé, sur le code fautif et sur le témoin. Sa réussite ne distingue pas le code corrigé du code fautif. Elle aurait été la même sans le correctif.

Le test fort échoue par une assertion sur le code fautif, aux deux tentatives, et passe sur le code corrigé et sur le témoin. C'est cette différence, et non le vert, que l'outil enregistre.

Le troisième test est rouge sur le code fautif et n'est pourtant pas crédité. Il est écrit pour lever son exception quand il reconnaît la forme fautive, mais l'outil n'accepte comme détection qu'un échec d'assertion attribué au test (modèle de preuve). Le même principe s'applique au délai dépassé de l'essai ci-dessus. La documentation le dit en français :

« Un délai dépassé, une compilation impossible ou une exception générique ne prouvent pas que le test détecte le défaut déclaré. »

docs/diagnostics.md, section « Français »

On peut dire

« Ce test échoue par une assertion, deux fois sur deux, sur le commit fautif be437c0c. Il passe sur le commit corrigé 3c46e97e et sur le témoin 6c703f81. Mesuré avec assertledger 1.1.1, sans isolation, le 29 septembre 2026. »

On ne peut pas dire

  • que ce test détecte les défauts de cette famille ;
  • que le test est bon, ou que la correction est complète ;
  • que le code n'a plus de défaut.

Que le candidat vienne d'une personne ou d'un agent, la phrase « On peut dire » est celle que la sortie permet de soutenir. Elle décrit ce que le test a fait dans des mondes déclarés, pas ce qu'il vaut ailleurs.

Ce que ça ne prouve pas

Deux listes : ce que l'outil déclare lui-même ne pas prouver, puis ce que cet essai ne couvre pas. Les citations sont en anglais, telles quelles ; les liens pointent vers le tag v1.1.1. Le registre du site range d'ailleurs sous le statut « Non établi », notamment, l'isolement de l'exécution et la correction générale du programme.

Ce que l'outil déclare ne pas prouver

  • Le code s'exécute sans isolation.

    “Run trusted code only. --allow-unsafe-execution authorizes local code execution. This backend is explicitly UNSANDBOXED. Use a trusted checkout; it cannot contain hostile code.”

    README, « Try the example ». Un test proposé par un agent est du code qui s'exécute sur votre machine dans ces conditions : relisez-le avant de le lancer. La section « Limitations » de chaque sortie le répète :

    Section « Limitations » de la sortie, identique pour les trois tests
    ## Limitations:
    
    - UNSANDBOXED trusted\-local execution cannot safely contain hostile candidate code\.
    - Process\-tree termination is best effort and depends on host operating\-system facilities\.
    - Git regression qualification covers committed revisions only; dirty checkout content is ignored\.
    - The first Git slice supports node:test JavaScript with built\-in and relative imports and no runtime dependencies\.
    - The operator supplied the neutral revision and reason; AssertLedger does not infer its independence\.
    - Manifest integrity binds recorded Git provenance but replay does not independently authenticate Git objects or execution\.
  • Un délai dépassé, une erreur de syntaxe, un échec de collecte ou un plantage ne comptent jamais comme détection.

    “A timeout, syntax error, collection failure or process crash never counts as a detected bug.”

    README, « Understand the result ». Constaté ci-dessus sur le plantage et sur le délai dépassé.

  • Un rejet ne juge que le défaut déclaré.

    “A rejection concerns the declared fault model; the test may still have value elsewhere.”

    Même section du README.

  • Le défaut et le témoin viennent de l'opérateur.

    “The operator supplies the fault and the neutral control. AssertLedger does not invent their meaning.”

    README, « A passing test can miss the bug ». Dans ce cas, le témoin ne change que la documentation. Le README de l'exemple le dit :

    “The neutral commit changes only documentation, so it is a declared control, not an independent demonstration of robustness.”

    Le vert du témoin ne doit donc pas être lu comme une validation indépendante.

  • Le rejeu vérifie le fichier, pas les faits.

    “Replay checks integrity and decision consistency. It does not authenticate whoever produced the observations, prove general program correctness or guarantee permanent freedom from flaky tests.”

    Même section du README. Les deux manifestes rejetés se rejouent valides : c'est cohérent avec cette phrase.

  • « VERIFIED » a un sens étroit.

    “A VERIFIED decision means only that the selected candidate produced the policy-required, repeatable observations in the declared worlds. It is not proof of program correctness, absence of flakiness, absence of bugs, complete fault detection, or resistance to a candidate designed to recognize the worlds.”

    SECURITY.md, « Semantic boundary ». Le dernier point, un test conçu pour reconnaître les mondes, n'a pas été essayé ici.

  • Le résultat ne se généralise pas.

    “This statement applies only to the recorded attempts and worlds. It does not generalize to unexecuted states, future environments, or faults outside the supplied targets.”

    docs/proof-model.md, « What VERIFIED means ». La liste complète des non-affirmations est dans « Explicit non-claims ».

Ce que cet essai ne couvre pas

  • Un seul poste (Windows 11), une série d'essais et une répétition du cas fort. Deux créations successives du dépôt de démonstration ont donné les mêmes trois identifiants de commit et la même empreinte de décision pour le test fort, sur cette machine seulement : sha256:090cf7b86ba678851c5d201b75aa4eab0db6c52d73e1cd47e04e726c02751d42. L'empreinte d'artefact, elle, diffère d'une exécution à l'autre.
  • Ni Linux ni macOS, ni mode conteneur, ni tests instables, ni dépôt réel. Le flux Git n'a été lancé qu'avec un seul test candidat par check ; la seule campagne à deux candidats est celle de l'exemple isEven.
  • Les trois tests viennent de l'exemple livré, que son README décrit comme « AssertLedger-authored » ; ce texte ne dit pas qui les a rédigés. Le candidat du délai dépassé a été écrit pour l'essai par l'assistant. Le mode décrit dans la section « Use it with an agent » du README (un agent propose un candidat, le moteur évalue) et le serveur MCP n'ont pas été exercés.
  • Aucune statistique, aucun gain de temps, aucun avant/après. Les observations portent sur l'exemple isEven et sur un module historique projeté dans un dépôt de démonstration. Le registre du site range d'ailleurs les chiffres avant/après sous le statut « Non établi » (team.figures).
  • Le commit amont modifie aussi test.js, le test AVA du projet. Ce test n'est pas exécuté ici.
  • Les dates des trois commits de démonstration sont fictives (1er janvier 2000, fixé par le script). Ne les lisez pas comme des dates réelles.
  • Pas de comparaison avec la vérification à la main, qui consiste à lancer le nouveau test sur le commit d'avant.

L'essayer vous-même

Le README de la 1.1.1 donne ces commandes, à exécuter sur du code de confiance seulement. Il faut Git, Node.js 22.15 ou plus, et pnpm 11.1.2 (section « Try the example »).

README 1.1.1, commandes recopiées (deux blocs réunis)
git clone --branch v1.1.1 https://github.com/hoklims/assertledger.git
cd assertledger
pnpm install --frozen-lockfile
pnpm build
node dist/cli.js verify examples/node-test/request.json --allow-unsafe-execution --json

Décision attendue selon le README : VERIFIED, candidat strong retenu, POLICY_SATISFIED. C'est ce qui a été obtenu le 29 septembre 2026, avec le binaire déjà installé (assertledger verify …) plutôt qu'avec un clone construit. Le README de la 1.3.0 ajoute node dist/cli.js demo --allow-unsafe-execution : cette commande n'existe pas dans la 1.1.1 (code de sortie 64, essayé).

Pour le cas de cet article, le README de l'exemple demande d'installer le paquet (npm install --save-dev [email protected]), puis de lancer la commande suivante, en visant un dossier qui n'existe pas encore :

README de l'exemple, tel quel
node node_modules/assertledger/examples/git-history/create-demo.mjs regression-demo

La sortie JSON donne before, after, neutral, le test et le motif du témoin. Recopiez ces valeurs dans la commande check montrée plus haut, en changeant --test et --out pour chacun des trois tests. Les identifiants de commit obtenus lors de l'essai sont ceux de cet article ; rien ne garantit les mêmes sur une autre machine.

D'après le README, le flux Git de la 1.1.1 suppose un test node:test en JavaScript commité, sans dépendance d'exécution déclarée, un test de base inchangé dans chaque révision et les mêmes chemins de fichiers hors du test candidat. Les ajouts, suppressions et renommages de fichiers ne sont pas encore qualifiés.

Une décision à prendre côté engineering

La décision porte sur votre définition de « terminé » pour un correctif : un test de régression vert n'y compte que si on l'a vu échouer, par une assertion, sur le commit fautif. Cela se vérifie déjà sans outil, en lançant le nouveau test sur le commit d'avant ; cet essai ne compare pas cette voie à AssertLedger.

D'après son README, l'outil ajoute des exécutions répétées et un test de base (pour attribuer la différence au test), un témoin neutre et une preuve rejouable. Cet essai ne mesure ni ce que ces ajouts valent, ni la fréquence des tests faibles dans une suite réelle : aucune mesure de ce genre n'a été faite.

Vos agents écrivent déjà des tests ?

En cinq jours, le Diagnostic Agents mesure votre revue, audite votre setup d'agents et pose une première porte dans votre CI. Il rend un verdict : ce qui tient, ce qui casse, ce qui vaut d'être industrialisé.

Voir le Diagnostic Agents