gestion_table_tournante_libre/scripts/banc/lancer.test.js
Mathieu Benoit ff31a6e1ca [ADD] engine for quality: A_max, facilitator diversity, run trace, pairs
The quality page needs figures the engine did not yet produce. It now
bounds each person's distinct memberships (A_max) and checks it with
the indicator guard; reads facilitator diversity over the evening and
per round; records the search's moves and record points; lists repeated
and imposed pairs; and lays out charts through one geometry module with
two bench thresholds. Chart colour tokens carry their own validation.
Default plans still equal the reference fingerprints; nothing new is
written to the file yet.
Checked: node 4768, navigateur 967, node-long 128; new modules at 100 %.

--- FR ---

[ADD] moteur de la qualité : A_max, diversité, course, paires

La page de qualité demande des chiffres que le moteur ne produisait pas.
Il borne désormais les appartenances distinctes de chacun (A_max) et la
garde des indicateurs la vérifie ; il lit la diversité des animateurs sur
la soirée et par tour ; il relève les mouvements et les points de la
recherche ; il liste les paires répétées et imposées ; un seul module de
géométrie dispose les graphiques, avec deux seuils au banc. Les jetons
des graphiques portent leur propre validation. Les plans par défaut
égalent les empreintes ; rien de neuf ne s'écrit encore au fichier.
Vérifié : node 4768, navigateur 967, longues 128 ; nouveaux modules à 100 %.

Assisted-by: Claude Opus 5.5
2026-10-12 04:29:27 -04:00

400 lines
18 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)
// Épreuves de l'orchestrateur du banc (§ 19.10, § 14.1, § 19.8), sous node
// et sans navigateur : l'assemblage du relevé, qui refuse une mesure
// manquante et range les mesures dans l'ordre du registre ; la
// configuration, liste fermée du contrat, qui ne nomme pas la machine ;
// le jugement qui rend la sortie non nulle ; la région des constantes ; le
// déroulé de executer, sur des effets factices. Les machines, les versions
// et les valeurs sont inventées ; le relevé committé n'est lu que pour sa
// conformité.
import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js';
import { CONSTANTES } from '../../src/geometrie/constantes.js';
import { projetBanc } from './projet.js';
import { accordRegistre, verifierReleve } from './releve.js';
import releveCommite from './releve.json' with { type: 'json' };
import {
ATTENDUES,
CHEMIN_RELEVE,
FAMILLE_EMBARQUEE,
GARDES,
SORTIES_NAVIGATEUR,
assembler,
configurationDuBanc,
executer,
jugement,
regionConstantes,
} from './lancer.js';
// Un système d'exploitation inventé, au nom d'hôte, au dossier personnel, à
// l'utilisateur et au modèle de processeur reconnaissables : aucun ne doit
// atteindre le relevé.
const HOTE_INVENTE = 'poste-quenouille-7';
const UTILISATEUR_INVENTE = 'quenouille';
const DOSSIER_INVENTE = '/home/quenouille';
const MODELE_INVENTE = 'Processeur Quenouille 6 cœurs';
const systemeInvente = () => ({
hostname: () => HOTE_INVENTE,
homedir: () => DOSSIER_INVENTE,
userInfo: () => ({ username: UTILISATEUR_INVENTE, homedir: DOSSIER_INVENTE }),
type: () => 'Linux',
arch: () => 'x64',
cpus: () => Array.from({ length: 6 }, () => ({ model: MODELE_INVENTE })),
availableParallelism: () => 6,
});
const configuration = () => configurationDuBanc({ systeme: systemeInvente(), node: 'v26.10.0', navigateur: 'Chromium 140.0' });
// Les sorties d'un passage du banc, de valeurs inventées : une mesure par
// constante du banc et par garde, rangées dans un ordre qui n'est pas celui
// du registre. Comme celles du navigateur, les mesures de texte et la garde
// du temps par image portent leur ralentissement ; celles du parallèle n'en
// portent pas, et leur sortie le dit dans sa configuration.
const sorties = () =>
new Map([
['plafond', { plafondAPriori: [{ cle: 'grande', N: 260, T: 33, R: 4, msPlafonds: 0.07, msDiagnostic: 0.9 }] }],
[
'parallele',
{
format: 1,
configuration: {
cadre: { largeur: 1366, hauteur: 700 },
facteurEchelle: 1,
navigateur: null,
node: null,
processeursLogiques: 6,
ralentissementCpu: 1,
police: null,
},
mesures: [
{ nom: 'RALENTISSEMENT_PLEINE_CHARGE', unite: '×', valeurProposee: 2.5, critere: 't(L) / t(1)', courbe: [] },
{ nom: 'DUREE_GENERATION_PAR_DEFAUT', unite: 'ms', valeurProposee: 600, critere: 'médiane de 3 générations' },
],
parallele: [{ coeurs: 1, ralentissement: 1 }],
},
],
[
'temps_image',
{
format: 1,
mesures: [{ nom: 'TEMPS_IMAGE_LISTES', unite: 'ms', valeurProposee: null, critere: 'p95 sous 33 ms', courbe: [], verdict: 'tenu', ralentissementCpu: 4 }],
},
],
[
'temoin',
{
format: 1,
mesures: [{ nom: 'TOLERANCE_CHAINE_TEMOIN', unite: 'rapport', valeurProposee: 0.02, critere: 'quart du repli', police: 'Noto Sans', ralentissementCpu: 1 }],
},
],
[
'qualite',
{
format: 1,
mesures: [
{ nom: 'LARGEUR_TRONCATURE_NOM_PX', unite: 'px', valeurProposee: 190, critere: 'nom de référence', police: 'Noto Sans', ralentissementCpu: 1 },
{ nom: 'SEUIL_MATRICE_PARTICIPANTS', unite: 'participants', valeurProposee: 38, critere: 'case de 24 px', ralentissementCpu: 1 },
],
},
],
[
'sieges_lisibles',
{
format: 1,
mesures: [{ nom: 'SEUIL_SIEGES_LISIBLES', unite: 'places', valeurProposee: 17, critere: 'liste dans le plan', police: 'Noto Sans', ralentissementCpu: 1 }],
},
],
['harnais', { format: 1, releves: [{ controle: 'cadre' }] }],
]);
const noms = (releve) => releve.mesures.map(({ nom }) => nom);
const DU_BANC = CONSTANTES.filter((c) => c.proprietaire === 'banc').map((c) => c.nom);
describe('lancer : les mesures attendues', () => {
test('chaque constante du banc du registre est attendue, dans l’ordre du registre, puis les gardes, par ordre de nom', () => {
assert.ok(DU_BANC.length > 0);
assert.deepEqual([...ATTENDUES], [...DU_BANC, ...GARDES]);
assert.deepEqual([...GARDES], ['DUREE_GENERATION_PAR_DEFAUT', 'TEMPS_IMAGE_LISTES']);
assert.ok(GARDES.every((nom) => !CONSTANTES.some((c) => c.nom === nom)));
});
});
describe('lancer : assembler (§ 19.10)', () => {
test('un relevé conforme : les mesures dans l’ordre du registre, puis les gardes ; plafond et parallèle repris', () => {
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
assert.deepEqual(verifierReleve(releve), []);
assert.deepEqual(noms(releve), [
'SEUIL_SIEGES_LISIBLES',
'TOLERANCE_CHAINE_TEMOIN',
'RALENTISSEMENT_PLEINE_CHARGE',
'SEUIL_MATRICE_PARTICIPANTS',
'LARGEUR_TRONCATURE_NOM_PX',
'DUREE_GENERATION_PAR_DEFAUT',
'TEMPS_IMAGE_LISTES',
]);
assert.deepEqual(Object.keys(releve), ['format', 'configuration', 'mesures', 'plafondAPriori', 'parallele']);
assert.equal(releve.plafondAPriori[0].cle, 'grande');
assert.deepEqual(releve.parallele, [{ coeurs: 1, ralentissement: 1 }]);
assert.equal(releve.mesures[0].police, 'Noto Sans');
});
test('l’ordre des mesures ne dépend ni de l’ordre des sorties ni de celui des attendues', () => {
const inverse = new Map([...sorties()].reverse());
const attendues = [...ATTENDUES].reverse();
const releve = assembler({ configuration: configuration(), sorties: inverse, attendues });
assert.deepEqual(noms(releve), [...DU_BANC, ...GARDES]);
});
test('l’ordre des mesures est celui du registre reçu : un registre renversé les renverse, les gardes restant après', () => {
const renverse = [...CONSTANTES].reverse();
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES, constantes: renverse });
assert.deepEqual(noms(releve), [...[...DU_BANC].reverse(), ...GARDES]);
});
test('chaque mesure porte le ralentissement sous lequel elle s’est prise : le sien, sinon celui de la configuration de sa sortie', () => {
const recues = sorties();
const releve = assembler({ configuration: configuration(), sorties: recues, attendues: ATTENDUES });
assert.deepEqual(
releve.mesures.map(({ nom, ralentissementCpu }) => [nom, ralentissementCpu]),
[
['SEUIL_SIEGES_LISIBLES', 1],
['TOLERANCE_CHAINE_TEMOIN', 1],
['RALENTISSEMENT_PLEINE_CHARGE', 1],
['SEUIL_MATRICE_PARTICIPANTS', 1],
['LARGEUR_TRONCATURE_NOM_PX', 1],
['DUREE_GENERATION_PAR_DEFAUT', 1],
['TEMPS_IMAGE_LISTES', 4],
],
);
assert.equal(releve.configuration.ralentissementCpu, 4);
assert.ok(!Object.hasOwn(recues.get('parallele').mesures[0], 'ralentissementCpu'), 'les sorties reçues restent intactes');
const sansConfiguration = sorties();
delete sansConfiguration.get('parallele').configuration;
const autre = assembler({ configuration: configuration(), sorties: sansConfiguration, attendues: ATTENDUES });
assert.ok(!Object.hasOwn(autre.mesures[2], 'ralentissementCpu'));
});
test('une sortie manquante se refuse en nommant chaque mesure qu’elle portait', () => {
const manque = sorties();
manque.delete('parallele');
assert.throws(
() => assembler({ configuration: configuration(), sorties: manque, attendues: ATTENDUES }),
/mesure attendue absente : RALENTISSEMENT_PLEINE_CHARGE, DUREE_GENERATION_PAR_DEFAUT/,
);
const vide = new Map();
assert.throws(() => assembler({ configuration: configuration(), sorties: vide, attendues: ATTENDUES }), /absente : SEUIL_SIEGES_LISIBLES/);
});
test('une mesure en double, une mesure inattendue, un champ de sommet deux fois porté se refusent', () => {
const double = sorties();
double.set('autre', { mesures: [{ nom: 'SEUIL_SIEGES_LISIBLES', unite: 'places', valeurProposee: 3, critere: 'x' }] });
assert.throws(() => assembler({ configuration: configuration(), sorties: double, attendues: ATTENDUES }), /en double : SEUIL_SIEGES_LISIBLES/);
const inattendue = sorties();
inattendue.set('autre', { mesures: [{ nom: 'TEMPS_INVENTE', unite: 'ms', valeurProposee: 1, critere: 'x' }] });
assert.throws(() => assembler({ configuration: configuration(), sorties: inattendue, attendues: ATTENDUES }), /inattendue : TEMPS_INVENTE/);
const deuxParalleles = sorties();
deuxParalleles.set('autre', { parallele: [] });
assert.throws(() => assembler({ configuration: configuration(), sorties: deuxParalleles, attendues: ATTENDUES }), /parallele : porté par deux sorties/);
});
test('un relevé que verifierReleve refuse ne s’assemble pas : la faute est nommée', () => {
const fautive = sorties();
fautive.get('temoin').mesures[0].critere = 'relevé sous /home/quenouille/banc';
assert.throws(
() => assembler({ configuration: configuration(), sorties: fautive, attendues: ATTENDUES }),
/mesures\[1\]\.critere : chemin de compte/,
);
});
test('des sorties qui ne sont pas une table se refusent', () => {
assert.throws(() => assembler({ configuration: configuration(), sorties: {}, attendues: ATTENDUES }), TypeError);
});
});
describe('lancer : la configuration ne nomme pas la machine (contrainte 27)', () => {
test('ni le nom d’hôte, ni l’utilisateur, ni le dossier personnel, ni le modèle du processeur injectés n’y paraissent', () => {
const texte = JSON.stringify(configuration());
for (const nom of [HOTE_INVENTE, UTILISATEUR_INVENTE, DOSSIER_INVENTE, MODELE_INVENTE]) assert.ok(!texte.includes(nom), nom);
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
for (const nom of [HOTE_INVENTE, MODELE_INVENTE]) assert.ok(!JSON.stringify(releve).includes(nom), nom);
});
test('la liste fermée du contrat : cadre du projet banc, facteur, versions, processeurs logiques, ralenti ×4, police embarquée', () => {
assert.deepEqual(configuration(), {
cadre: projetBanc().provide.cadreBanc,
facteurEchelle: 1,
navigateur: 'Chromium 140.0',
node: 'v26.10.0',
processeursLogiques: 6,
ralentissementCpu: 4,
police: FAMILLE_EMBARQUEE,
});
assert.equal(FAMILLE_EMBARQUEE, 'Noto Sans');
});
});
describe('lancer : le jugement qui rend la sortie non nulle', () => {
const releve = () => assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
test('un relevé tenu sous la police embarquée : aucun motif', () => {
assert.deepEqual(jugement(releve()), { bloquants: [], nonTenus: [] });
});
test('une mesure de texte sous une autre police, ou sans police nommée : motif bloquant', () => {
const r = releve();
r.mesures[0].police = 'DejaVu Sans';
delete r.mesures[1].police;
assert.deepEqual(jugement(r).bloquants, [
'SEUIL_SIEGES_LISIBLES mesurée sous la police « DejaVu Sans », et non sous Noto Sans',
'TOLERANCE_CHAINE_TEMOIN mesurée sous la police « inconnue », et non sous Noto Sans',
]);
});
test('un verdict non tenu : motif non bloquant, qui nomme la mesure et son critère', () => {
const r = releve();
r.mesures.find(({ nom }) => nom === 'TEMPS_IMAGE_LISTES').verdict = 'nonTenu';
assert.deepEqual(jugement(r), { bloquants: [], nonTenus: ['TEMPS_IMAGE_LISTES : verdict non tenu — p95 sous 33 ms'] });
});
});
describe('lancer : la région des constantes (§ 19.8)', () => {
test('autant de lignes que le registre, sous un en-tête et son séparateur', () => {
const lignes = regionConstantes().split('\n');
assert.equal(lignes.length, CONSTANTES.length + 2);
CONSTANTES.forEach((c, i) => assert.ok(lignes[i + 2].startsWith(`| \`${c.nom}\` |`), c.nom));
});
test('elle lit le relevé reçu : la valeur proposée paraît', () => {
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
const ligne = regionConstantes({ releve }).split('\n').find((l) => l.startsWith('| `SEUIL_SIEGES_LISIBLES` |'));
assert.match(ligne, /\| 17 places \|/);
});
test('chaque constante du banc cite le ralentissement de sa mesure : à ×1, « processeur non ralenti », jamais ×4', () => {
const releve = assembler({ configuration: configuration(), sorties: sorties(), attendues: ATTENDUES });
const lignes = regionConstantes({ releve }).split('\n');
for (const nom of DU_BANC) {
const ligne = lignes.find((l) => l.startsWith(`| \`${nom}\` |`));
assert.match(ligne, /, processeur non ralenti, police Noto Sans \|$/, nom);
assert.doesNotMatch(ligne, /ralenti ×4/, nom);
}
});
});
describe('lancer : executer, sur des effets factices', () => {
// Un passage factice : chaque effet se note dans journal, dans l'ordre.
// fichiers tient les sorties que le navigateur aurait écrites.
function passage({ codeVitest = 0, fichiers = sorties(), plafond = sorties().get('plafond').plafondAPriori } = {}) {
const journal = [];
const ecrits = new Map();
const lignes = [];
const effets = {
mesurerPlafond: () => {
journal.push('plafond');
return plafond;
},
lancerVitest: () => {
journal.push('vitest');
return codeVitest;
},
lireNavigateur: async () => 'Chromium 140.0',
systeme: systemeInvente(),
node: 'v26.10.0',
effacer: (chemin) => journal.push(`effacer ${chemin}`),
lire: (chemin) => {
const nom = /^scripts\/banc\/sorties\/([a-z_]+)\.json$/.exec(chemin)?.[1];
if (nom === undefined || !fichiers.has(nom)) throw Object.assign(new Error(`absent : ${chemin}`), { code: 'ENOENT' });
return JSON.stringify(fichiers.get(nom));
},
ecrire: (chemin, texte) => ecrits.set(chemin, texte),
sortie: (ligne) => lignes.push(ligne),
};
return { effets, journal, ecrits, lignes };
}
test('le déroulé : sorties effacées, mesures node, banc navigateur ; le relevé écrit, le tableau et les écarts imprimés ; code 0', async () => {
const p = passage();
assert.equal(await executer(p.effets), 0);
assert.deepEqual(p.journal, [...SORTIES_NAVIGATEUR.map((nom) => `effacer scripts/banc/sorties/${nom}.json`), 'plafond', 'vitest']);
assert.deepEqual([...p.ecrits.keys()], [CHEMIN_RELEVE]);
const ecrit = p.ecrits.get(CHEMIN_RELEVE);
assert.ok(ecrit.endsWith('}\n'));
const releve = JSON.parse(ecrit);
assert.deepEqual(verifierReleve(releve), []);
assert.deepEqual(noms(releve), [...ATTENDUES]);
const texte = p.lignes.join('\n');
assert.ok(texte.includes(regionConstantes({ releve })));
// Les valeurs du registre se lisent au registre : une adoption ne change
// pas ce que l'épreuve éprouve.
const registre = (nom) => CONSTANTES.find((c) => c.nom === nom).valeur;
assert.ok(texte.includes(`SEUIL_SIEGES_LISIBLES : registre ${registre('SEUIL_SIEGES_LISIBLES')} places, relevé 17 places — à adopter`));
assert.match(
texte,
/RALENTISSEMENT_PLEINE_CHARGE : registre 2 ×, relevé 2,5 × — relevé, non adopté : défaut de travail sous la machine de référence/,
);
assert.doesNotMatch(texte, /RALENTISSEMENT_PLEINE_CHARGE : .*à adopter/);
// L'accord imprimé est celui du registre avec le relevé écrit : une
// constante adoptée que ce relevé inventé contredit y est nommée.
const ecarts = accordRegistre(CONSTANTES, releve);
const accord = ecarts.length === 0
? 'Accord du registre avec le relevé : aucun écart.'
: ['Accord du registre avec le relevé :', ...ecarts.map(({ nom, faute }) => `- ${nom} : ${faute}`)].join('\n');
assert.ok(texte.includes(accord), accord);
});
test('une épreuve du banc tombée (un contrôle du harnais) : code 1, aucun relevé écrit', async () => {
const p = passage({ codeVitest: 1 });
assert.equal(await executer(p.effets), 1);
assert.equal(p.ecrits.size, 0);
assert.match(p.lignes.join('\n'), /épreuve du banc est tombée/);
});
test('une sortie manquante : code 1, aucun relevé écrit, la mesure nommée', async () => {
const fichiers = sorties();
fichiers.delete('temoin');
const p = passage({ fichiers });
assert.equal(await executer(p.effets), 1);
assert.equal(p.ecrits.size, 0);
assert.match(p.lignes.join('\n'), /absente : TOLERANCE_CHAINE_TEMOIN/);
});
test('un verdict non tenu : le relevé s’écrit, qui le porte, et le code vaut 1', async () => {
const fichiers = sorties();
fichiers.get('temps_image').mesures[0].verdict = 'nonTenu';
const p = passage({ fichiers });
assert.equal(await executer(p.effets), 1);
assert.equal(JSON.parse(p.ecrits.get(CHEMIN_RELEVE)).mesures.find(({ nom }) => nom === 'TEMPS_IMAGE_LISTES').verdict, 'nonTenu');
assert.match(p.lignes.join('\n'), /TEMPS_IMAGE_LISTES : verdict non tenu/);
});
test('une mesure de texte hors de la police embarquée : code 1, aucun relevé écrit', async () => {
const fichiers = sorties();
fichiers.get('temoin').mesures[0].police = 'DejaVu Sans';
const p = passage({ fichiers });
assert.equal(await executer(p.effets), 1);
assert.equal(p.ecrits.size, 0);
assert.match(p.lignes.join('\n'), /sous la police « DejaVu Sans »/);
});
});
describe('lancer : le relevé committé', () => {
test('il est conforme, et porte chaque mesure attendue dans l’ordre du registre', () => {
assert.deepEqual(verifierReleve(releveCommite), []);
assert.deepEqual(noms(releveCommite), [...ATTENDUES]);
for (const champ of ['navigateur', 'node', 'processeursLogiques']) {
assert.notEqual(releveCommite.configuration[champ] ?? null, null, champ);
}
assert.deepEqual(Object.keys(releveCommite.configuration), [
'cadre',
'facteurEchelle',
'navigateur',
'node',
'processeursLogiques',
'ralentissementCpu',
'police',
]);
for (const mesure of releveCommite.mesures) assert.ok(Number.isFinite(mesure.ralentissementCpu), mesure.nom);
});
});