gestion_table_tournante_libre/src/demo/noms.test.js
Mathieu Benoit a2a3a7eca8 [ADD] demo: four seeded configurations, forged names, witness round
The demonstrations are built from a seed in a fixed draw order. The large
one seats 260 at 29 tables of 8 and 4 of 7 with one facilitator each, its
variant at 33 tables of 8; the small one seats 12 at four tables of 3 in
groups 4/4/4, its conflict variant 5/4/3. Group sizes follow the weighted
law, a seed with no witness round is refused, and forged names appear
nowhere else in the repository.

Checked: the large configuration is tight at 260 seats; each of the four
refusals raises its named error; a wrong table split fails 12 tests.

--- FR ---

[ADD] demo : quatre configurations ensemencées, noms forgés, tour témoin

Les démonstrations se construisent depuis une graine, dans un ordre de
tirages fixé. La grande assoit 260 personnes à 29 tables de 8 et 4 de 7,
un animateur par table, sa variante à 33 tables de 8 ; la petite assoit
12 personnes à quatre tables de 3 en groupes 4/4/4, sa variante de
conflit 5/4/3. Les tailles de groupe suivent la loi pondérée, une graine
sans tour témoin est refusée, et les noms forgés n'apparaissent nulle
part ailleurs dans le dépôt.

Vérifié : la grande configuration est tendue à 260 sièges ; chacun des
quatre refus lève son erreur nommée ; une mauvaise répartition des tables
fait échouer 12 épreuves.

Assisted-by: Claude Opus 5.5
2026-10-05 17:27:34 -04:00

156 lines
6.1 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 des réservoirs de noms du générateur (§ 15.4, § 15.6) : tailles,
// unicité, ordre des produits, forme des fragments, noms longs. Ce fichier
// n'écrit aucun nom forgé : il les lit dans noms.js, seul fichier où le
// balayage de catalogue.long.test.js les admet.
import assert from 'node:assert/strict';
import { describe, test } from '../../test/lanceur.js';
import {
NOMS_LONGS,
PREFIXES,
PRENOMS,
RACINES,
TERMINAISONS,
TETES,
organisations,
patronymes,
} from './noms.js';
// [nom de la liste, liste, taille minimale du plan]
const LISTES = [
['PRENOMS', PRENOMS, 40],
['PREFIXES', PREFIXES, 20],
['TERMINAISONS', TERMINAISONS, 16],
['TETES', TETES, 10],
['RACINES', RACINES, 26],
];
const distincts = (liste) => new Set(liste).size;
// Un mot : une capitale suivie de minuscules, sans espace, trait d'union ni
// chiffre. Un nom de cette forme se cherche en mot entier sans ambiguïté.
const MOT = /^\p{Lu}\p{Ll}+$/u;
// Caractères d'une chaîne, en points de code.
const caracteres = (texte) => [...texte];
describe('noms : listes', () => {
test('chaque liste atteint sa taille minimale, sans doublon, figée et en NFC', () => {
const ecarts = [];
for (const [nom, liste, minimum] of LISTES) {
if (liste.length < minimum) ecarts.push(`${nom} : ${liste.length} éléments, minimum ${minimum}`);
if (distincts(liste) !== liste.length) ecarts.push(`${nom} : doublon`);
if (!Object.isFrozen(liste)) ecarts.push(`${nom} : modifiable`);
if (liste.some((element) => element !== element.normalize('NFC'))) {
ecarts.push(`${nom} : élément hors NFC`);
}
}
assert.deepEqual(ecarts, []);
});
test('les fragments forgés et les têtes ont la forme de mots', () => {
assert.ok([PREFIXES, TERMINAISONS, RACINES, TETES].every((liste) => liste.length > 0), 'liste vide');
const ecarts = [
...PREFIXES.filter((p) => !/^\p{Lu}\p{Ll}*$/u.test(p)).map((p) => `préfixe ${p}`),
...TERMINAISONS.filter((t) => !/^\p{Ll}+$/u.test(t)).map((t) => `terminaison ${t}`),
...RACINES.filter((r) => !MOT.test(r)).map((r) => `racine ${r}`),
...TETES.filter((t) => !MOT.test(t)).map((t) => `tête ${t}`),
];
assert.deepEqual(ecarts, []);
});
});
describe('noms : patronymes', () => {
test('au moins 320 patronymes distincts, chacun un mot', () => {
const liste = patronymes();
assert.ok(liste.length >= 320, `${liste.length} patronymes`);
assert.equal(distincts(liste), liste.length);
assert.deepEqual(liste.filter((p) => !MOT.test(p)).length, 0);
});
test('le produit PREFIXES × TERMINAISONS, préfixe par préfixe', () => {
assert.ok(PREFIXES.length > 0 && TERMINAISONS.length > 0, 'liste vide');
assert.deepEqual(
patronymes(),
PREFIXES.flatMap((prefixe) => TERMINAISONS.map((terminaison) => prefixe + terminaison)),
);
});
test('chaque appel rend une liste neuve', () => {
const premiere = patronymes();
assert.ok(premiere.length > 0, 'aucun patronyme');
premiere.length = 0;
assert.equal(patronymes().length, PREFIXES.length * TERMINAISONS.length);
});
});
describe('noms : organisations', () => {
test('au moins 260 organisations distinctes', () => {
const liste = organisations();
assert.ok(liste.length >= 260, `${liste.length} organisations`);
assert.equal(distincts(liste), liste.length);
});
test('le produit TETES × RACINES, tête par tête, séparées par une espace', () => {
assert.ok(TETES.length > 0 && RACINES.length > 0, 'liste vide');
assert.deepEqual(
organisations(),
TETES.flatMap((tete) => RACINES.map((racine) => `${tete} ${racine}`)),
);
});
test('chaque appel rend une liste neuve', () => {
const premiere = organisations();
assert.ok(premiere.length > 0, 'aucune organisation');
premiere.length = 0;
assert.equal(organisations().length, TETES.length * RACINES.length);
});
test('aucune racine n’est aussi un patronyme', () => {
const noms = new Set(patronymes());
assert.ok(noms.size > 0 && RACINES.length > 0, 'liste vide');
assert.deepEqual(RACINES.filter((racine) => noms.has(racine)).length, 0);
});
});
describe('noms : noms longs (§ 15.4)', () => {
test('trois noms distincts, figés, en NFC, hors du réservoir des patronymes et des racines', () => {
assert.equal(NOMS_LONGS.length, 3);
assert.equal(distincts(NOMS_LONGS), 3);
assert.ok(Object.isFrozen(NOMS_LONGS));
const reservoir = new Set([...patronymes(), ...RACINES]);
assert.deepEqual(
NOMS_LONGS.filter((nom) => reservoir.has(nom) || nom !== nom.normalize('NFC')).length,
0,
);
});
test('chacun compte au moins 20 caractères, plus que tout patronyme du réservoir', () => {
const plusLong = Math.max(...patronymes().map((nom) => caracteres(nom).length));
assert.ok(Number.isInteger(plusLong), 'aucun patronyme');
assert.ok(NOMS_LONGS.length > 0, 'aucun nom long');
const seuil = Math.max(20, plusLong + 1);
// [rang du nom long, caractères] de chaque nom sous le seuil.
const courts = NOMS_LONGS.map((nom, rang) => [rang, caracteres(nom).length]).filter(([, n]) => n < seuil);
assert.deepEqual(courts, []);
});
test('le premier est fait de glyphes larges : M et W pour au moins la moitié de ses caractères', () => {
const tous = caracteres(NOMS_LONGS[0]);
const larges = tous.filter((c) => /[MWmw]/u.test(c));
assert.ok(larges.length > 0 && larges.length * 2 >= tous.length, `${larges.length} sur ${tous.length}`);
});
test('le deuxième porte au moins cinq lettres à diacritique', () => {
const marques = [...NOMS_LONGS[1].normalize('NFD')].filter((c) => /\p{M}/u.test(c));
assert.ok(marques.length >= 5, `${marques.length} diacritiques`);
});
test('le troisième est le plus long, et compte au moins 40 caractères', () => {
const [large, accentue, long] = NOMS_LONGS.map((nom) => caracteres(nom).length);
assert.ok(long >= 40, `${long} caractères`);
assert.ok(long > large && long > accentue);
});
});