gestion_table_tournante_libre/scripts/mutation/rapport.test.js
Mathieu Benoit aaa2c1a3a7 [IMP] mutation: count long-series kills apart, test the audit end to end
A mutant the node series missed but the long series killed was still
counted as a survivor, so the committed audit overstated what escapes
the tests. It now has its own column: 15 of the 137 survivors move
there, leaving 122; the committed files are regenerated from their own
results, without a rerun. The audit itself now runs in the long series
on a small fixture tree: long-series replay, exit code 0 with
survivors, exit 2 and nothing written when the baseline fails. The
committed audit files are read back and must match what rediger writes.
Checked: 3868 node and 105 long tests from the index alone.

--- FR ---

[IMP] mutation : tués par la série longue à part, audit éprouvé entier

Un mutant que la série node manquait et que la série longue tuait
comptait encore comme survivant : l'audit commis surestimait ce qui
échappe aux épreuves. Il a sa colonne : 15 des 137 survivants y passent,
il en reste 122 ; les fichiers commis sont réengendrés de leurs propres
résultats, sans rejeu. L'audit tourne entier dans la série longue sur un
petit arbre : rejeu long, sortie 0 avec survivants, sortie 2 et rien
d'écrit quand la ligne de base tombe. Les fichiers commis sont relus et
doivent égaler ce que rediger écrit.
Vérifié : 3868 node et 105 longues depuis l'index seul.

Assisted-by: Claude Opus 5.5
2026-10-10 18:51:43 -04:00

431 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le rapport de l'audit par mutation (§ 14.13) : rediger rend le Markdown et
// le JSON d'un audit, sur des résultats inventés. Les modules nommés ici
// n'existent pas : rediger ne lit aucun fichier. La dernière partie lit, elle,
// les trois fichiers committés d'audit/, comme du texte, sans importer
// audit.js.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { VERSION } from '../../src/version.genere.js';
import { describe, test } from '../../test/lanceur.js';
import { equivalentsDe, rediger, relire } from './rapport.js';
const RACINE = fileURLToPath(new URL('../../', import.meta.url));
const ENTETE = Object.freeze({
version: VERSION.affichee,
perimetre: ['src/verger'],
empreinte: 'a'.repeat(64),
node: 'v26.1.0',
});
// Un mutant inventé, aux champs du contrat de mutants.js.
function mutant(fichier, ligne, operateur, original, remplacement, colonne = 5) {
return {
cle: `${fichier}:${operateur}:${ligne}${colonne}:0`,
fichier,
ligne,
colonne,
operateur,
original,
remplacement,
debut: ligne * 100 + colonne,
fin: ligne * 100 + colonne + original.length,
};
}
const resultat = (m, issue, { couverte = true, longue = null, equivalent = null } = {}) => ({
mutant: m,
issue,
couverte,
longue,
equivalent,
});
// Neuf résultats sur deux modules, dans le désordre :
// pommes.js arithmetique tué ; frontiere survivant couvert (l. 30) ;
// negation survivant non couvert (l. 4) ; nombre-zero délai ;
// si-vrai survivant équivalent
// poires.js arithmetique survivant couvert (l. 12), tué par la série
// longue, donc compté à part des survivants ; frontiere tué ;
// logique erreur ; frontiere survivant couvert (l. 12,
// colonne 2)
const RESULTATS = Object.freeze([
resultat(mutant('src/verger/pommes.js', 3, 'arithmetique', '+', '-'), 'tue'),
resultat(mutant('src/verger/poires.js', 12, 'arithmetique', '*', '/'), 'survivant', { longue: 'tue' }),
resultat(mutant('src/verger/pommes.js', 30, 'frontiere', '<', '<='), 'survivant', { longue: 'survivant' }),
resultat(mutant('src/verger/pommes.js', 4, 'negation', '===', '!=='), 'survivant', { couverte: false }),
resultat(mutant('src/verger/poires.js', 7, 'frontiere', '>', '>='), 'tue'),
resultat(mutant('src/verger/pommes.js', 9, 'nombre-zero', '1', '0'), 'delai'),
resultat(mutant('src/verger/poires.js', 20, 'logique', 'a && b', '((a )||( b))'), 'erreur'),
resultat(mutant('src/verger/pommes.js', 15, 'si-vrai', 'n > 0', 'true'), 'survivant', {
equivalent: 'le test est toujours vrai à cet endroit',
}),
resultat(mutant('src/verger/poires.js', 12, 'frontiere', '<=', '<', 2), 'survivant'),
]);
const lieux = (liste) => liste.map(({ fichier, ligne, colonne }) => `${fichier}:${ligne}:${colonne}`);
describe('rapport de mutation : les comptes', () => {
const { json } = rediger({ entete: ENTETE, resultats: RESULTATS });
const lu = JSON.parse(json);
test('le bilan compte chaque issue ; un survivant que la série longue tue, et un équivalent, à part', () => {
assert.deepEqual(lu.bilan, { total: 9, tues: 2, tuesSerieLongue: 1, survivants: 3, equivalents: 1, delais: 1, erreurs: 1 });
});
test('par fichier, dans l’ordre des chemins', () => {
assert.deepEqual(lu.parFichier, [
{ fichier: 'src/verger/poires.js', total: 4, tues: 1, tuesSerieLongue: 1, survivants: 1, equivalents: 0, delais: 0, erreurs: 1 },
{ fichier: 'src/verger/pommes.js', total: 5, tues: 1, tuesSerieLongue: 0, survivants: 2, equivalents: 1, delais: 1, erreurs: 0 },
]);
});
test('par opérateur, dans l’ordre d’OPERATEURS', () => {
const vide = { tues: 0, tuesSerieLongue: 0, survivants: 0, equivalents: 0, delais: 0, erreurs: 0 };
assert.deepEqual(lu.parOperateur, [
{ operateur: 'arithmetique', total: 2, ...vide, tues: 1, tuesSerieLongue: 1 },
{ operateur: 'frontiere', total: 3, ...vide, tues: 1, survivants: 2 },
{ operateur: 'negation', total: 1, ...vide, survivants: 1 },
{ operateur: 'logique', total: 1, ...vide, erreurs: 1 },
{ operateur: 'nombre-zero', total: 1, ...vide, delais: 1 },
{ operateur: 'si-vrai', total: 1, ...vide, equivalents: 1 },
]);
});
test('le Markdown porte les mêmes comptes, en lignes de tableau', () => {
const { markdown } = rediger({ entete: ENTETE, resultats: RESULTATS });
const lignes = markdown.split('\n');
assert.ok(lignes.includes('| mutants | tués | tués par la série longue | survivants | équivalents | délais | erreurs |'), 'en-tête du bilan');
assert.ok(lignes.includes('| 9 | 2 | 1 | 3 | 1 | 1 | 1 |'), 'ligne du bilan');
assert.ok(lignes.includes('| `src/verger/poires.js` | 4 | 1 | 1 | 1 | 0 | 0 | 1 |'), 'ligne de poires.js');
assert.ok(lignes.includes('| `src/verger/pommes.js` | 5 | 1 | 0 | 2 | 1 | 1 | 0 |'), 'ligne de pommes.js');
assert.ok(lignes.includes('| frontiere | 3 | 1 | 0 | 2 | 0 | 0 | 0 |'), 'ligne de frontiere');
});
test('l’en-tête se recopie tel quel quand il ne nomme pas la machine', () => {
assert.deepEqual(lu.entete, { ...ENTETE });
const { markdown } = rediger({ entete: ENTETE, resultats: RESULTATS });
for (const valeur of [ENTETE.version, ENTETE.empreinte, ENTETE.node, 'src/verger']) {
assert.ok(markdown.includes(valeur), valeur);
}
});
});
describe('rapport de mutation : l’ordre', () => {
const { json, markdown } = rediger({ entete: ENTETE, resultats: RESULTATS });
const lu = JSON.parse(json);
test('les survivants couverts d’abord, puis par fichier, ligne et colonne', () => {
assert.deepEqual(lieux(lu.survivants), ['src/verger/poires.js:12:2', 'src/verger/pommes.js:30:5', 'src/verger/pommes.js:4:5']);
assert.deepEqual(
lu.survivants.map(({ couverte, longue }) => [couverte, longue]),
[
[true, null],
[true, 'survivant'],
[false, null],
],
);
});
test('un survivant que la série longue tue se liste à part, entre les survivants et les équivalents', () => {
assert.deepEqual(
lu.tuesSerieLongue.map((r) => [`${r.fichier}:${r.ligne}:${r.colonne}`, r.couverte, r.longue]),
[['src/verger/poires.js:12:5', true, 'tue']],
);
const section = (titre) => markdown.indexOf(`\n## ${titre}`);
const ici = markdown.indexOf('`src/verger/poires.js:12:5`');
assert.ok(section('Survivants (3)') > 0, 'trois survivants annoncés');
assert.ok(section('Tués par la série longue (1)') > section('Survivants (3)'), 'la section suit les survivants');
assert.ok(ici > section('Tués par la série longue (1)') && ici < section('Mutants équivalents (1)'), 'le mutant est dans sa section');
assert.equal(markdown.indexOf('`src/verger/poires.js:12:5`', ici + 1), -1, 'et nulle part ailleurs');
});
test('dans un même fichier, les lignes en ordre numérique : 9 avant 12', () => {
// L'opérateur de la ligne 12 précède celui de la ligne 9 dans OPERATEURS,
// et « 12 » précède « 9 » en ordre lexical : seul l'ordre numérique des
// lignes met la ligne 9 en tête.
const paire = [
resultat(mutant('src/verger/pommes.js', 12, 'arithmetique', '+', '-'), 'survivant'),
resultat(mutant('src/verger/pommes.js', 9, 'frontiere', '<', '<='), 'survivant'),
];
for (const resultats of [paire, [...paire].reverse()]) {
const { survivants } = JSON.parse(rediger({ entete: ENTETE, resultats }).json);
assert.deepEqual(lieux(survivants), ['src/verger/pommes.js:9:5', 'src/verger/pommes.js:12:5']);
}
});
test('le Markdown range les survivants dans le même ordre, l’équivalent dans sa section', () => {
const rangs = lieux(lu.survivants).map((lieu) => markdown.indexOf(`\`${lieu}\``));
assert.ok(rangs.every((rang) => rang > 0), 'chaque survivant est nommé');
assert.deepEqual([...rangs].sort((a, b) => a - b), rangs);
const section = markdown.indexOf('## Mutants équivalents');
assert.ok(section > rangs.at(-1), 'la section des équivalents suit les survivants');
assert.ok(markdown.indexOf('`src/verger/pommes.js:15:5`') > section);
assert.ok(markdown.includes('le test est toujours vrai à cet endroit'));
});
test('équivalents, délais et erreurs sont listés à part, hors des survivants', () => {
assert.deepEqual(lieux(lu.equivalents), ['src/verger/pommes.js:15:5']);
assert.equal(lu.equivalents[0].raison, 'le test est toujours vrai à cet endroit');
assert.deepEqual(
lu.delaisEtErreurs.map((r) => `${r.fichier}:${r.ligne} ${r.issue}`),
['src/verger/poires.js:20 erreur', 'src/verger/pommes.js:9 delai'],
);
});
test('l’ordre des résultats reçus ne change pas un octet de la sortie', () => {
const renverse = rediger({ entete: ENTETE, resultats: [...RESULTATS].reverse() });
assert.equal(renverse.markdown, markdown);
assert.equal(renverse.json, json);
});
});
describe('rapport de mutation : ce qu’il refuse', () => {
test('des résultats vides ou absents', () => {
assert.throws(() => rediger({ entete: ENTETE, resultats: [] }), /aucun résultat/);
assert.throws(() => rediger({ entete: ENTETE }), /aucun résultat/);
});
test('aucune date ni heure dans le texte', () => {
const { markdown, json } = rediger({ entete: ENTETE, resultats: RESULTATS });
for (const texte of [markdown, json]) {
assert.doesNotMatch(texte, /\d{4}-\d{2}-\d{2}/);
assert.doesNotMatch(texte, /\d{1,2}:\d{2}:\d{2}/);
assert.doesNotMatch(texte, /\b(?:date|instant|horodatage)\b/i);
}
});
test('un en-tête qui nomme la machine : un nom d’hôte, un chemin de compte', () => {
const refuse = (entete) =>
assert.throws(() => rediger({ entete, resultats: RESULTATS }), /ne nomme pas sa machine/, JSON.stringify(entete));
refuse({ ...ENTETE, hote: 'atelier-ombrelle' });
refuse({ ...ENTETE, poste: 'atelier-ombrelle.local' });
refuse({ ...ENTETE, node: 'atelier-ombrelle' });
refuse({ ...ENTETE, node: '/home/exemple/.local/node/bin/node' });
refuse({ ...ENTETE, perimetre: ['/home/exemple/verger/src/verger'] });
refuse({ ...ENTETE, perimetre: ['C:\\Users\\exemple\\verger'] });
refuse({ ...ENTETE, date: 'mardi' });
refuse({ ...ENTETE, node: 'v26.0.0-/home/exemple' });
});
test('une version de Node avec un suffixe de préversion, sans blanc ni barre oblique', () => {
for (const node of ['v26.0.0-rc.1', 'v27.0.0-pre']) {
assert.equal(JSON.parse(rediger({ entete: { ...ENTETE, node }, resultats: RESULTATS }).json).entete.node, node);
}
for (const node of ['v26.0.0-rc 1', 'v26.0.0-a/b', 'v26.0.0-', 'v26.0.0+rc']) {
assert.throws(() => rediger({ entete: { ...ENTETE, node }, resultats: RESULTATS }), /en-tête : node/, node);
}
});
test('une version que la dérivation de scripts/version.js refuse', () => {
const [annee, , , rang] = VERSION.affichee.split('.');
for (const version of [[annee, '13', '45', rang].join('.'), `${VERSION.affichee}2`]) {
assert.throws(() => rediger({ entete: { ...ENTETE, version }, resultats: RESULTATS }), /en-tête : version/, version);
}
});
test('un en-tête incomplet ou d’une autre forme', () => {
const refuse = (entete) => assert.throws(() => rediger({ entete, resultats: RESULTATS }), TypeError, JSON.stringify(entete));
const { empreinte, ...sansEmpreinte } = ENTETE;
assert.equal(empreinte.length, 64);
refuse(sansEmpreinte);
refuse({ ...ENTETE, empreinte: 'b'.repeat(63) });
refuse({ ...ENTETE, version: 'v1' });
refuse({ ...ENTETE, perimetre: [] });
refuse({ ...ENTETE, perimetre: ['src/../verger'] });
refuse(null);
});
test('un résultat d’une autre forme : issue, opérateur, chemin, équivalence, rejeu, clé', () => {
const [premier, second] = RESULTATS;
const refuse = (remplace, motif) =>
assert.throws(() => rediger({ entete: ENTETE, resultats: [remplace, second] }), motif, JSON.stringify(remplace));
refuse({ ...premier, issue: 'blesse' }, /issue/);
refuse({ ...premier, mutant: { ...premier.mutant, operateur: 'inversion' } }, /opérateur/);
refuse({ ...premier, mutant: { ...premier.mutant, fichier: '/home/exemple/verger/pommes.js' } }, /ne nomme pas sa machine/);
refuse({ ...premier, mutant: { ...premier.mutant, fichier: '../verger/pommes.js' } }, /chemin/);
refuse({ ...premier, mutant: { ...premier.mutant, ligne: 0 } }, /ligne/);
refuse({ ...premier, equivalent: 'tué pourtant' }, /équivalent/);
for (const equivalent of ['vu dans /home/exemple/notes.txt', 'vu dans C:\\Users\\exemple\\notes.txt']) {
assert.throws(
() => rediger({ entete: ENTETE, resultats: [{ ...RESULTATS[2], equivalent }, second] }),
/ne nomme pas sa machine : chemin de compte dans résultat 0, équivalent/,
equivalent,
);
}
refuse({ ...premier, longue: 'tue' }, /série longue/);
assert.throws(
() => rediger({ entete: ENTETE, resultats: [{ ...RESULTATS[1], equivalent: 'déclaré à tort' }] }),
/équivalent/,
'un survivant que la série longue tue n’est pas équivalent',
);
refuse({ ...premier, couverte: 'oui' }, /couverte/);
refuse({ ...premier, mutant: { ...premier.mutant, cle: second.mutant.cle } }, /clé/);
});
});
describe('rapport de mutation : les cellules de code', () => {
test('une barre verticale s’échappe, un nœud sur plusieurs lignes tient sur une', () => {
const resultats = [
resultat(mutant('src/verger/pommes.js', 2, 'logique', 'a ||\n b', '((a )&&(\n b))'), 'survivant'),
resultat(mutant('src/verger/pommes.js', 3, 'corps-si', `{ ${'x += 1; '.repeat(20)}}`, '{}'), 'survivant'),
];
const { markdown, json } = rediger({ entete: ENTETE, resultats });
const ligne = markdown.split('\n').find((l) => l.includes('`src/verger/pommes.js:2:5`'));
assert.ok(ligne.includes('`a \\|\\| b`'), ligne);
assert.ok(ligne.includes('`((a )&&( b))`'), ligne);
const longue = markdown.split('\n').find((l) => l.includes('`src/verger/pommes.js:3:5`'));
assert.ok(longue.includes('…`'), longue);
assert.ok(longue.length < 200, longue);
assert.equal(JSON.parse(json).survivants[0].original, 'a ||\n b');
});
test('un accent grave dans le code garde la cellule entière', () => {
const resultats = [resultat(mutant('src/verger/pommes.js', 2, 'logique', 'a && `x`', '((a )||( `x`))'), 'survivant')];
const { markdown } = rediger({ entete: ENTETE, resultats });
assert.ok(markdown.includes('`` a && `x` ``'), markdown);
});
});
describe('rapport de mutation : la relecture du JSON écrit', () => {
const ecrit = rediger({ entete: ENTETE, resultats: RESULTATS });
// Le JSON écrit, modifié par changer sur sa forme lue, réécrit comme
// rediger l'écrit.
const modifie = (changer) => {
const lu = JSON.parse(ecrit.json);
changer(lu);
return `${JSON.stringify(lu, null, 2)}\n`;
};
test('relire rend le Markdown que rediger a écrit, octet pour octet', () => {
const relu = relire(ecrit.json);
assert.equal(relu.markdown, ecrit.markdown);
assert.deepEqual(relu.donnees, JSON.parse(ecrit.json));
});
test('relire refuse un JSON réindenté, un en-tête qui nomme la machine, des comptes contraires aux listes', () => {
assert.throws(() => relire(JSON.stringify(JSON.parse(ecrit.json))), /forme/);
assert.throws(() => relire('{'), /illisible/);
assert.throws(() => relire(modifie((lu) => Object.assign(lu.entete, { hote: 'atelier-ombrelle' }))), /ne nomme pas sa machine/);
assert.throws(() => relire(modifie((lu) => Object.assign(lu, { format: 2 }))), /format/);
assert.throws(() => relire(modifie((lu) => (lu.bilan.survivants += 1))), /bilan/);
assert.throws(() => relire(modifie((lu) => (lu.parFichier[0].tues += 1))), /par fichier/);
assert.throws(() => relire(modifie((lu) => (lu.parOperateur[0].tues += 1))), /par opérateur/);
});
test('relire refuse un survivant que la série longue tue, laissé parmi les survivants', () => {
// L'ancien compte : le mutant tué par la série longue rangé avec les
// survivants, bilan, fichier et opérateur ajustés pour que seuls les
// listes et la règle du compte le trahissent.
const ancien = modifie((lu) => {
lu.survivants.splice(1, 0, ...lu.tuesSerieLongue.splice(0));
for (const compte of [lu.bilan, lu.parFichier[0], lu.parOperateur[0]]) {
compte.survivants += 1;
compte.tuesSerieLongue -= 1;
}
});
assert.throws(() => relire(ancien), /série longue/);
});
});
describe('rapport de mutation : les équivalents déclarés', () => {
const texte = (liste) => `${JSON.stringify(liste, null, 2)}\n`;
const CLE = 'src/verger/pommes.js:si-vrai:00000000000000aa:0';
test('equivalentsDe rend chaque clé avec sa raison, dans l’ordre du fichier', () => {
const lus = equivalentsDe(texte([{ cle: CLE, raison: 'toujours vrai ici' }, { cle: `${CLE}1`, raison: 'idem' }]));
assert.deepEqual([...lus], [
[CLE, 'toujours vrai ici'],
[`${CLE}1`, 'idem'],
]);
assert.deepEqual([...equivalentsDe('[]\n')], []);
});
test('equivalentsDe refuse un texte illisible, une autre forme, une raison vide, une clé en double, un chemin de compte', () => {
assert.throws(() => equivalentsDe('['), /illisible/);
assert.throws(() => equivalentsDe('{}'), /liste de \{ cle, raison \}/);
assert.throws(() => equivalentsDe(texte([{ cle: CLE }])), new RegExp(`raison.*${CLE}`));
assert.throws(() => equivalentsDe(texte([{ cle: CLE, raison: '' }])), new RegExp(`raison.*${CLE}`));
assert.throws(() => equivalentsDe(texte([{ cle: '', raison: 'x' }])), /clé/);
assert.throws(() => equivalentsDe(texte([null])), /liste de \{ cle, raison \}/);
assert.throws(
() => equivalentsDe(texte([{ cle: CLE, raison: 'a' }, { cle: CLE, raison: 'b' }])),
new RegExp(`en double.*${CLE}`),
);
assert.throws(() => equivalentsDe(texte([{ cle: CLE, raison: 'lu dans /home/exemple/notes' }])), /ne nomme pas sa machine/);
});
});
// Les fautes des trois fichiers d'audit/, lus comme du texte : un JSON que
// relire accepte, au périmètre du moteur seul ; le Markdown que sa
// composition rejoue ; les équivalents déclarés, exactement ceux que le JSON
// range à part, chacun avec la même raison. Une liste vide quand tout
// s'accorde.
function fautesAudit({ markdown, json, equivalents }) {
let relu;
try {
relu = relire(json);
} catch (erreur) {
return [`mutation-moteur.json : ${erreur.message}`];
}
const fautes = [];
const { perimetre } = relu.donnees.entete;
if (perimetre.length !== 1 || perimetre[0] !== 'src/moteur') fautes.push(`périmètre ${perimetre.join(', ')} : src/moteur seul attendu`);
if (markdown !== relu.markdown) fautes.push('mutation-moteur.md : autre que le Markdown que rediger rejoue');
let declares;
try {
declares = equivalentsDe(equivalents);
} catch (erreur) {
return [...fautes, `mutants-equivalents.json : ${erreur.message}`];
}
const ranges = new Map(relu.donnees.equivalents.map(({ cle, raison }) => [cle, raison]));
for (const [cle, raison] of declares) {
if (!ranges.has(cle)) fautes.push(`mutants-equivalents.json : clé inconnue du JSON, ${cle}`);
else if (ranges.get(cle) !== raison) fautes.push(`mutants-equivalents.json : raison autre que celle du JSON, ${cle}`);
}
for (const cle of ranges.keys()) {
if (!declares.has(cle)) fautes.push(`mutation-moteur.json : équivalent non déclaré, ${cle}`);
}
return fautes;
}
describe('rapport de mutation : audit/ committé (§ 14.13)', () => {
const lire = (fichier) => readFileSync(join(RACINE, 'audit', fichier), 'utf8');
const COMMITTE = Object.freeze({
markdown: lire('mutation-moteur.md'),
json: lire('mutation-moteur.json'),
equivalents: lire('mutants-equivalents.json'),
});
test('audit/ committé : l’en-tête passe verifierEntete, le Markdown est celui que rediger rejoue, chaque équivalent a une clé unique et une raison non vide, connue du JSON', () => {
assert.deepEqual(fautesAudit(COMMITTE), []);
assert.deepEqual(relire(COMMITTE.json).donnees.entete.perimetre, ['src/moteur']);
assert.ok(equivalentsDe(COMMITTE.equivalents).size > 0, 'des équivalents déclarés');
});
test('la garde relève, en mémoire, une raison vidée, une clé inconnue, un Markdown retouché, un périmètre élargi', () => {
const liste = JSON.parse(COMMITTE.equivalents);
const premiere = liste[0].cle;
const videe = `${JSON.stringify([{ ...liste[0], raison: '' }, ...liste.slice(1)], null, 2)}\n`;
assert.deepEqual(fautesAudit({ ...COMMITTE, equivalents: videe }), [
`mutants-equivalents.json : liste de { cle, raison }, raison vide pour ${premiere}`,
]);
const inconnue = 'src/moteur/inventee.js:frontiere:0123456789abcdef:0';
const ajoutee = `${JSON.stringify([...liste, { cle: inconnue, raison: 'une clé que nul audit n’a rendue' }], null, 2)}\n`;
assert.deepEqual(fautesAudit({ ...COMMITTE, equivalents: ajoutee }), [`mutants-equivalents.json : clé inconnue du JSON, ${inconnue}`]);
assert.deepEqual(fautesAudit({ ...COMMITTE, markdown: COMMITTE.markdown.replace('## Bilan', '## Bilan retouché') }), [
'mutation-moteur.md : autre que le Markdown que rediger rejoue',
]);
const elargi = COMMITTE.json.replace('"src/moteur"\n', '"src/moteur",\n "src/application"\n');
assert.notEqual(elargi, COMMITTE.json);
assert.deepEqual(fautesAudit({ ...COMMITTE, json: elargi }), [
'périmètre src/moteur, src/application : src/moteur seul attendu',
'mutation-moteur.md : autre que le Markdown que rediger rejoue',
]);
});
});