#!/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 à l’identique', 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' }; 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 n’engage personne.', muter: (e) => { e.medias = [mediaValide()]; e.medias[0].valide_par = 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' }; 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); });