Files
ki-fwi/tools/data/test-porte.mjs
T

239 lines
7.5 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* test-porte.mjs — prouve que la porte se ferme.
*
* Une règle de sécurité qu'on n'a jamais vue refuser quoi que ce soit n'est pas une règle,
* c'est une intention. Ce test fabrique les cas interdits par le brief et vérifie que
* `validate.mjs` les rejette bien avec un code de sortie non nul.
*
* Chaque cas correspond à une phrase du brief, citée en regard.
*/
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { spawnSync } from 'node:child_process';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const BAC = join('tools', 'data', 'out', 'test-porte');
/** Applique une mutation à une copie profonde de l'entrée de référence. */
const muter = (base, fn) => {
const c = structuredClone(base);
fn(c);
return c;
};
/** Un média de référence, valide et utilisable comme épreuve. */
const mediaValide = (over = {}) => ({
id: 'photo-temoin',
filiere: 'A',
type_media: 'photo',
vue: 'fruit_entier',
usages: ['epreuve'],
source: 'wikimedia_commons',
auteur: 'Témoin',
licence: 'CC-BY-SA-4.0',
licence_url: null,
url_fichier: 'https://upload.wikimedia.org/temoin.jpg',
url_description: 'https://commons.wikimedia.org/wiki/File:temoin.jpg',
largeur: 800,
hauteur: 600,
mime: 'image/jpeg',
description: null,
date_prise_de_vue: null,
attribution_requise: true,
partage_identique: true,
usage_non_commercial: false,
composite: false,
valide: true,
valide_par: 'porteur',
valide_le: '2026-07-27',
note_validation: null
});
const CAS = [
{
nom: 'épreuve sur un média non validé',
regle: 'brief §2 — « Le build échoue si un média non validé est référencé par une épreuve. »',
muter: (e) => {
e.medias = [mediaValide({}), mediaValide()];
e.medias[0].id = 'photo-non-validee';
e.medias[0].valide = false;
e.medias[0].valide_par = null;
}
},
{
nom: 'épreuve sur un asset généré (filière B)',
regle: "brief §10 — « Aucune épreuve d'identification ne s'appuie sur un asset IA. »",
muter: (e) => {
e.medias = [mediaValide()];
e.medias[0].filiere = 'B';
e.medias[0].type_media = 'image_generee';
}
},
{
nom: 'épreuve sur une planche botanique',
regle: "brief §2 — filière A = photographie réelle ; une gravure documente mais n'entraîne pas la reconnaissance du réel.",
muter: (e) => {
e.medias = [mediaValide()];
e.medias[0].type_media = 'planche_botanique';
}
},
{
nom: 'épreuve portée par une plante dangereuse',
regle: 'brief §6 — « Pa touché : jamais chronométré, jamais scoré, jamais transformé en épreuve. »',
muter: (e) => {
e.categorie = 'plante_dangereuse';
e.danger = { niveau: 'critique', resume: 'Ne jamais toucher ni manger ce fruit.' };
delete e.saison_mois;
e.comestible = false;
e.medias = [mediaValide()];
}
},
{
nom: 'média à attribution sans auteur',
regle: 'brief §3 — « Récupérer auteur, licence, URL du fichier, URL de la page de description. »',
muter: (e) => {
e.medias = [mediaValide()];
e.medias[0].auteur = null;
}
},
{
nom: 'compositing sur un média en partage à lidentique',
regle: "ADR 0001 — le SA contamine les dérivés, le NC est incompatible avec le SA : aucun compositing.",
muter: (e) => {
e.medias = [mediaValide()];
e.medias[0].composite = true;
}
},
{
nom: 'entrée publiée avec une seule photo validée',
regle: 'brief §8 — « ≥ 2 photos Commons par fruit avec auteur, licence et URL. »',
muter: (e) => {
e.medias = [mediaValide()];
e.orthographe = { norme: 'gwadloupeyen', statut: 'relu', relu_par: 'relecteur', question_ouverte: null };
e.valide = true;
}
},
{
nom: 'entrée publiée sans relecture kréyòl',
regle: "brief §5 — « L'orthographe kréyòl n'est pas de ton ressort. »",
muter: (e) => {
e.medias = [mediaValide(), muter(mediaValide(), (m) => (m.id = 'photo-temoin-2'))];
e.valide = true;
}
},
{
nom: 'validation anonyme',
regle: 'porte du projet — une validation sans nom nengage personne.',
muter: (e) => {
e.medias = [mediaValide()];
e.medias[0].valide_par = null;
}
},
{
nom: 'doute orthographique signalé sans question posée',
regle: 'ADR 0005 — on ne signale pas un doute sans dire lequel, sinon on décourage au lieu dappeler.',
muter: (e) => {
e.orthographe = { norme: 'gwadloupeyen', statut: 'en_discussion', relu_par: null, question_ouverte: null };
}
},
{
nom: 'enregistrement sans locuteur crédité',
regle: 'brief §6 et LICENSE-CONTENT §4 — le locuteur conserve ses droits voisins et est crédité nominativement.',
muter: (e) => {
e.audio = {
gcf: '/audio/kowosol.opus',
locuteur: null,
source: 'contributeur',
licence: 'CC-BY-SA-4.0',
url_description: null,
valide: false,
note: null
};
}
},
{
nom: 'enregistrement externe sans page dorigine',
regle: 'brief §3 — toute ressource tierce porte sa source vérifiable.',
muter: (e) => {
e.audio = {
gcf: '/audio/kowosol.opus',
locuteur: 'Palé Kréyol',
source: 'lingualibre',
licence: 'CC-BY-SA-4.0',
url_description: null,
valide: false,
note: null
};
}
},
{
nom: 'variante orthographique identique au nom principal',
regle: 'ADR 0005 — une graphie confirmée est une corroboration, pas une variante : ne pas gonfler le doute.',
muter: (e) => {
e.noms.variantes_gcf = [{ graphie: e.noms.gcf, source: 'un locuteur', ile: 'gp', note: null }];
}
}
];
/** Cas témoin : doit passer. Un test qui échoue toujours ne prouve rien non plus. */
const TEMOIN = {
nom: 'TÉMOIN — entrée correcte, doit passer',
attendu: 0,
muter: (e) => {
e.medias = [mediaValide(), muter(mediaValide(), (m) => (m.id = 'photo-temoin-2'))];
e.orthographe = { norme: 'gwadloupeyen', statut: 'relu', relu_par: 'relecteur', question_ouverte: null };
e.valide = true;
}
};
async function jouer(cas, base, i) {
const dir = join(BAC, `cas-${i}`);
await mkdir(join(ROOT, dir), { recursive: true });
await writeFile(join(ROOT, dir, `${base.id}.json`), JSON.stringify(muter(base, cas.muter), null, '\t'));
const r = spawnSync(process.execPath, [join(ROOT, 'tools', 'data', 'validate.mjs')], {
cwd: ROOT,
env: { ...process.env, KI_FWI_DATA: dir },
encoding: 'utf8'
});
const attendu = cas.attendu ?? 1;
const obtenu = r.status;
const ok = obtenu === attendu;
console.log(`${ok ? '✔' : '✘'} ${cas.nom}`);
if (cas.regle) console.log(` ${cas.regle}`);
if (!ok) {
console.log(` attendu code ${attendu}, obtenu ${obtenu}`);
console.log((r.stderr || r.stdout).split('\n').slice(-8).join('\n'));
}
return ok;
}
async function main() {
const base = JSON.parse(await readFile(join(ROOT, 'src/lib/data/fruits/kowosol.json'), 'utf8'));
await rm(join(ROOT, BAC), { recursive: true, force: true });
console.log('\nTest de la porte de validation\n');
const resultats = [];
resultats.push(await jouer(TEMOIN, base, 0));
for (const [i, cas] of CAS.entries()) resultats.push(await jouer(cas, base, i + 1));
await rm(join(ROOT, BAC), { recursive: true, force: true });
const echecs = resultats.filter((r) => !r).length;
console.log(`\n${resultats.length - echecs}/${resultats.length} cas conformes.`);
if (echecs) {
console.error(`✘ ${echecs} cas non couvert(s) : la porte laisse passer ce qu'elle devrait refuser.`);
process.exit(1);
}
console.log('✔ La porte refuse tout ce que le brief interdit.');
}
main().catch((e) => {
console.error('✘', e.stack ?? e.message);
process.exit(1);
});