[IMP] tooling: late off-loopback requests, version gap shown in tests

A request leaving the loopback after the last browser capture went
unnoticed: the walkthrough now checks requests at its closing too, and a
test that both emits one and gets a collector error names the request.
The short cycle prints the gap between the manifest version and the build
in the node series, without failing, as the spec asks; a test proves the
line reaches the console. The documentation script detects a direct run
through realpath, and the bench reading reruns the README region test.
Checked: node 3903, browser 770, each fix red under its mutant.

--- FR ---

[IMP] outillage : requêtes hors boucle tardives, écart de version montré

Une requête sortie de la boucle locale après la dernière capture passait
inaperçue : le parcours relève les requêtes à sa clôture aussi, et une
épreuve qui en émet une avec une erreur du collecteur nomme la requête.
Le cycle court affiche dans la série node l'écart entre la version du
manifeste et la construction, sans échouer, comme le veut la spec ; une
épreuve prouve que la ligne atteint la console. Le script de documentation
reconnaît l'appel direct par realpath, et le relevé du banc relance
l'épreuve de la région du README.
Vérifié : node 3903, navigateur 770, chaque correction rouge sous son mutant.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-10 20:17:25 -04:00
parent 9a2c8c20f3
commit a50d3278f1
6 changed files with 266 additions and 29 deletions

View file

@ -51,14 +51,15 @@
// porteur : un document privé de ses images ne passe pas sur un balayage
// vide.
//
// Ligne de commande, depuis la racine du projet ou d'ailleurs :
// Ligne de commande, depuis la racine du projet ou d'ailleurs, par un
// chemin qui traverse ou non un lien symbolique :
// node scripts/documentation.js engendrer | verifier [--livraison]
// [--racine <dossier>] [--porteur <chemin>]…
// --racine remplace la racine du projet ; chaque --porteur, relatif à la
// racine, remplace la liste PORTEURS. Code de sortie : 0 sans faute, 1 sur
// une faute ou, sous --livraison, un écart de version, 2 sur des arguments
// illisibles.
import { readFileSync, writeFileSync } from 'node:fs';
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { COMMANDES } from '../src/application/commandes.js';
@ -934,6 +935,23 @@ export function engendrerArbre({
return { fautes, ecrits: aEcrire.map(([chemin]) => chemin), code: 0 };
}
/**
* La ligne qui nomme un écart de version (§ 19.7) : sa portée — bloquant
* sous livraison, non bloquant au cycle court —, la version du manifeste,
* celle de la construction. La ligne de commande l'imprime, et la série node
* l'affiche au cycle court (test/documentation.test.js).
*
* @param {null|{manifeste: string|null, construction: string}} ecart celui de verifierArbre
* @param {{livraison?: boolean}} [options]
* @returns {string|null} null sans écart
*/
export function ligneEcart(ecart, { livraison = false } = {}) {
if (ecart === null) return null;
const portee = livraison ? 'bloquant' : 'non bloquant';
const manifeste = ecart.manifeste ?? 'aucune (manifeste absent ou sans capture)';
return `écart de version (${portee}) : manifeste ${manifeste}, construction ${ecart.construction}`;
}
const USAGE =
'usage : node scripts/documentation.js engendrer | verifier [--livraison] [--racine <dossier>] [--porteur <chemin>]…';
@ -992,15 +1010,29 @@ export function executer(argv, { racine = RACINE, porteurs = PORTEURS, version,
...(lireOctets === undefined ? {} : { lireOctets }),
});
ecrireFautes(bilan.fautes);
if (bilan.ecart !== null) {
const portee = lus.livraison ? 'bloquant' : 'non bloquant';
const manifeste = bilan.ecart.manifeste ?? 'aucune (manifeste absent ou sans capture)';
sortie(`écart de version (${portee}) : manifeste ${manifeste}, construction ${bilan.ecart.construction}`);
}
const ecart = ligneEcart(bilan.ecart, { livraison: lus.livraison });
if (ecart !== null) sortie(ecart);
sortie(`verifier : ${options.porteurs.length} porteur(s), ${bilan.regions} région(s), ${bilan.images} image(s), ${bilan.fautes.length} faute(s)`);
return bilan.code;
}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
/**
* Vrai quand chemin désigne ce module : Node charge le script lancé sous son
* chemin réel, et un chemin qui traverse un lien symbolique se compare donc
* par realpathSync. Un chemin absent ou illisible rend faux.
*
* @param {string|undefined} [chemin] le script que Node a lancé
* @returns {boolean}
*/
export function estLanceDirectement(chemin = process.argv[1]) {
if (chemin === undefined) return false;
try {
return realpathSync(chemin) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (estLanceDirectement()) {
process.exitCode = executer(process.argv.slice(2));
}

View file

@ -9,7 +9,7 @@
// de 1 366 × 700. Un seul montage porte toutes les épreuves du fichier, qui
// s'exécutent dans l'ordre : chacune part de l'événement que la précédente a
// laissé, affirme sa condition avant la suivante, et finit sur un collecteur
// d'erreurs vide.
// d'erreurs vide et sans requête hors de la boucle locale (§ 19.11).
//
// Le parcours est le parcours photographié (§ 19.1, § 19.3) : il suit les
// captures du § 19.3 que l'itération photographie — 01, 01b, 02, 02b, 03 à
@ -81,6 +81,7 @@ import {
entiereDansLaFenetre,
modeFourni,
photographier,
requeteHorsBoucle,
} from '../../test/navigateur/captures.js';
import { NOM_CAS_LIMITES, attendre, centre, octetsCasLimites } from '../../test/navigateur/ecrans.js';
import { fixerCadre, pointer, touche } from '../../test/navigateur/entrees.js';
@ -180,8 +181,21 @@ beforeEach(({ task, onTestFailed }) => {
expect(new TextEncoder().encode(titre).length, titre).toBeLessThanOrEqual(TITRE_MAX_OCTETS);
});
afterEach(() => {
expect(m.erreurs(), 'erreurs reçues par le collecteur').toEqual([]);
// Chaque épreuve, photographiée ou non, finit sans erreur au collecteur et
// sans requête hors de la boucle locale : celles d'après la dernière capture
// d'écran — les pages du PDF, les sorties, l'ordre de lecture, les fenêtres,
// la lecture et le plan bloqué — se relèvent ici. Les deux causes se
// lisent avant d'échouer et se nomment ensemble, la requête d'abord : une
// requête se nomme dans l'épreuve qui l'a émise, même quand le collecteur a
// aussi reçu une erreur.
afterEach(async () => {
const requete = await requeteHorsBoucle();
const erreurs = m.erreurs();
const fautes = [
...(requete === null ? [] : [`requête hors de la boucle locale : ${requete}`]),
...(erreurs.length === 0 ? [] : [`erreurs reçues par le collecteur : ${JSON.stringify(erreurs)}`]),
];
if (fautes.length > 0) expect.fail(fautes.join(' ; '));
});
afterAll(async () => {
@ -680,7 +694,8 @@ function parPointsDeCode(a, b) {
/**
* Photographie une page du PDF de la planche (§ 19.6) : tramerPdf la lit
* d'abord sans rien écrire, conditions(lue) affirme ce qu'elle montre, puis,
* d'abord sans rien écrire, conditions(lue) affirme ce qu'elle montre, aucune
* requête hors de la boucle locale n'attend d'être nommée (§ 19.11), puis,
* en écriture, la trame en PNG sous le dossier d'attente et dépose l'entrée ;
* en vérification, compare l'entrée au manifeste. L'entrée porte la source
* pdf, la taille de la page à 96 ppp pour cadre, la couche de texte de la
@ -700,6 +715,8 @@ async function photographierPdf(octets, { page, fichier }, conditions) {
expect(lu.pdf).toBe(await empreinte(octets));
const [lue] = lu.pages;
conditions(lue);
const requete = await requeteHorsBoucle();
if (requete !== null) expect.fail(`capture ${fichier} : requête hors de la boucle locale : ${requete}`);
const racineNeutre = etat().racine.chemin;
expect(await commands.cheminsHorsRacine({ texte: lue.textes, racine: racineNeutre }), fichier).toEqual([]);
const entree = {

View file

@ -62,6 +62,7 @@ import {
rgb,
cloreCaptures,
photographier,
requeteHorsBoucle,
situer,
stylesDuTheme,
texteReleve,
@ -1183,6 +1184,12 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu
return echecs;
}
// Laisse passer autant d'images de la page qu'echecsReleves en attend au
// plus, sans lire aucun relevé : l'échec d'une requête a rejoint node.
async function laisserArriverLEchec() {
for (let image = 0; image < 60; image += 1) await new Promise((resoudre) => requestAnimationFrame(resoudre));
}
// Le mandataire mort de test/chromium.js refuse la connexion : la requête
// n'est jamais partie de la machine, et Chromium le dit par sa cause.
test('réseau : une requête vers une adresse littérale inventée échoue en connexion au mandataire mort, la page de la boucle locale se charge, le relevé la nomme', async () => {
@ -1195,6 +1202,16 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu
expect(await commands.requetesHorsBoucle()).toEqual([ADRESSE_INVENTEE]);
});
test('relevé : requeteHorsBoucle nomme la requête suivie de sa cause une fois l’échec reçu, vide les deux relevés, puis rend null', async () => {
await commands.requetesHorsBoucle();
await commands.echecsHorsBoucle();
await requeteInventee();
await laisserArriverLEchec();
expect(await requeteHorsBoucle()).toBe(`${ADRESSE_INVENTEE} (net::ERR_PROXY_CONNECTION_FAILED)`);
expect(await commands.echecsHorsBoucle()).toEqual([]);
expect(await requeteHorsBoucle()).toBe(null);
});
test('un sélecteur manquant, une erreur au collecteur, une requête hors boucle : chacun échoue en nommant la capture', async () => {
await avecMontage(async (m, { ecrire }) => {
await expect(photographier(m, '01-premier-ecran', { ...ecrire, condition: '[data-absent]' })).rejects.toThrow(
@ -1458,13 +1475,26 @@ describe('les captures d’écran : chaque contrôle échoue en nommant la captu
});
});
test('clôture : une épreuve du parcours en échec ne publie rien, jette l’attente et se nomme ; sans échec, la clôture publie', async () => {
test('clôture : une épreuve en échec ou une requête hors boucle ne publie rien, jette l’attente et se nomme ; sinon, la clôture publie', async () => {
await avecMontage(async (m, { racine, racineNeutre, ecrire }) => {
const declarees = ['01-premier-ecran'];
const clore = (echecs) => cloreCaptures({ echecs, declarees, mode: 'ecrire', racine, racineNeutre });
await photographier(m, '01-premier-ecran', ecrire);
await expect(clore(['20 « Défaire » en lecture'])).rejects.toThrow('captures non publiées : 1 épreuve(s) du parcours en échec : « 20 « Défaire » en lecture »');
expect(await commands.empreintesDossier({ racine })).toEqual([]);
// Une requête émise après la dernière capture : la clôture seule la lit.
await photographier(m, '01-premier-ecran', ecrire);
await requeteInventee();
await echecsReleves();
await expect(clore([])).rejects.toThrow(`captures non publiées : requête hors de la boucle locale : ${ADRESSE_INVENTEE}`);
expect(await commands.empreintesDossier({ racine })).toEqual([]);
await photographier(m, '01-premier-ecran', ecrire);
await requeteInventee();
await echecsReleves();
await expect(clore(['20 « Défaire » en lecture'])).rejects.toThrow(
`captures non publiées : 1 épreuve(s) du parcours en échec : « 20 « Défaire » en lecture » ; requête hors de la boucle locale : ${ADRESSE_INVENTEE}`,
);
expect(await commands.empreintesDossier({ racine })).toEqual([]);
await photographier(m, '01-premier-ecran', ecrire);
expect(await clore([])).toEqual({ declarees, bilan: { ecrites: declarees, gardees: [], retirees: [], manifeste: true } });
});

View file

@ -18,9 +18,13 @@
// (une image retirée, un nombre changé, un nom de fonction collé), elle en
// rend une, ce qui montre qu'elle ne passe pas à vide (§ 14.2).
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, relative } from 'node:path';
import { describe, test } from './lanceur.js';
import { versionVoisine } from './version_voisine.js';
import configuration from '../vitest.config.js';
import { TABLES_APPLICATION } from '../src/application/libelles.js';
import { creerTraduction } from '../src/application/traduction.js';
import { CATALOGUE } from '../src/demo/catalogue.js';
@ -32,6 +36,7 @@ import { VERSION } from '../src/version.genere.js';
import { CONSTANTES } from '../src/geometrie/constantes.js';
import { tableauConstantes } from '../scripts/banc/releve.js';
import {
CHEMIN_RELEVE,
PORTEURS,
PRODUCTEURS,
RACINE,
@ -39,6 +44,7 @@ import {
executer,
fautesImagesCitees,
imagesCitees,
ligneEcart,
lireRegions,
tableauRegistre,
verifierArbre,
@ -401,8 +407,12 @@ describe('documentation : les régions engendrées des documents livrés (§ 19.
assert.deepEqual(servies, PRODUCTEURS.map(({ nom }) => nom).sort());
});
test('verifier, sur l’arbre réel et la liste par défaut : aucune faute, toutes les régions lues', () => {
const bilan = verifierArbre();
test('verifier, sur l’arbre réel et la liste par défaut : aucune faute, toutes les régions lues ; l’écart de version affiché, non bloquant', () => {
// L'écrivain relève chaque ligne et la passe à la sortie d'erreur de
// l'épreuve : la série surveillée affiche l'écart de l'arbre réel.
const lignes = [];
const { bilan } = verifierEtAfficher({ ecrire: (ligne) => { lignes.push(ligne); console.warn(ligne); } });
assert.deepEqual(lignes, bilan.ecart === null ? [] : [ligneEcart(bilan.ecart)]);
assert.deepEqual(bilan.fautes, []);
assert.equal(bilan.regions, PRODUCTEURS.length);
assert.equal(bilan.code, 0);
@ -435,6 +445,123 @@ describe('documentation : les régions engendrées des documents livrés (§ 19.
});
});
// --- L'écart de version, la ligne de commande, la surveillance (§ 19.7, § 14.4) ---------
// Vérifie l'arbre réel au cycle court, par lire quand il est donné, et
// affiche l'écart de version du bilan par ecrire : par défaut console.warn,
// la sortie d'erreur de l'épreuve, que le rapporteur imprime sous le nom de
// l'épreuve sans la faire échouer (§ 19.7). L'épreuve du manifeste voisin
// éprouve les deux écrivains, l'injecté et celui par défaut ; celle de
// l'arbre réel passe un écrivain qui relève puis transmet à console.warn,
// et attend la ligne de son propre écart, aucune sans écart.
// Rend { bilan, ligne } : ligne est la ligne écrite, null sans écart.
function verifierEtAfficher({ lire, ecrire = (ligne) => console.warn(ligne) } = {}) {
const bilan = verifierArbre({ lire });
const ligne = ligneEcart(bilan.ecart);
if (ligne !== null) ecrire(ligne);
return { bilan, ligne };
}
// Les épreuves que la série surveillée relance quand chemin change : la
// réunion des motifs de vitest.config.js qui y répondent, comme Vitest la
// calcule.
function relanceesPar(chemin) {
const motifs = configuration.test.watchTriggerPatterns.filter(({ pattern }) => new RegExp(pattern.source, pattern.flags).test(chemin));
return new Set(motifs.flatMap(({ testsToRun }) => [testsToRun(chemin) ?? []].flat()));
}
describe('documentation : l’écart de version, la ligne de commande, la surveillance (§ 19.7, § 14.4)', () => {
// Le manifeste réel, chaque version réelle passée à la voisine de la
// construction : seules les versions diffèrent, images et empreintes
// restent celles de l'arbre.
const versionDuManifeste = (version) => {
const manifeste = lireManifeste();
manifeste.captures = manifeste.captures.map((capture) => ({ ...capture, versionReelle: version }));
return lireAvec(CHEMIN_MANIFESTE, `${JSON.stringify(manifeste, null, 2)}\n`);
};
test('rouge : un manifeste à une version voisine — au cycle court, l’écart s’affiche, se nomme et ne bloque pas ; à la livraison, il bloque', () => {
const voisine = versionVoisine(VERSION.affichee);
const lire = versionDuManifeste(voisine);
const lignes = [];
const { bilan: court, ligne } = verifierEtAfficher({ lire, ecrire: (ecrite) => lignes.push(ecrite) });
assert.deepEqual([court.fautes, court.code], [[], 0]);
assert.deepEqual(court.ecart, { manifeste: voisine, construction: VERSION.affichee });
assert.deepEqual(lignes, [`écart de version (non bloquant) : manifeste ${voisine}, construction ${VERSION.affichee}`]);
assert.equal(ligne, lignes[0]);
// Sans écrivain, la ligne va à console.warn, remplacé le temps de l'appel.
const recues = [];
const avertir = console.warn;
console.warn = (...valeurs) => recues.push(valeurs.join(' '));
try {
verifierEtAfficher({ lire });
} finally {
console.warn = avertir;
}
assert.deepEqual(recues, lignes);
const livraison = verifierArbre({ lire, livraison: true });
assert.equal(livraison.code, 1);
assert.equal(ligneEcart(livraison.ecart, { livraison: true }), `écart de version (bloquant) : manifeste ${voisine}, construction ${VERSION.affichee}`);
// La ligne de commande imprime la même ligne, par la même composition.
for (const [argv, code, portee] of [[['verifier'], 0, 'non bloquant'], [['verifier', '--livraison'], 1, 'bloquant']]) {
const sortie = [];
assert.equal(executer(argv, { lire, sortie: (ligne) => sortie.push(ligne) }), code);
assert.ok(sortie.includes(`écart de version (${portee}) : manifeste ${voisine}, construction ${VERSION.affichee}`), sortie.join('\n'));
}
// Sans écart, rien ne s'affiche.
const accorde = verifierEtAfficher({ lire: versionDuManifeste(VERSION.affichee), ecrire: () => assert.fail('aucune ligne sans écart') });
assert.equal(accorde.bilan.ecart, null);
assert.equal(accorde.ligne, null);
});
test('un manifeste absent ne prouve aucune version : l’écart le nomme', () => {
assert.equal(
ligneEcart({ manifeste: null, construction: VERSION.affichee }),
`écart de version (non bloquant) : manifeste aucune (manifeste absent ou sans capture), construction ${VERSION.affichee}`,
);
assert.equal(ligneEcart(null, { livraison: true }), null);
});
// Node charge le script lancé sous son chemin réel : un lancement par un
// chemin qui traverse un lien symbolique est un lancement direct.
test('rouge : lancé par un lien symbolique vers son dossier, le script s’exécute — un argument illisible rend l’usage et le code 2', () => {
const dossier = mkdtempSync(join(tmpdir(), 'gtt-documentation-lien-'));
try {
const lien = join(dossier, 'scripts');
symlinkSync(join(RACINE, 'scripts'), lien, 'junction');
const lance = spawnSync(process.execPath, [join(lien, 'documentation.js'), 'illisible'], { encoding: 'utf8' });
assert.equal(lance.status, 2, lance.stderr);
assert.match(lance.stdout, /^usage : node scripts\/documentation\.js engendrer \| verifier/);
} finally {
rmSync(dossier, { recursive: true, force: true });
}
});
// verifier lit par le disque les porteurs, le manifeste, le relevé du banc
// et les captures citées, qu'aucun graphe d'imports ne relie aux épreuves
// de la documentation : chacun a son motif de surveillance. Les lectures
// se relèvent sur l'arbre réel, avant l'appel à chaque lecteur. Les textes
// relancent les deux épreuves ; une capture relance celle de l'arbre réel,
// scripts/documentation.test.js ne lisant que ses propres images.
test('rouge : chaque fichier que verifier lit par le disque relance les épreuves de la documentation qui le lisent', () => {
const lus = new Set();
const relever = (lecteur) => (chemin) => {
lus.add(chemin);
return lecteur(chemin);
};
const bilan = verifierArbre({ lire: relever(lire), lireOctets: relever(lireOctets) });
const images = [...lus].filter((chemin) => chemin.endsWith('.png'));
assert.ok(bilan.images > 0, 'aucune capture citée');
assert.equal(images.length, bilan.images);
assert.deepEqual([...PORTEURS, CHEMIN_MANIFESTE, CHEMIN_RELEVE].filter((chemin) => !lus.has(chemin)), []);
const epreuvesDe = (chemin) =>
chemin.endsWith('.png') ? ['test/documentation.test.js'] : ['test/documentation.test.js', 'scripts/documentation.test.js'];
const manquent = [...lus].flatMap((chemin) =>
epreuvesDe(chemin).filter((e) => !relanceesPar(chemin).has(e)).map((e) => `${chemin} ne relance pas ${e}`));
assert.deepEqual(manquent, []);
});
});
// --- La région des constantes et le relevé du banc (§ 19.8, § 19.10) ----------------
describe('documentation : la région « constantes » lit le relevé du banc quand il existe (§ 19.10)', () => {

View file

@ -647,6 +647,24 @@ function conditionVisible(element) {
return entiereDansLaFenetre(element) ? true : 'sa boîte déborde de la fenêtre ou d’un ancêtre qui la rogne : l’image la coupe';
}
/**
* La première requête hors de la boucle locale que la page a émise depuis
* le relevé précédent (§ 2, § 19.11), suivie de la cause de son échec entre
* parenthèses quand node l'a déjà reçue ; null sans requête. Vide les deux
* relevés de test/chromium.js, requêtes et échecs : chaque requête se
* nomme une fois, au premier contrôle qui la lit. Photographier, une page
* du PDF, la fin de chaque épreuve du parcours et sa clôture la lisent.
*
* @returns {Promise<string|null>}
*/
export async function requeteHorsBoucle() {
const [adresse] = await commands.requetesHorsBoucle();
const echecs = await commands.echecsHorsBoucle();
if (adresse === undefined) return null;
const cause = echecs.find((echec) => echec.adresse === adresse)?.cause ?? null;
return cause === null ? adresse : `${adresse} (${cause})`;
}
// Ce que porte le userAgent de Chromium sans tête, le seul mode d'affichage
// des captures, MODE_AFFICHAGE au manifeste (§ 19.4) : les métriques de
// texte diffèrent d'un mode à l'autre.
@ -719,8 +737,8 @@ export async function photographier(m, fichier, { condition = [], graine = null,
}
const [erreur] = m.erreurs();
if (erreur !== undefined) throw echec(`collecteur : ${erreur.type} « ${erreur.message} »`);
const [requete] = await commands.requetesHorsBoucle();
if (requete !== undefined) throw echec(`requête hors de la boucle locale : ${requete}`);
const requete = await requeteHorsBoucle();
if (requete !== null) throw echec(`requête hors de la boucle locale : ${requete}`);
if (innerWidth !== CADRE.largeur || innerHeight !== CADRE.hauteur) {
throw echec(`cadre ${innerWidth} × ${innerHeight}, attendu ${CADRE.largeur} × ${CADRE.hauteur}`);
}
@ -794,13 +812,14 @@ export async function photographier(m, fichier, { condition = [], graine = null,
}
/**
* Clôt un parcours photographié (§ 19.1, § 19.5, § 19.7). declarees : les
* captures que le parcours porte, CAPTURES hors A_VENIR par défaut. Une
* épreuve du fichier en échec, qu'elle ait photographié ou non, refuse la
* clôture en se nommant : en écriture, le dossier d'attente est jeté et
* rien n'est publié. Sans échec, l'écriture publie d'un bloc et rend le
* bilan de publierCaptures ; la vérification affirme que le manifeste porte
* exactement les déclarées.
* Clôt un parcours photographié (§ 19.1, § 19.5, § 19.7, § 19.11).
* declarees : les captures que le parcours porte, CAPTURES hors A_VENIR par
* défaut. Une épreuve du fichier en échec, qu'elle ait photographié ou non,
* refuse la clôture en se nommant ; une requête hors de la boucle locale
* émise depuis le dernier relevé la refuse en se nommant aussi : en
* écriture, le dossier d'attente est jeté et rien n'est publié. Sans
* échec, l'écriture publie d'un bloc et rend le bilan de publierCaptures ;
* la vérification affirme que le manifeste porte exactement les déclarées.
*
* @param {Object} options
* @param {string[]} options.echecs les épreuves du fichier qui ont échoué
@ -817,10 +836,13 @@ export async function cloreCaptures({ echecs, declarees = null, mode = modeFourn
liste = captures.map(({ fichier }) => fichier).filter((fichier) => !aVenir.includes(fichier));
}
if (liste.length === 0) throw new Error('captures : aucune capture déclarée — zéro capture n’est jamais un succès');
if (echecs.length > 0) {
const requete = await requeteHorsBoucle();
const causes = [];
if (echecs.length > 0) causes.push(`${echecs.length} épreuve(s) du parcours en échec : ${echecs.map((nom) => `« ${nom} »`).join(', ')}`);
if (requete !== null) causes.push(`requête hors de la boucle locale : ${requete}`);
if (causes.length > 0) {
if (mode === 'ecrire') await commands.viderAttente({ racine });
const natures = echecs.map((nom) => `« ${nom} »`).join(', ');
throw new Error(`captures non ${mode === 'ecrire' ? 'publiées' : 'vérifiées'} : ${echecs.length} épreuve(s) du parcours en échec : ${natures}`);
throw new Error(`captures non ${mode === 'ecrire' ? 'publiées' : 'vérifiées'} : ${causes.join(' ; ')}`);
}
const memes = (a, b) => [...a].sort().join('\n') === [...b].sort().join('\n');
if (mode === 'ecrire') {

View file

@ -55,7 +55,8 @@ const PROCESSUS = { pool: SERIES.pool, maxWorkers: SERIES.maxWorkers };
// composants dans son motif, et son graphe porte les commandes des captures
// d'écran, qui balaient doc/captures ; la garde des lecteurs par le disque
// (test/arborescence.test.js), textuelle, les compte parmi les lecteurs des
// composants.
// composants. test/documentation.test.js en est : elle lit ces motifs pour
// les fichiers que scripts/documentation.js lit par le disque.
const RELANCES_D_UN_COMPOSANT = Object.freeze([
'test/arborescence.test.js',
'test/sources_interface.test.js',
@ -69,6 +70,7 @@ const RELANCES_D_UN_COMPOSANT = Object.freeze([
'test/chromium.test.js',
'test/machine.test.js',
'test/rapporteur.test.js',
'test/documentation.test.js',
]);
// Les épreuves node qui lisent la feuille des jetons par le système de
@ -174,6 +176,13 @@ export default defineConfig({
// captures, que scripts/documentation.js lit par le disque (§ 19.8).
{ pattern: /(^|\/)(README\.md|GUIDE-[^/]*\.md|doc\/captures\/manifeste\.json)$/,
testsToRun: () => 'scripts/documentation.test.js' },
// Le relevé du banc, que scripts/documentation.js lit par le disque
// pour la région « constantes » (§ 19.10) : les deux épreuves de la
// documentation, et celles qui l'importent, que le motif ne relancerait
// plus sinon.
{ pattern: /(^|\/)scripts\/banc\/releve\.json$/,
testsToRun: () => ['test/documentation.test.js', 'scripts/documentation.test.js', 'src/geometrie/constantes.test.js',
'scripts/banc/releve.test.js', 'scripts/banc/lancer.test.js'] },
],
projects: [
{ test: {