[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves de la séance (§ 2.1, § 8.1 à § 8.6, § 8.8, § 9, § 10.1), toutes sur
// le système de fichiers d'épreuve, qui compte ses écritures, avec une
// horloge et un planificateur factices : rien n'attend le temps réel. Une
// ouverture n'écrit rien ; un geste écrit une entrée puis un état ; un refus
// n'écrit rien ; le verrou se prend à l'entrée en écriture et se rend au
// retour en lecture, explicite ou après l'inactivité ; le support qui rompt
// se remplace par un autre dossier. Les noms d'épreuve sont inventés ; la
// version vient du module engendré.
import assert from 'node:assert/strict' ;
import { readFileSync } from 'node:fs' ;
import { creerFichiersSimules } from '../../test/fichiers_simules.js' ;
import { describe , test } from '../../test/lanceur.js' ;
import { apercevoir } from '../csv/apercu.js' ;
import { rechercher } from '../moteur/recherche.js' ;
import { serialiser } from '../stockage/canonique.js' ;
import { creer as creerDansLeDepot , ouvrir as ouvrirDansLeDepot } from '../stockage/depot.js' ;
import { analyser , configurationDepuisCharge , creerCharge , etatDeduit } from '../stockage/document.js' ;
import { ErreurStockage } from '../stockage/erreurs.js' ;
import { ligneEntree , ligneOuverture , lireJournal , reconstruire } from '../stockage/journal.js' ;
import { SUFFIXES } from '../stockage/noms.js' ;
import { VERSION } from '../version.genere.js' ;
import { libelle } from './libelles.js' ;
import { INACTIVITE _MS , creerSeance } from './seance.js' ;
const V1 = VERSION . affichee ;
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre' ;
const DOC = { id : 'documents' , chemin : DOCUMENTS } ;
const PORTABLE = { id : 'portable' , chemin : 'E:\\soirees\\data' } ;
const BASE = 'Soirée des Lucioles' ;
const ETAT = ` ${ BASE } ${ SUFFIXES . etat } ` ;
const JOURNAL = ` ${ BASE } ${ SUFFIXES . journal } ` ;
const VERROU = ` ${ BASE } ${ SUFFIXES . verrou } ` ;
const CREATION = "Création de l'événement « Soirée des Lucioles »" ;
// --- Le montage -------------------------------------------------------------
const deux = ( valeur ) => String ( valeur ) . padStart ( 2 , '0' ) ;
// Horloge d'épreuve : une minute de plus à chaque lecture, à partir de 8 h le
// 17 mai 2031, à l'heure de l'Est.
function creerHorloge ( ) {
let n = 0 ;
return {
horodatage ( ) {
const minutes = 8 * 60 + n ;
n += 1 ;
return ` 2031-05-17T ${ deux ( Math . floor ( minutes / 60 ) ) } : ${ deux ( minutes % 60 ) } :00-04:00 ` ;
} ,
} ;
}
// Identifiants d'épreuve, numérotés dans l'ordre des tirages : le premier est
// celui de la séance. tirages les garde.
function creerIdentifiants ( prefixe ) {
const tirages = [ ] ;
const identifiant = ( ) => {
tirages . push ( ` ${ prefixe } - ${ tirages . length + 1 } ` ) ;
return tirages . at ( - 1 ) ;
} ;
identifiant . tirages = tirages ;
return identifiant ;
}
// Planificateur d'épreuve : rien ne part seul. avancer(ms) déclenche les
// minuteries échues, dans l'ordre de leur échéance puis de leur création, et
// attend ce que chacune rend.
function creerPlanificateur ( ) {
let maintenant = 0 ;
let suivant = 1 ;
const minuteries = new Map ( ) ;
return {
planifier ( fn , ms ) {
const id = suivant ;
suivant += 1 ;
minuteries . set ( id , { fn , echeance : maintenant + ms } ) ;
return id ;
} ,
annuler ( id ) {
minuteries . delete ( id ) ;
} ,
async avancer ( ms ) {
maintenant += ms ;
const echues = [ ... minuteries ]
. filter ( ( [ , { echeance } ] ) => echeance <= maintenant )
. sort ( ( [ a , m ] , [ b , n ] ) => m . echeance - n . echeance || a - b ) ;
for ( const [ id , { fn } ] of echues ) {
if ( ! minuteries . has ( id ) ) continue ;
minuteries . delete ( id ) ;
await fn ( ) ;
}
} ,
get actives ( ) {
return minuteries . size ;
} ,
} ;
}
// Une séance sur le système d'épreuve, sa racine créée : documents par
// défaut.
function monter ( { fs = creerFichiersSimules ( ) , racine = DOC , prefixe = 'seance' , inactiviteMs } = { } ) {
fs . deposerDossier ( racine . id ) ;
const planificateur = creerPlanificateur ( ) ;
const identifiant = creerIdentifiants ( prefixe ) ;
const seance = creerSeance ( {
fs ,
racine ,
horloge : creerHorloge ( ) ,
identifiant ,
produitVersion : V1 ,
planifier : planificateur . planifier ,
annuler : planificateur . annuler ,
... ( inactiviteMs === undefined ? { } : { inactiviteMs } ) ,
} ) ;
return { fs , seance , planificateur , identifiant } ;
}
// Crée l'événement d'essai par la séance, puis passe en écriture.
async function enEcriture ( montage , champs = { } ) {
await montage . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 , ... champs } ) ;
await montage . seance . passerEnEcriture ( ) ;
}
const personne = ( id , nom , prenom = null , appartenance = null ) => ( {
id ,
nom ,
prenom ,
appartenance ,
courriel : null ,
titrePressenti : null ,
notes : null ,
exclu : false ,
} ) ;
// Quatre personnes, deux tables de deux places, deux tours ; Iris Ombrelle
// anime la table 1 : une réservation « tous » au siège 1 et le titre de ce
// siège.
function chargeEssai ( ) {
const charge = creerCharge ( { id : 'evt-lucioles' , nom : BASE , siegesParDefaut : 2 , tours : 2 } ) ;
charge . prochainsIds = { participant : 5 , table : 3 , proposition : 1 } ;
charge . participants = [
personne ( 1 , 'Ombrelle' , 'Iris' , 'Club des Merles' ) ,
personne ( 2 , 'Grisaille' , null , 'Club des Merles' ) ,
personne ( 3 , 'Pervenche' , 'Théo' ) ,
personne ( 4 , 'Lacasse' , 'Ondine' , 'Société Alpha' ) ,
] ;
charge . tables = [ 1 , 2 ] . map ( ( id ) => ( { id , numero : id , sieges : null , forme : 'ronde' , position : { x : 250 * ( id - 1 ) , y : 0 } } ) ) ;
charge . reservations = [ { participant : 1 , table : 1 , siege : 1 , portee : 'tous' , tour : null } ] ;
charge . titres = [ { table : 1 , siege : 1 , libelle : 'animation' } ] ;
return charge ;
}
// Pose sur le disque, par le dépôt, l'événement de la charge d'essai.
async function deposerEssai ( fs , charge = chargeEssai ( ) ) {
fs . deposerDossier ( 'documents' ) ;
await creerDansLeDepot ( fs , DOC , charge , {
libelle : libelle ( 'ENTREE_CREATION' , { nom : charge . evenement . nom } ) ,
nomJalon : libelle ( 'JALON_CREATION' ) ,
horodatage : '2031-05-17T07:00:00-04:00' ,
produitVersion : V1 ,
} ) ;
}
// Les arguments d'enregistrerGeneration : une génération courte du moteur.
function generer ( charge , nombre = 2 , graine = 7 ) {
const configuration = configurationDepuisCharge ( charge ) ;
return { propositions : rechercher ( configuration , { graine , arret : 40 , nombre } ) , configuration , produitVersion : V1 } ;
}
// Le journal et l'état que porte le disque.
const journalDe = ( fs , racine = 'documents' , base = BASE ) => lireJournal ( fs . contenu ( racine , ` ${ base } ${ SUFFIXES . journal } ` ) ) ;
const etatDe = ( fs , racine = 'documents' , base = BASE ) => analyser ( fs . contenu ( racine , ` ${ base } ${ SUFFIXES . etat } ` ) ) ;
const libellesDe = ( journal ) => journal . entrees . map ( ( entree ) => entree . libelle ) ;
const verrouDe = ( fs , racine = 'documents' ) => {
const texte = fs . contenu ( racine , VERROU ) ;
return texte === null ? null : JSON . parse ( texte ) ;
} ;
const noms = ( charge ) => charge . participants . map ( ( { nom } ) => nom ) ;
// Ouvre base par la séance d'un montage qui ne tient aucun verrou, et
// constate qu'ouvrir n'écrit rien (§ 8.4).
async function ouvrirSansEcrire ( { fs , seance } , base = BASE ) {
const avant = fs . compteurs . ecritures ;
const rendu = await seance . ouvrir ( base ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'ouvrir écrit' ) ;
assert . equal ( seance . mode , 'lecture' ) ;
return rendu ;
}
// Le refus attendu : le code, et les détails quand ils sont donnés. Rend
// l'erreur.
async function refus ( promesse , code , details ) {
let erreur ;
await assert . rejects ( promesse , ( raison ) => {
erreur = raison ;
return true ;
} ) ;
assert . equal ( erreur . code , code , ` ${ erreur } ` ) ;
if ( details !== undefined ) assert . deepEqual ( erreur . details , details ) ;
return erreur ;
}
// --- Les épreuves -----------------------------------------------------------
describe ( 'séance : créer et ouvrir (§ 2.1, § 8.1, § 8.4)' , ( ) => {
test ( "créer écrit le journal puis l'état, pose le jalon « Création », et laisse l'événement ouvert en lecture" , async ( ) => {
const { fs , seance , identifiant } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
const { base } = await seance . creer ( { nom : ' Soirée des Lucioles ' , date : '2031-05-17' , siegesParDefaut : 8 , tours : 3 } ) ;
assert . equal ( base , BASE ) ;
assert . equal ( fs . compteurs . ecritures - avant , 2 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . deepEqual ( identifiant . tirages , [ 'seance-1' , 'seance-2' ] ) ;
assert . deepEqual ( seance . charge . evenement , { ... creerCharge ( { id : 'seance-2' , nom : BASE , date : '2031-05-17' , siegesParDefaut : 8 , tours : 3 } ) . evenement } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [ [ 1 , 'Création' ] ] ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 1 ) ;
} ) ;
test ( "une création refusée n'écrit rien et ne tire aucun identifiant" , async ( ) => {
const { fs , seance , identifiant } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . creer ( { nom : ' ' , date : null , siegesParDefaut : 8 , tours : 3 } ) , 'NOM_REQUIS' ) ;
await refus ( seance . creer ( { nom : BASE , date : null , siegesParDefaut : 1 , tours : 3 } ) , 'SIEGES_INVALIDES' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( identifiant . tirages , [ 'seance-1' ] ) ;
assert . equal ( seance . ouvert , null ) ;
} ) ;
test ( "créer ferme l'événement en écriture : son verrou est rendu, et le nouveau s'ouvre en lecture, sans verrou" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance , planificateur } = montage ;
await enEcriture ( montage ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . creer ( { nom : 'Soirée des Hiboux' , date : null , siegesParDefaut : 2 , tours : 1 } ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom , planificateur . actives ] , [ 'lecture' , 'Soirée des Hiboux' , 0 ] ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . equal ( fs . contenu ( 'documents' , ` Soirée des Hiboux ${ SUFFIXES . verrou } ` ) , null ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "une création refusée laisse l'événement ouvert, son mode et son verrou, sans rien écrire" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . creer ( { nom : ' ' , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'NOM_REQUIS' ) ;
await refus ( seance . creer ( { nom : 'Soirée des Hiboux' , date : '2031-02-30' , siegesParDefaut : 2 , tours : 2 } ) , 'DATE_INVALIDE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom , verrouDe ( fs ) ? . seance ] , [ 'ecriture' , BASE , seance . id ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' ] ) ;
} ) ;
test ( "ouvrir n'écrit rien et laisse en lecture ; en lecture, un geste, défaire et nommer sont refusés MODE_LECTURE" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const { seance } = monter ( { fs } ) ;
const avant = fs . compteurs . ecritures ;
const { base , avertissements } = await seance . ouvrir ( BASE ) ;
assert . deepEqual ( [ base , avertissements ] , [ BASE , [ ] ] ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'ouvrir écrit' ) ;
assert . equal ( seance . mode , 'lecture' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' , { } ) ;
await refus ( seance . defaire ( ) , 'MODE_LECTURE' ) ;
await refus ( seance . revenir ( 1 ) , 'MODE_LECTURE' ) ;
await refus ( seance . nommer ( 'Avant le dessert' ) , 'MODE_LECTURE' ) ;
assert . equal ( fs . compteurs . ecritures , avant , 'un refus écrit' ) ;
assert . match ( libelle ( 'MODE_LECTURE' ) , /« Modifier »/ ) ;
} ) ;
test ( "ouvrir un secours n'écrit rien et laisse en lecture" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) } ) ;
await enEcriture ( premier ) ;
await premier . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await premier . seance . fermer ( ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
const avant = second . fs . compteurs . ecritures ;
await second . seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . equal ( second . fs . compteurs . ecritures , avant , 'ouvrirSecours écrit' ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( [ second . seance . ouvert . revision , noms ( second . seance . charge ) ] , [ 1 , [ ] ] ) ;
} ) ;
test ( "ouvrir ferme le précédent sans question : son verrou est rendu, et seul ce rendu s'écrit" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await creerDansLeDepot ( fs , DOC , creerCharge ( { id : 'evt-hiboux' , nom : 'Soirée des Hiboux' , siegesParDefaut : 2 , tours : 1 } ) , {
libelle : 'Création' ,
nomJalon : 'Création' ,
horodatage : '2031-05-17T07:00:00-04:00' ,
produitVersion : V1 ,
} ) ;
assert . notEqual ( verrouDe ( fs ) , null ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( 'Soirée des Hiboux' ) ;
assert . equal ( fs . compteurs . ecritures - avant , 1 ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . nom ] , [ 'lecture' , 'Soirée des Hiboux' ] ) ;
} ) ;
test ( "un événement qui ne s'ouvre pas laisse la séance sans événement, le précédent fermé" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . ouvrir ( 'Soirée absente' ) , 'ABSENT' ) ;
assert . equal ( montage . fs . compteurs . ecritures - avant , 1 , "seul le rendu du verrou s'écrit" ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . deepEqual ( [ montage . seance . ouvert , montage . seance . charge , montage . seance . mode ] , [ null , null , 'lecture' ] ) ;
} ) ;
test ( "sans événement ouvert, chaque geste est refusé AUCUN_EVENEMENT, sans écriture" , async ( ) => {
const { fs , seance } = monter ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . passerEnEcriture ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . defaire ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . debloquer ( ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . nommer ( 'Jalon' ) , 'AUCUN_EVENEMENT' ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'AUCUN_EVENEMENT' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : false } ) ;
} ) ;
} ) ;
describe ( "séance : le verrou, pris à l'entrée en écriture (§ 8.8)" , ( ) => {
test ( "passer en écriture prend le verrou de la séance ; le redemander ne refait rien" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( montage . fs ) . seance , montage . seance . id ) ;
assert . equal ( montage . seance . id , 'seance-1' ) ;
const avant = montage . fs . compteurs . ecritures ;
assert . deepEqual ( await montage . seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
test ( 'une seconde séance sur le même disque ouvre en lecture, et son passage en écriture est refusé VERROU_PRIS' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
const details = { seance : 'premiere-1' , depuis , vivant : true } ;
await refus ( second . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , details ) ;
assert . equal ( second . seance . mode , 'lecture' ) ;
assert . deepEqual ( second . seance . verrouAutre , details ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'premiere-1' ) ;
await refus ( second . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "le verrou d'une séance morte du même poste se reprend, et l'avertissement le dit" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
premier . fs . pannes . couperApres ( 0 ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
const repris = [ { code : 'VERROU_REPRIS' , details : { seance : 'premiere-1' , depuis } } ] ;
assert . deepEqual ( await second . seance . passerEnEcriture ( ) , { avertissements : repris } ) ;
assert . equal ( second . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( second . fs ) . seance , 'seconde-1' ) ;
assert . deepEqual ( second . seance . avertissements , repris ) ;
assert . equal ( second . seance . verrouAutre , null ) ;
} ) ;
test ( "le verrou d'un autre poste, ou illisible, ne se reprend pas" , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await enEcriture ( premier ) ;
const { depuis } = verrouDe ( premier . fs ) ;
const lointain = monter ( { fs : creerFichiersSimules ( { disque , hote : 'autre-poste' } ) , prefixe : 'lointaine' } ) ;
await ouvrirSansEcrire ( lointain ) ;
await refus ( lointain . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : 'premiere-1' , depuis , vivant : null } ) ;
premier . fs . deposer ( 'documents' , VERROU , 'pas un verrou\n' ) ;
const voisin = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'voisine' } ) ;
await ouvrirSansEcrire ( voisin ) ;
await refus ( voisin . seance . passerEnEcriture ( ) , 'VERROU_PRIS' , { seance : null , depuis : null , vivant : null } ) ;
assert . equal ( premier . fs . contenu ( 'documents' , VERROU ) , 'pas un verrou\n' ) ;
} ) ;
test ( 'sans verrou disponible, passer en écriture ne pose aucun fichier' , async ( ) => {
const montage = monter ( { fs : creerFichiersSimules ( { verrouDisponible : false } ) } ) ;
await enEcriture ( montage ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
await montage . seance . passerEnLecture ( ) ;
assert . equal ( montage . seance . mode , 'lecture' ) ;
} ) ;
test ( "fermer rend le verrou et oublie l'événement" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . seance . fermer ( ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . deepEqual ( [ montage . seance . ouvert , montage . seance . mode , montage . planificateur . actives ] , [ null , 'lecture' , 0 ] ) ;
} ) ;
} ) ;
describe ( "séance : l'inactivité (§ 8.4)" , ( ) => {
test ( 'au terme du délai, la séance revient seule en lecture et rend son verrou' , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . planificateur . avancer ( INACTIVITE _MS - 1 ) ;
assert . equal ( montage . seance . mode , 'ecriture' ) ;
await montage . planificateur . avancer ( 1 ) ;
assert . equal ( montage . seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . equal ( montage . planificateur . actives , 0 ) ;
assert . equal ( INACTIVITE _MS , 10 * 60 * 1000 ) ;
} ) ;
test ( "pendant un geste ouvert, rien ne tombe ; les gestes s'emboîtent ; le dernier fini, le compte repart" , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
assert . equal ( planificateur . actives , 1 ) ;
seance . debuterGeste ( ) ;
seance . debuterGeste ( ) ;
assert . equal ( planificateur . actives , 0 , 'un geste ouvert laisse une minuterie armée' ) ;
await planificateur . avancer ( 3 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirGeste ( ) ;
assert . equal ( planificateur . actives , 0 , 'le geste encore ouvert laisse une minuterie armée' ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . equal ( planificateur . actives , 0 , 'un geste achevé arme le compte malgré le geste ouvert' ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirGeste ( ) ;
assert . equal ( planificateur . actives , 1 ) ;
await planificateur . avancer ( INACTIVITE _MS - 1 ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
await planificateur . avancer ( 1 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
} ) ;
test ( "pendant un calcul ouvert, rien ne tombe ; fini, le compte repart ; un calcul fini de trop ne compte pas" , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
seance . debuterCalcul ( ) ;
assert . equal ( seance . calculEnCours , true ) ;
assert . equal ( planificateur . actives , 0 , 'un calcul ouvert laisse une minuterie armée' ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
seance . finirCalcul ( ) ;
seance . finirCalcul ( ) ;
assert . equal ( seance . calculEnCours , false ) ;
seance . debuterCalcul ( ) ;
await planificateur . avancer ( 2 * INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'ecriture' , 'le calcul fini de trop a compté contre le suivant' ) ;
seance . finirCalcul ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( 'chaque geste achevé relance le compte ; le délai se règle' , async ( ) => {
const montage = monter ( { inactiviteMs : 5000 } ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
await planificateur . avancer ( 4000 ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await planificateur . avancer ( 4000 ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : ' ' } } ) , 'NOM_REQUIS' ) ;
await planificateur . avancer ( 4000 ) ;
assert . equal ( seance . mode , 'ecriture' , 'un geste refusé est une activité' ) ;
await planificateur . avancer ( 1000 ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( 'passer en lecture annule le geste en cours et rend le verrou ; repassé en écriture, le compte court' , async ( ) => {
const montage = monter ( ) ;
const { seance , planificateur } = montage ;
await enEcriture ( montage ) ;
seance . debuterGeste ( ) ;
await seance . passerEnLecture ( ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( montage . fs ) , null ) ;
assert . equal ( planificateur . actives , 0 ) ;
await seance . passerEnEcriture ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
} ) ;
describe ( 'séance : un geste, une entrée, un état (§ 2.1, § 8.2, § 10.1)' , ( ) => {
test ( "un geste écrit le journal puis l'état ; son libellé reste figé quand on renomme la personne" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const avant = fs . compteurs . ecritures ;
const rendu = await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ombrelle' , prenom : 'Iris' } } ) ;
assert . deepEqual ( rendu , { libelle : 'Ajouté Iris Ombrelle' , avertissements : [ ] } ) ;
assert . equal ( fs . compteurs . ecritures - avant , 3 , "la ligne, le .precedent, l'état" ) ;
await seance . executer ( 'modifierParticipant' , { id : 1 , champs : { nom : 'Parasol' } } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Ajouté Iris Ombrelle' , 'Modifié Iris Ombrelle : nom (désormais Iris Parasol)' ] ) ;
assert . deepEqual ( etatDe ( fs ) . charge , reconstruire ( journal , 3 ) ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 3 ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Parasol' ] ) ;
} ) ;
test ( "deux gestes lancés sans attendre écrivent deux entrées, dans l'ordre" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
const premier = montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const second = montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
await Promise . all ( [ premier , second ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( montage . fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
assert . deepEqual ( montage . seance . charge . participants . map ( ( { id , nom } ) => [ id , nom ] ) , [ [ 1 , 'Ancolie' ] , [ 2 , 'Bruyère' ] ] ) ;
} ) ;
test ( "un geste sans effet, ou refusé par sa commande, n'écrit rien" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . executer ( 'modifierParticipant' , { id : 1 , champs : { nom : 'Ancolie' } } ) , 'SANS_EFFET' ) ;
await refus ( seance . executer ( 'exclureParticipant' , { id : 7 } ) , 'PARTICIPANT_INCONNU' ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'AUCUN_RETENU' ) ;
await assert . rejects ( seance . executer ( 'commandeInventee' , { } ) , TypeError ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "nommer l'instant courant pose un jalon, sans entrée" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
const avant = montage . fs . compteurs . ecritures ;
await montage . seance . nommer ( ' Avant le dessert ' ) ;
assert . equal ( montage . fs . compteurs . ecritures - avant , 1 ) ;
const journal = journalDe ( montage . fs ) ;
assert . equal ( journal . entrees . length , 2 ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) , { type : 'jalon' , revision : 2 , nom : 'Avant le dessert' , horodatage : journal . jalons . at ( - 1 ) . horodatage } ) ;
await refus ( montage . seance . nommer ( ' ' ) , 'NOM_REQUIS' ) ;
} ) ;
test ( "un import est une seule entrée ; il n'avertit pas en brouillon, et porte l'avertissement de dérive en retenu (§ 10.1)" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const csv = 'nom;prenom\nAncolie;Bastien\nBruyère;Capucine\nCerfeuil;Damien\n' ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await seance . executer ( 'importerParticipants' , { apercu : apercevoir ( csv , { participants : seance . charge . participants } ) , mode : 'ajouter' } ) ;
assert . equal ( rendu . libelle , 'Importé 3 participants' ) ;
assert . deepEqual ( rendu . avertissements , [ ] ) ;
assert . equal ( rendu . resume . ajoutes , 3 ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees + 1 ) ;
assert . equal ( seance . charge . participants . length , 7 ) ;
await seance . executer ( 'supprimerParticipant' , { id : 7 } ) ;
await seance . executer ( 'supprimerParticipant' , { id : 6 } ) ;
await seance . executer ( 'supprimerParticipant' , { id : 5 } ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
const plus = apercevoir ( 'nom\nEpicéa\n' , { participants : seance . charge . participants } ) ;
const enRetenu = await seance . executer ( 'importerParticipants' , { apercu : plus , mode : 'ajouter' } ) ;
assert . deepEqual ( enRetenu . avertissements , [ { code : 'IMPORT_EN_RETENU' , details : { } } ] ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
// La séance montre aussi la dérive que l'Ouvert écrit porte désormais.
assert . deepEqual ( seance . avertissements , [ ... seance . ouvert . avertissements , ... enRetenu . avertissements ] ) ;
assert . deepEqual ( seance . ouvert . avertissements . map ( ( { code } ) => code ) , [ 'DERIVE' ] ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
} ) ;
test ( "importer sur un plan bloqué est refusé, et le texte du refus nomme « Débloquer »" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const avant = fs . compteurs . ecritures ;
const apercu = apercevoir ( 'nom\nAncolie\n' , { participants : [ ] } ) ;
const erreur = await refus ( seance . executer ( 'importerParticipants' , { apercu , mode : 'ajouter' } ) , 'LECTURE_SEULE' , { raison : 'BLOQUE' } ) ;
assert . match ( libelle ( erreur . code , erreur . details ) , /« Débloquer »/ ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
test ( "exclure une personne réservée suspend ses réservations, son titre n'est plus pourvu, et l'avertissement le dit ; la réintégrer les rend (§ 4.4)" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 1 } ) ;
assert . deepEqual ( exclusion , {
libelle : 'Exclu Iris Ombrelle' ,
avertissements : [ { code : 'RESERVATIONS_SUSPENDUES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ] ,
} ) ;
assert . deepEqual ( [ seance . charge . participants [ 0 ] . exclu , seance . charge . reservations . length ] , [ true , 1 ] ) ;
assert . deepEqual ( seance . avertissements , exclusion . avertissements ) ;
const retour = await seance . executer ( 'reintegrerParticipant' , { id : 1 } ) ;
assert . deepEqual ( retour . avertissements , [
{ code : 'RESERVATIONS_RETABLIES' , details : { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } } ,
] ) ;
assert . equal ( seance . charge . participants [ 0 ] . exclu , false ) ;
} ) ;
} ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
describe ( "séance : ce qu'un import dit, et ce que la séance montre (§ 4.4, § 9, § 10.1)" , ( ) => {
// L'événement d'essai, ouvert depuis le disque, en écriture.
async function essaiEnEcriture ( ) {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
await ouvrirSansEcrire ( montage ) ;
await montage . seance . passerEnEcriture ( ) ;
return montage ;
}
const mettreAJour = ( seance , csv ) =>
seance . executer ( 'importerParticipants' , {
apercu : apercevoir ( csv , { participants : seance . charge . participants , mode : 'mettreAJour' } ) ,
mode : 'mettreAJour' ,
} ) ;
test ( "un import qui exclut l'animatrice réservée dit ses réservations suspendues et son titre non pourvu, comme exclure (§ 4.4)" , async ( ) => {
const { seance } = await essaiEnEcriture ( ) ;
const rendu = await mettreAJour ( seance , 'nom;prenom;exclu\nOmbrelle;Iris;oui\n' ) ;
const suspendues = { participant : 1 , personne : 'Iris Ombrelle' , reservations : 1 , titres : 1 } ;
assert . equal ( rendu . libelle , 'Mis à jour la liste par import : 0 ajouté, 1 modifié' ) ;
assert . deepEqual ( rendu . avertissements , [ { code : 'RESERVATIONS_SUSPENDUES' , details : suspendues } ] ) ;
assert . equal ( seance . charge . participants [ 0 ] . exclu , true ) ;
const retour = await mettreAJour ( seance , 'nom;prenom;exclu\nOmbrelle;Iris;non\n' ) ;
assert . deepEqual ( retour . avertissements , [ { code : 'RESERVATIONS_RETABLIES' , details : suspendues } ] ) ;
} ) ;
test ( "un import dont les lignes valides ne changent rien, mais qui refuse des lignes, n'écrit rien et rend son résumé" , async ( ) => {
const { fs , seance } = await essaiEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Lacasse' , prenom : 'Ondine' } } ) ;
const avant = fs . compteurs . ecritures ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await mettreAJour ( seance , 'nom;prenom;courriel\nLacasse;Ondine;o@exemple.test\nPervenche;Théo;\n' ) ;
assert . equal ( rendu . libelle , null ) ;
assert . deepEqual ( rendu . avertissements , [ ] ) ;
assert . deepEqual ( rendu . resume . refusees . map ( ( { ligne , code } ) => [ ligne , code ] ) , [ [ 2 , 'PLUSIEURS_CORRESPONDENT' ] ] ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
} ) ;
test ( "après un geste, la séance montre les avertissements de l'Ouvert écrit : la dérive paraît et s'en va (§ 9)" , async ( ) => {
const { seance } = await essaiEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
const exclusion = await seance . executer ( 'exclureParticipant' , { id : 2 } ) ;
assert . deepEqual ( exclusion . avertissements , [ ] ) ;
assert . deepEqual ( seance . avertissements . map ( ( { code } ) => code ) , [ 'DERIVE' ] ) ;
assert . deepEqual ( seance . avertissements , seance . ouvert . avertissements ) ;
assert . equal ( seance . avertissements [ 0 ] . details . retenu , true ) ;
await seance . executer ( 'reintegrerParticipant' , { id : 2 } ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
} ) ;
test ( "le journal incomplet de l'ouverture ne s'affiche plus une fois la reprise écrite" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const plus = chargeEssai ( ) ;
plus . participants . push ( personne ( 5 , 'Ancolie' ) ) ;
plus . prochainsIds . participant = 6 ;
fs . deposer ( 'documents' , ETAT , serialiser ( plus , { revision : 4 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
const incomplet = [ { code : 'JOURNAL_INCOMPLET' , details : { etat : 4 , journal : 1 } } ] ;
assert . deepEqual ( seance . avertissements , incomplet ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . avertissements , incomplet ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
assert . deepEqual ( seance . avertissements , [ ] ) ;
} ) ;
} ) ;
describe ( 'séance : la forme nommée se lit sans rien écrire (§ 8.9, point 4)' , ( ) => {
test ( "la forme nommée d'une proposition ou du retenu se lit en lecture, sans entrée ni écriture" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 2 } ) ;
await seance . passerEnLecture ( ) ;
const avant = fs . compteurs . ecritures ;
const entrees = journalDe ( fs ) . entrees . length ;
const proposition = await seance . formeNommee ( { proposition : 1 } ) ;
assert . deepEqual ( proposition . fautes , [ ] ) ;
assert . deepEqual (
proposition . lignes . filter ( ( { tour } ) => tour === 1 ) . map ( ( { nom } ) => nom ) . sort ( ) ,
[ 'Grisaille' , 'Lacasse' , 'Ombrelle' , 'Pervenche' ] ,
) ;
const retenu = await seance . formeNommee ( { retenu : true } ) ;
assert . equal ( retenu . lignes . length , 8 ) ;
await refus ( seance . formeNommee ( { proposition : 7 } ) , 'PROPOSITION_INCONNUE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
assert . equal ( seance . mode , 'lecture' ) ;
} ) ;
test ( "un retenu fautif sur le disque se lit sous forme nommée par la séance : ses lignes et ses fautes (RETENU_FAUTIF, § 8.9)" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
await seance . passerEnLecture ( ) ;
const { entete , charge } = etatDe ( fs ) ;
const premier = charge . retenu . placement [ 0 ] ;
const assis = premier . sieges . find ( ( liste ) => liste . length > 0 ) [ 0 ] ;
premier . reserve = [ ... premier . reserve , assis ] . sort ( ( a , b ) => a - b ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( charge , { revision : entete . revision , produitVersion : V1 } ) ) ;
await ouvrirSansEcrire ( montage ) ;
assert . equal ( seance . avertissements [ 0 ] . code , 'RETENU_FAUTIF' ) ;
const lue = await seance . formeNommee ( { retenu : true } ) ;
assert . deepEqual ( lue . fautes , [ { code : 'DOUBLON' , tour : 1 , participant : assis } ] ) ;
assert . equal ( lue . lignes . length , 9 ) ;
assert . equal ( lue . lignes . filter ( ( { tour , participant } ) => tour === 1 && participant === assis ) . length , 2 ) ;
} ) ;
test ( 'sans événement ouvert, la forme nommée est refusée AUCUN_EVENEMENT' , async ( ) => {
await refus ( monter ( ) . seance . formeNommee ( { retenu : true } ) , 'AUCUN_EVENEMENT' ) ;
} ) ;
} ) ;
describe ( "séance : une autre séance a écrit entre-temps (§ 8.2, § 8.8)" , ( ) => {
// Deux processus d'un même poste sur un même disque : la première séance
// crée l'événement d'essai, puis la seconde l'ouvre en lecture.
async function deuxSeances ( ) {
const disque = new Map ( ) ;
const premiere = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'premiere' } ) ;
await premiere . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
const seconde = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( seconde ) ;
return { premiere : premiere . seance , seconde : seconde . seance , fs : seconde . fs , fsPremiere : premiere . fs , disque } ;
}
test ( "passer en écriture relit l'événement : le travail de l'autre séance est adopté, EVENEMENT_CHANGE le dit, et le geste suivant s'écrit à sa suite" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await premiere . passerEnLecture ( ) ;
const change = [ { code : 'EVENEMENT_CHANGE' , details : { revision : 2 } } ] ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : change } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) . seance ] , [ 'ecriture' , seconde . id ] ) ;
assert . deepEqual ( noms ( seconde . charge ) , [ 'Xavier' ] ) ;
assert . deepEqual ( seconde . avertissements , change ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Ajouté Xavier' , 'Ajouté Yvette' ] ) ;
assert . equal ( journal . ecartees , 0 ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' , 'Yvette' ] ) ;
assert . deepEqual ( etatDe ( fs ) . charge , reconstruire ( journal , 3 ) ) ;
} ) ;
test ( "sans geste de l'autre séance, rien n'avertit ; son jalon est adopté avec le journal relu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . nommer ( 'Avant les invitations' ) ;
await premiere . passerEnLecture ( ) ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( seconde . ouvert . journal . jalons . map ( ( { nom } ) => nom ) , [ 'Création' , 'Avant les invitations' ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
assert . deepEqual ( journalDe ( fs ) . jalons . map ( ( { nom } ) => nom ) , [ 'Création' , 'Avant les invitations' ] ) ;
} ) ;
test ( "un événement qui a quitté le disque entre-temps, renommé ailleurs : le passage en écriture est refusé, le verrou rendu, rien ne se recrée" , async ( ) => {
const { seconde , fs , disque } = await deuxSeances ( ) ;
const autre = creerFichiersSimules ( { disque } ) ;
for ( const fichier of [ ETAT , JOURNAL ] ) await autre . supprimer ( DOC , fichier ) ;
await refus ( seconde . passerEnEcriture ( ) , 'ABSENT' ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) , [ ] ) ;
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "un plan que l'autre séance a bloqué entre-temps : adopté, EVENEMENT_CHANGE le dit, et le passage est refusé BLOQUE, le verrou rendu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , { raison : 'BLOQUE' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . charge . evenement . etat ] , [ 'lecture' , null , 'bloque' ] ) ;
assert . deepEqual ( seconde . avertissements , [ { code : 'EVENEMENT_CHANGE' , details : { revision : 2 } } ] ) ;
} ) ;
test ( "débloquer relit l'événement sous le verrou : le déblocage s'écrit à la suite du travail de l'autre séance" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
await premiere . debloquer ( ) ;
await premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( premiere . mode , 'lecture' ) ;
const rendu = await seconde . debloquer ( ) ;
assert . deepEqual ( rendu , {
libelle : 'Plan débloqué, état « Brouillon »' ,
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 5 } } ] ,
} ) ;
assert . equal ( seconde . mode , 'ecriture' ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( journal . entrees . map ( ( { revision } ) => revision ) , [ 1 , 2 , 3 , 4 , 5 , 6 ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' ] ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'brouillon' ) ;
} ) ;
test ( "débloquer un plan que l'autre séance a débloqué entre-temps : refusé PLAN_NON_BLOQUE, adopté, le verrou rendu" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
await premiere . debloquer ( ) ;
await premiere . passerEnLecture ( ) ;
const entrees = journalDe ( fs ) . entrees . length ;
await refus ( seconde . debloquer ( ) , 'PLAN_NON_BLOQUE' ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . charge . evenement . etat ] , [ 'lecture' , null , 'brouillon' ] ) ;
assert . deepEqual ( seconde . avertissements , [ { code : 'EVENEMENT_CHANGE' , details : { revision : 3 } } ] ) ;
assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
} ) ;
test ( "un secours ouvert se relit par la même source : passer en écriture ne bute pas sur l'état illisible" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Xavier' } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'journal' ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Yvette' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Xavier' , 'Yvette' ] ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
} ) ;
// L'événement de la première séance porte Aubépine puis Bourrache, son
// état devient illisible, et chacune des deux ouvre son .precedent : la
// révision 2, Aubépine seule.
async function surPrecedent ( ) {
const deux = await deuxSeances ( ) ;
const { premiere , seconde , fs } = deux ;
await premiere . passerEnEcriture ( ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await premiere . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await premiere . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
for ( const seance of [ premiere , seconde ] ) await seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . deepEqual ( [ seconde . ouvert . revision , noms ( seconde . charge ) ] , [ 2 , [ 'Aubépine' ] ] ) ;
return deux ;
}
for ( const gestes of [ [ 'Cerfeuil' ] , [ 'Cerfeuil' , 'Dictame' ] ] ) {
test ( ` un secours .precedent se relit par l'état qu'une autre séance a écrit depuis ( ${ gestes . length } geste·s) : adopté, EVENEMENT_CHANGE le dit, rien ne s'efface ` , async ( ) => {
const { premiere , seconde , fs } = await surPrecedent ( ) ;
await premiere . passerEnEcriture ( ) ;
for ( const nom of gestes ) await premiere . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await premiere . passerEnLecture ( ) ;
const { revision } = etatDe ( fs ) . entete ;
const change = [ { code : 'EVENEMENT_CHANGE' , details : { revision } } ] ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , { avertissements : change } ) ;
assert . deepEqual ( [ seconde . ouvert . revision , noms ( seconde . charge ) ] , [ revision , [ 'Aubépine' , ... gestes ] ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Estragon' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , ... gestes , 'Estragon' ] ) ;
assert . equal ( journalDe ( fs ) . ecartees , 0 ) ;
} ) ;
}
test ( "un secours .precedent dont l'état reste illisible, mais dont le journal a reçu le geste d'une autre séance : le dernier instant du journal est adopté, EVENEMENT_CHANGE le dit" , async ( ) => {
const { premiere , seconde , fs , fsPremiere } = await surPrecedent ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . nommer ( 'Point' ) ;
fsPremiere . pannes . renommage ( 'documents' ) ;
await refus ( premiere . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'ETAT_NON_ECRIT' ) ;
await premiere . passerEnLecture ( ) ;
assert . equal ( fs . contenu ( 'documents' , ETAT ) , '' ) ;
assert . deepEqual ( await seconde . passerEnEcriture ( ) , {
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 5 } } ] ,
} ) ;
assert . deepEqual ( noms ( seconde . charge ) , [ 'Aubépine' , 'Cerfeuil' ] ) ;
await seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Estragon' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Cerfeuil' , 'Estragon' ] ) ;
} ) ;
test ( "un secours .precedent dont l'état redevient lisible entre-temps se relit par l'état, sans entrée nouvelle au journal : EVENEMENT_CHANGE le dit" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
const lisible = fs . contenu ( 'documents' , ETAT ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
fs . deposer ( 'documents' , ETAT , lisible ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , {
avertissements : [ { code : 'EVENEMENT_CHANGE' , details : { revision : 3 } } ] ,
} ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Aubépine' , 'Bourrache' ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Bourrache' , 'Cerfeuil' ] ) ;
} ) ;
test ( "un secours .precedent ouvert devant un état lisible se relit par le .precedent tant que l'état ne change pas : le choix de l'opérateur reste (§ 8.9, point 3)" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 2 , [ 'Aubépine' ] ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Aubépine' , 'Cerfeuil' ] ) ;
} ) ;
test ( "après un geste sur un secours .precedent, la séance se relit par l'état : redevenu illisible, il refuse le passage en écriture, sans revenir au .precedent" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await refus ( seance . passerEnEcriture ( ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs ) , noms ( seance . charge ) ] , [ 'lecture' , null , [ 'Aubépine' , 'Cerfeuil' ] ] ) ;
} ) ;
test ( "après avoir écrit ailleurs un secours .precedent, la séance s'y relit par l'état, que le dossier nouveau porte seul" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'portable' ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'ECRITURE' ) ;
await seance . ecrireAilleurs ( PORTABLE ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . mode , noms ( seance . charge ) ] , [ 'ecriture' , [ 'Aubépine' ] ] ) ;
} ) ;
test ( "le jalon d'une séance seule sur un secours .precedent pose la reprise : la relecture la retrouve, sans EVENEMENT_CHANGE ni recul de révision" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , '' ) ;
await seance . ouvrirSecours ( BASE , 'precedent' ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( await seance . nommer ( 'Point' ) , { revision : 4 } ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 4 , [ 'Aubépine' ] ] ) ;
const codes = seance . avertissements . map ( ( { code } ) => code ) ;
assert . ok ( ! codes . includes ( 'JOURNAL_EN_AVANCE_DE' ) , codes . join ( ) ) ;
} ) ;
test ( "le jalon d'une séance seule sur un état que le journal devance de deux entrées pose la reprise : la relecture la retrouve, sans EVENEMENT_CHANGE" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const premier = fs . contenu ( 'documents' , ETAT ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
fs . deposer ( 'documents' , ETAT , premier ) ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( await seance . nommer ( 'Point' ) , { revision : 4 } ) ;
await seance . passerEnLecture ( ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
assert . deepEqual ( [ seance . ouvert . revision , noms ( seance . charge ) ] , [ 4 , [ ] ] ) ;
} ) ;
// L'événement réécrit par une construction plus récente : l'état, ou la
// première ligne du journal, déclarent le format 2.
const plusRecent = {
etat : ( fs ) => fs . deposer ( 'documents' , ETAT , fs . contenu ( 'documents' , ETAT ) . replace ( '"format":1' , '"format":2' ) ) ,
journal : ( fs ) => fs . deposer ( 'documents' , JOURNAL , fs . contenu ( 'documents' , JOURNAL ) . replace ( '"format":1' , '"format":2' ) ) ,
} ;
for ( const [ fichier , designe ] of [ [ 'etat' , "l'état" ] , [ 'journal' , 'le journal' ] ] ) {
test ( ` ${ designe } qu'une construction plus récente a réécrit entre-temps : passer en écriture est refusé LECTURE_SEULE FORMAT, le verrou rendu ` , async ( ) => {
const { seconde , fs } = await deuxSeances ( ) ;
const entrees = journalDe ( fs ) . entrees . length ;
plusRecent [ fichier ] ( fs ) ;
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , { raison : 'FORMAT' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'LECTURE_SEULE' , { raison : 'FORMAT' } ) ;
if ( fichier === 'etat' ) assert . equal ( journalDe ( fs ) . entrees . length , entrees ) ;
} ) ;
test ( ` un secours .precedent dont ${ designe } est réécrit entre-temps par une construction plus récente : passer en écriture est refusé LECTURE_SEULE FORMAT, le verrou rendu ` , async ( ) => {
const { seconde , fs } = await surPrecedent ( ) ;
if ( fichier === 'etat' ) {
const precedent = fs . contenu ( 'documents' , ` ${ BASE } ${ SUFFIXES . precedent } ` ) ;
fs . deposer ( 'documents' , ETAT , precedent . replace ( '"format":1' , '"format":2' ) ) ;
} else {
plusRecent . journal ( fs ) ;
}
await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , { raison : 'FORMAT' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
} ) ;
test ( ` un plan bloqué dont ${ designe } est réécrit entre-temps par une construction plus récente : débloquer est refusé LECTURE_SEULE FORMAT, le verrou rendu, sans entrée ` , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seconde . ouvrir ( BASE ) ;
const texte = fs . contenu ( 'documents' , JOURNAL ) ;
plusRecent [ fichier ] ( fs ) ;
await refus ( seconde . debloquer ( ) , 'LECTURE_SEULE' , { raison : 'FORMAT' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) , seconde . ouvert . formatPlusRecent ] , [ 'lecture' , null , true ] ) ;
if ( fichier === 'etat' ) assert . equal ( fs . contenu ( 'documents' , JOURNAL ) , texte ) ;
} ) ;
}
// L'état réécrit en octets hors UTF-8 : son premier « é » remplacé par
// l'octet E9 d'un éditeur réglé en windows-1252. Rend ces octets.
function etatMalEncode ( fs ) {
const octets = [ ... new TextEncoder ( ) . encode ( fs . contenu ( 'documents' , ETAT ) ) ] ;
const rang = octets . findIndex ( ( octet , i ) => octet === 0xc3 && octets [ i + 1 ] === 0xa9 ) ;
octets . splice ( rang , 2 , 0xe9 ) ;
fs . deposerOctets ( 'documents' , ETAT , new Uint8Array ( octets ) ) ;
return new Uint8Array ( octets ) ;
}
// Les noms des fichiers de documents et le texte du journal : ce qu'un
// refus laisse tel quel.
const empreinte = async ( fs ) => [ ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) , fs . contenu ( 'documents' , JOURNAL ) ] ;
test ( "l'état réécrit entre-temps en octets hors UTF-8 : passer en écriture est refusé LECTURE_SEULE {raison: 'ENCODAGE'}, comme un format plus récent, le verrou rendu et rien de l'événement écrit ; le refus a son texte" , async ( ) => {
const { seconde , fs } = await deuxSeances ( ) ;
const octets = etatMalEncode ( fs ) ;
const avant = await empreinte ( fs ) ;
const erreur = await refus ( seconde . passerEnEcriture ( ) , 'LECTURE_SEULE' , { raison : 'ENCODAGE' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( await empreinte ( fs ) , avant ) ;
assert . deepEqual ( fs . octets ( 'documents' , ETAT ) , octets ) ;
assert . match ( libelle ( erreur . code , erreur . details ) , /UTF-8/ ) ;
await refus ( seconde . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) , 'MODE_LECTURE' ) ;
} ) ;
test ( "un plan bloqué dont l'état est réécrit entre-temps en octets hors UTF-8 : débloquer est refusé LECTURE_SEULE {raison: 'ENCODAGE'}, le verrou rendu, sans entrée" , async ( ) => {
const { premiere , seconde , fs } = await deuxSeances ( ) ;
await premiere . passerEnEcriture ( ) ;
await premiere . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await premiere . passerEnLecture ( ) ;
await seconde . ouvrir ( BASE ) ;
const octets = etatMalEncode ( fs ) ;
const avant = await empreinte ( fs ) ;
await refus ( seconde . debloquer ( ) , 'LECTURE_SEULE' , { raison : 'ENCODAGE' } ) ;
assert . deepEqual ( [ seconde . mode , verrouDe ( fs ) ] , [ 'lecture' , null ] ) ;
assert . deepEqual ( await empreinte ( fs ) , avant ) ;
assert . deepEqual ( fs . octets ( 'documents' , ETAT ) , octets ) ;
} ) ;
for ( const source of [ 'precedent' , 'journal' ] ) {
test ( ` ouvert par son secours ${ source } , un état aux octets hors UTF-8 ne fige rien : passer en écriture réussit, et le premier geste met l'état dans un dossier daté de la corbeille, ses octets intacts, avant d'écrire ` , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Aubépine' , 'Bourrache' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . passerEnLecture ( ) ;
const octets = etatMalEncode ( fs ) ;
const illisible = await refus ( seance . ouvrir ( BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . equal ( illisible . details . raison , 'ENCODAGE' ) ;
assert . match ( libelle ( illisible . code , illisible . details ) , /UTF-8/ ) ;
await seance . ouvrirSecours ( BASE , source ) ;
assert . equal ( seance . ouvert . etatMalEncode , true ) ;
assert . deepEqual ( await seance . passerEnEcriture ( ) , { avertissements : [ ] } ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Cerfeuil' } } ) ;
const [ dossier ] = ( await fs . lister ( DOC , 'corbeille' ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( fs . octets ( 'documents' , ` corbeille/ ${ dossier } / ${ ETAT } ` ) , octets ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ ... noms ( seance . charge ) ] ) ;
assert . ok ( noms ( seance . charge ) . includes ( 'Cerfeuil' ) ) ;
assert . equal ( seance . ouvert . etatMalEncode , false ) ;
for ( const { code , details } of seance . avertissements ) assert . equal ( typeof libelle ( code , details ) , 'string' ) ;
} ) ;
}
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
describe ( 'séance : revenir, défaire, refaire (§ 8.3)' , ( ) => {
// Trois gestes après la création : Ancolie, Bruyère, Capucine, aux
// révisions 2, 3 et 4.
async function troisGestes ( ) {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Ancolie' , 'Bruyère' , 'Capucine' ] ) await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
return montage ;
}
test ( 'défaire, refaire et revenir écrivent des entrées de retour, et le journal ne se tronque jamais' , async ( ) => {
const montage = await troisGestes ( ) ;
const { fs , seance } = montage ;
assert . deepEqual ( await seance . defaire ( ) , { libelle : 'Défait : « Ajouté Capucine »' , avertissements : [ ] } ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' , 'Capucine' ] ) ;
await seance . revenir ( 2 ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual (
journal . entrees . map ( ( { revision , sens , retour , libelle : texte } ) => [ revision , sens , retour , texte ] ) ,
[
[ 1 , null , null , CREATION ] ,
[ 2 , null , null , 'Ajouté Ancolie' ] ,
[ 3 , null , null , 'Ajouté Bruyère' ] ,
[ 4 , null , null , 'Ajouté Capucine' ] ,
[ 5 , 'defaire' , 3 , 'Défait : « Ajouté Capucine »' ] ,
[ 6 , 'refaire' , 4 , 'Refait : « Ajouté Capucine »' ] ,
[ 7 , 'revenir' , 2 , 'Retour à « Ajouté Ancolie »' ] ,
] ,
) ;
2026-10-06 23:40:40 -04:00
// L'instant 2 revient avec son contenu, et ses compteurs restent où trois
// ajouts les ont portés : aucun identifiant donné ne se redonne (§ 8.9).
const instant2 = reconstruire ( journal , 2 ) ;
const attendu = { ... instant2 , prochainsIds : { ... instant2 . prochainsIds , participant : 4 } } ;
assert . deepEqual ( etatDe ( fs ) . charge , attendu ) ;
assert . deepEqual ( reconstruire ( journal , 7 ) , attendu ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Digitale' } } ) ;
assert . deepEqual (
seance . charge . participants . map ( ( { id , nom } ) => [ id , nom ] ) ,
[ [ 1 , 'Ancolie' ] , [ 4 , 'Digitale' ] ] ,
) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
} ) ;
test ( "refaire n'existe qu'après des retours, tant qu'aucun geste ne les suit" , async ( ) => {
const montage = await troisGestes ( ) ;
const { fs , seance } = montage ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . refaire ( ) , 'RIEN_A_REFAIRE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
await seance . defaire ( ) ;
await seance . defaire ( ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : true } ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Dauphinelle' } } ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
await refus ( seance . refaire ( ) , 'RIEN_A_REFAIRE' ) ;
} ) ;
test ( "rien à défaire au premier instant" , async ( ) => {
const montage = monter ( ) ;
await enEcriture ( montage ) ;
assert . deepEqual ( montage . seance . retours , { defaire : false , refaire : false } ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
test ( 'après un retour de la révision 6 à la 3, défaire vise le prédécesseur de 3 sur le fil, non la ligne précédente' , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
for ( const nom of [ 'Ancolie' , 'Bruyère' , 'Capucine' , 'Dauphinelle' , 'Églantine' ] ) await seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) ;
await seance . revenir ( 3 ) ;
await seance . defaire ( ) ;
const derniere = journalDe ( fs ) . entrees . at ( - 1 ) ;
assert . deepEqual ( [ derniere . revision , derniere . sens , derniere . retour , derniere . libelle ] , [ 8 , 'defaire' , 2 , 'Défait : « Ajouté Bruyère »' ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
} ) ;
test ( "un journal qui finit avant l'état : défaire vise l'instant où il finit, par-dessus l'entrée de reprise" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
// L'état du disque, à la révision 4, porte une personne que le journal,
// arrêté à la révision 1, n'a pas.
const plus = chargeEssai ( ) ;
plus . participants . push ( personne ( 5 , 'Ancolie' ) ) ;
plus . prochainsIds . participant = 6 ;
fs . deposer ( 'documents' , ETAT , serialiser ( plus , { revision : 4 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
assert . deepEqual ( seance . ouvert . avertissements , [ { code : 'JOURNAL_INCOMPLET' , details : { etat : 4 , journal : 1 } } ] ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
const reprise = "Reprise de l'état trouvé sur le disque" ;
assert . deepEqual ( await seance . defaire ( ) , { libelle : ` Défait : « ${ reprise } » ` , avertissements : [ ] } ) ;
assert . deepEqual (
journalDe ( fs ) . entrees . map ( ( { revision , sens , retour , libelle : texte } ) => [ revision , sens , retour , texte ] ) ,
[
[ 1 , null , null , CREATION ] ,
[ 2 , null , null , reprise ] ,
[ 3 , 'defaire' , 1 , ` Défait : « ${ reprise } » ` ] ,
] ,
) ;
assert . deepEqual ( noms ( seance . charge ) , noms ( chargeEssai ( ) ) ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : true } ) ;
await seance . refaire ( ) ;
assert . deepEqual ( noms ( seance . charge ) , noms ( plus ) ) ;
} ) ;
test ( "un état sans journal : rien à défaire ; le premier geste ouvre le journal par l'entrée de reprise" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( chargeEssai ( ) , { revision : 3 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
assert . deepEqual ( seance . retours , { defaire : false , refaire : false } ) ;
await refus ( seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
assert . deepEqual (
journalDe ( fs ) . entrees . map ( ( { revision , libelle : texte } ) => [ revision , texte ] ) ,
[
[ 3 , "Reprise de l'état trouvé sur le disque" ] ,
[ 4 , 'Ajouté Ancolie' ] ,
] ,
) ;
assert . deepEqual ( seance . retours , { defaire : true , refaire : false } ) ;
} ) ;
test ( "revenir à un instant que le journal ne restitue plus est refusé, sans écriture" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const charge = chargeEssai ( ) ;
// Un journal élagué : sa première entrée est la révision 10.
const lignes = [
ligneOuverture ( charge . evenement . id ) ,
ligneEntree ( { revision : 10 , libelle : 'Geste 10' , horodatage : '2031-05-17T07:00:00-04:00' , produitVersion : V1 , apres : charge , instantane : true } ) ,
] ;
fs . deposer ( 'documents' , JOURNAL , ` ${ lignes . join ( '\n' ) } \n ` ) ;
fs . deposer ( 'documents' , ETAT , serialiser ( charge , { revision : 10 , produitVersion : V1 } ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . revenir ( 3 ) , 'INSTANT_NON_RESTITUABLE' , { revision : 3 } ) ;
await refus ( seance . revenir ( 11 ) , 'INSTANT_NON_RESTITUABLE' , { revision : 11 } ) ;
await refus ( seance . defaire ( ) , 'RIEN_A_DEFAIRE' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
} ) ;
} ) ;
describe ( 'séance : les jalons automatiques (§ 8.3)' , ( ) => {
test ( "la création, chaque changement d'état, et l'instant qui précède chaque génération sont nommés" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
await deposerEssai ( fs ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
await ouvrirSansEcrire ( montage ) ;
await seance . passerEnEcriture ( ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge ) ) ;
await seance . executer ( 'retenirProposition' , { id : 1 } ) ;
await seance . executer ( 'enregistrerGeneration' , generer ( seance . charge , 1 , 9 ) ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( libellesDe ( journal ) , [ CREATION , 'Généré 2 propositions (n° 1 à 2)' , 'Retenu la proposition n° 1' , 'Généré 1 proposition (n° 3)' ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [
[ 1 , 'Création' ] ,
[ 1 , 'Avant la génération' ] ,
[ 2 , 'État « Proposé »' ] ,
[ 3 , 'État « Retenu »' ] ,
[ 3 , 'Avant la génération' ] ,
] ) ;
} ) ;
test ( "un retour qui change l'état du plan pose le jalon qui le nomme : défaire un déblocage rebloque, et la séance revient en lecture" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seance . debloquer ( ) ;
const rendu = await seance . defaire ( ) ;
assert . deepEqual ( rendu , { libelle : 'Défait : « Plan débloqué, état « Brouillon » »' , avertissements : [ ] } ) ;
const journal = journalDe ( fs ) ;
const derniere = journal . entrees . at ( - 1 ) ;
assert . deepEqual ( [ derniere . revision , derniere . sens , derniere . retour ] , [ 4 , 'defaire' , 2 ] ) ;
assert . deepEqual ( journal . jalons . map ( ( { revision , nom } ) => [ revision , nom ] ) , [
[ 1 , 'Création' ] ,
[ 2 , 'État « Bloqué »' ] ,
[ 3 , 'État « Brouillon »' ] ,
[ 4 , 'État « Bloqué »' ] ,
] ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat , verrouDe ( fs ) ] , [ 'lecture' , 'bloque' , null ] ) ;
} ) ;
} ) ;
describe ( 'séance : bloquer et débloquer (§ 8.5, § 9)' , ( ) => {
test ( "bloquer écrit l'entrée et son jalon, puis la séance revient en lecture et rend le verrou" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
const rendu = await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( rendu . libelle , "Plan passé à l'état « Bloqué »" ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( verrouDe ( fs ) , null ) ;
assert . equal ( montage . planificateur . actives , 0 ) ;
const journal = journalDe ( fs ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) . nom , 'État « Bloqué »' ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'bloque' ) ;
} ) ;
test ( 'sur un plan bloqué : passer en écriture, un geste, défaire et nommer sont refusés BLOQUE, sans écriture' , async ( ) => {
const disque = new Map ( ) ;
const premier = monter ( { fs : creerFichiersSimules ( { disque } ) } ) ;
await enEcriture ( premier ) ;
await premier . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
await premier . seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const second = monter ( { fs : creerFichiersSimules ( { disque } ) , prefixe : 'seconde' } ) ;
await ouvrirSansEcrire ( second ) ;
for ( const { seance , fs } of [ premier , second ] ) {
const avant = fs . compteurs . ecritures ;
const bloque = { raison : 'BLOQUE' } ;
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . defaire ( ) , 'LECTURE_SEULE' , bloque ) ;
await refus ( seance . nommer ( 'Distribué' ) , 'LECTURE_SEULE' , bloque ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . equal ( seance . mode , 'lecture' ) ;
}
assert . match ( libelle ( 'LECTURE_SEULE' , { raison : 'BLOQUE' } ) , /« Débloquer »/ ) ;
} ) ;
test ( "débloquer prend le verrou, écrit une entrée et un jalon, rend l'état que le contenu impose, et laisse la séance en écriture" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const entrees = journalDe ( fs ) . entrees . length ;
const rendu = await seance . debloquer ( ) ;
assert . equal ( rendu . libelle , 'Plan débloqué, état « Brouillon »' ) ;
assert . equal ( seance . mode , 'ecriture' ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
assert . equal ( seance . charge . evenement . etat , etatDeduit ( seance . charge ) ) ;
const journal = journalDe ( fs ) ;
assert . equal ( journal . entrees . length , entrees + 1 ) ;
assert . deepEqual ( journal . jalons . at ( - 1 ) , { ... journal . jalons . at ( - 1 ) , revision : journal . entrees . at ( - 1 ) . revision , nom : 'État « Brouillon »' } ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
await seance . executer ( 'debloquer' , { } ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'ecriture' , 'brouillon' ] ) ;
} ) ;
test ( "débloquer devant le verrou d'un autre poste est refusé VERROU_PRIS, avant toute entrée" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
const depuis = '2031-05-17T09:00:00-04:00' ;
fs . deposer ( 'documents' , VERROU , ` ${ JSON . stringify ( { seance : 'lointaine-1' , pid : 99 , hote : 'autre-poste' , depuis } )} \n ` ) ;
const journal = fs . contenu ( 'documents' , JOURNAL ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . debloquer ( ) , 'VERROU_PRIS' , { seance : 'lointaine-1' , depuis , vivant : null } ) ;
assert . equal ( fs . compteurs . ecritures - avant , 1 , 'seule la prise du verrou se tente' ) ;
assert . equal ( fs . contenu ( 'documents' , JOURNAL ) , journal ) ;
assert . deepEqual ( [ seance . mode , seance . charge . evenement . etat ] , [ 'lecture' , 'bloque' ] ) ;
assert . equal ( verrouDe ( fs ) . seance , 'lointaine-1' ) ;
} ) ;
test ( "débloquer un plan qui ne l'est pas : en lecture MODE_LECTURE, en écriture PLAN_NON_BLOQUE" , async ( ) => {
const montage = monter ( ) ;
await montage . seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) ;
await refus ( montage . seance . debloquer ( ) , 'MODE_LECTURE' ) ;
await montage . seance . passerEnEcriture ( ) ;
const avant = montage . fs . compteurs . ecritures ;
await refus ( montage . seance . debloquer ( ) , 'PLAN_NON_BLOQUE' ) ;
assert . equal ( montage . fs . compteurs . ecritures , avant ) ;
} ) ;
} ) ;
describe ( "séance : un fichier d'un format plus récent (§ 8.8)" , ( ) => {
test ( "s'ouvre en lecture sans rien écrire, et tout ce qui écrirait est refusé LECTURE_SEULE FORMAT, avant BLOQUE" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' ) ;
const charge = chargeEssai ( ) ;
charge . evenement . etat = 'bloque' ;
fs . deposer ( 'documents' , ETAT , serialiser ( charge , { revision : 1 , produitVersion : V1 } ) . replace ( '"format":1' , '"format":2' ) ) ;
const { seance } = monter ( { fs } ) ;
const avant = fs . compteurs . ecritures ;
await seance . ouvrir ( BASE ) ;
assert . equal ( seance . ouvert . formatPlusRecent , true ) ;
const format = { raison : 'FORMAT' } ;
await refus ( seance . passerEnEcriture ( ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . executer ( 'changerEtat' , { etat : 'retenu' } ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . debloquer ( ) , 'LECTURE_SEULE' , format ) ;
await refus ( seance . defaire ( ) , 'LECTURE_SEULE' , format ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . doesNotMatch ( libelle ( 'LECTURE_SEULE' , format ) , /Débloquer/ ) ;
} ) ;
} ) ;
describe ( 'séance : la perte du support en cours de séance (§ 8.6)' , ( ) => {
// Un événement du disque portable, en écriture, après un premier geste.
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
async function surPortable ( reglages = { } ) {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' , ... reglages } ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
const montage = monter ( { fs , racine : PORTABLE } ) ;
await enEcriture ( montage ) ;
await montage . seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
return montage ;
}
test ( "une écriture refusée rompt le support : le geste est refusé, les suivants aussi, puis écrire ailleurs rétablit" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
const refusSupport = { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
assert . deepEqual ( seance . refusSupport , refusSupport ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'SUPPORT_ROMPU' , refusSupport ) ;
await refus ( seance . defaire ( ) , 'SUPPORT_ROMPU' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . match ( libelle ( 'SUPPORT_ROMPU' , refusSupport ) , /Écrire ailleurs/ ) ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport , seance . mode ] , [ false , null , 'ecriture' ] ) ;
assert . deepEqual ( seance . racine , DOC ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
} ) ;
test ( "un état qui ne s'écrit pas : le geste est au journal, la séance en montre la charge, et écrire ailleurs la garde" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . renommage ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport . echec ] , [ true , 'ETAT_NON_ECRIT' ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs , 'portable' ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' , 'Bruyère' ] ) ;
assert . equal ( seance . supportRompu , false ) ;
} ) ;
test ( "un jalon d'état qui ne s'écrit pas laisse le geste enregistré : JALON_NON_ECRIT le dit, et le support est rompu" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
// La deuxième ligne ajoutée au journal à partir d'ici, le jalon qui suit
// l'entrée, trouve le support en lecture seule.
const ajouterLigne = fs . ajouterLigne ;
let lignes = 0 ;
fs . ajouterLigne = async ( racine , chemin , ligne ) => {
lignes += 1 ;
if ( lignes === 2 ) fs . pannes . ecriture ( racine . id ) ;
return ajouterLigne ( racine , chemin , ligne ) ;
} ;
const refusSupport = { chemin : JOURNAL , dossier : DOCUMENTS , cause : 'EROFS' } ;
const rendu = await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . deepEqual ( rendu , {
libelle : "Plan passé à l'état « Bloqué »" ,
avertissements : [
{ code : 'JALON_NON_ECRIT' , details : refusSupport } ,
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : DOCUMENTS , cause : 'EROFS' } } ,
] ,
} ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport ] , [ true , { echec : 'JALON_NON_ECRIT' , ... refusSupport } ] ) ;
assert . equal ( seance . mode , 'lecture' ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'bloque' ) ;
const journal = journalDe ( fs ) ;
assert . equal ( journal . entrees . at ( - 1 ) . libelle , "Plan passé à l'état « Bloqué »" ) ;
assert . deepEqual ( journal . jalons . map ( ( { nom } ) => nom ) , [ 'Création' ] ) ;
} ) ;
test ( "une création dont l'état ne s'écrit pas reste ouverte en lecture, le support rompu, et s'écrit ailleurs" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
const montage = monter ( { fs , racine : PORTABLE } ) ;
const { seance } = montage ;
// La deuxième écriture atomique, l'état qui suit le journal, trouve le
// support en lecture seule.
const ecrireAtomique = fs . ecrireAtomique ;
let ecritures = 0 ;
fs . ecrireAtomique = async ( racine , chemin , texte ) => {
ecritures += 1 ;
if ( ecritures === 2 ) fs . pannes . ecriture ( racine . id ) ;
return ecrireAtomique ( racine , chemin , texte ) ;
} ;
await refus ( seance . creer ( { nom : BASE , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport . echec ] , [ 'lecture' , true , 'ETAT_NON_ECRIT' ] ) ;
assert . equal ( seance . charge . evenement . nom , BASE ) ;
assert . equal ( fs . contenu ( 'portable' , ETAT ) , null ) ;
const { base } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , verrouDe ( fs ) ] , [ false , 'lecture' , null ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION ] ) ;
assert . equal ( etatDe ( fs ) . entete . revision , 1 ) ;
} ) ;
test ( "un verrou que le support refuse rompt le support : « Modifier » est refusé, puis écrire ailleurs rétablit l'écriture" , async ( ) => {
const { fs , seance , planificateur } = await surPortable ( ) ;
await planificateur . avancer ( INACTIVITE _MS ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs , 'portable' ) ] , [ 'lecture' , null ] ) ;
fs . pannes . ecriture ( 'portable' ) ;
const details = { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . passerEnEcriture ( ) , 'ECRITURE' , details ) ;
const refusSupport = { echec : 'VERROU_NON_ECRIT' , ... details } ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . passerEnEcriture ( ) , 'SUPPORT_ROMPU' , refusSupport ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
const { base } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . racine ] , [ false , 'lecture' , DOC ] ) ;
await seance . passerEnEcriture ( ) ;
assert . equal ( verrouDe ( fs ) . seance , seance . id ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' , 'Ajouté Bruyère' ] ) ;
} ) ;
test ( "débloquer devant un verrou que le support refuse rompt aussi le support, et le plan se débloque ailleurs" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
await seance . executer ( 'changerEtat' , { etat : 'bloque' } ) ;
assert . equal ( seance . mode , 'lecture' ) ;
fs . pannes . ecriture ( 'portable' ) ;
const details = { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
await refus ( seance . debloquer ( ) , 'ECRITURE' , details ) ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , { echec : 'VERROU_NON_ECRIT' , ... details } ] ) ;
assert . equal ( seance . charge . evenement . etat , 'bloque' ) ;
await seance . ecrireAilleurs ( DOC ) ;
const rendu = await seance . debloquer ( ) ;
assert . equal ( rendu . libelle , 'Plan débloqué, état « Brouillon »' ) ;
assert . deepEqual ( [ seance . mode , verrouDe ( fs ) ? . seance ] , [ 'ecriture' , seance . id ] ) ;
assert . equal ( etatDe ( fs ) . charge . evenement . etat , 'brouillon' ) ;
} ) ;
test ( "écrire ailleurs dont le verrou ne s'écrit pas là-bas : la paire y est, la séance y passe en lecture, son support de nouveau rompu" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
// Les Documents prennent la paire, mais refusent le verrou.
const details = { chemin : VERROU , dossier : DOCUMENTS , cause : 'EACCES' } ;
const verrouiller = fs . verrouiller ;
fs . verrouiller = async ( racine , chemin , jeton ) => {
if ( racine . id === 'documents' ) throw new ErreurStockage ( 'ECRITURE' , details ) ;
return verrouiller ( racine , chemin , jeton ) ;
} ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements . map ( ( { code } ) => code ) , [ 'VERROU_NON_RENDU' , 'ECRITURE' ] ) ;
const refusSupport = { echec : 'VERROU_NON_ECRIT' , ... details } ;
assert . deepEqual ( [ seance . racine , seance . mode , seance . supportRompu , seance . refusSupport ] , [ DOC , 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( noms ( etatDe ( fs ) . charge ) , [ 'Ancolie' ] ) ;
await refus ( seance . passerEnEcriture ( ) , 'SUPPORT_ROMPU' , refusSupport ) ;
} ) ;
test ( "écrire ailleurs refuse un dossier qui porte déjà l'événement, le sien compris : jamais deux copies d'un identifiant dans un dossier" , async ( ) => {
const montage = monter ( ) ;
const { fs , seance } = montage ;
await enEcriture ( montage ) ;
await seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . equal ( seance . supportRompu , true ) ;
const avant = fs . compteurs . ecritures ;
const id = seance . charge . evenement . id ;
const erreur = await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
const etats = ( await fs . lister ( DOC , '' ) ) . filter ( ( { nom } ) => nom . endsWith ( SUFFIXES . etat ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( etats , [ ETAT ] ) ;
assert . doesNotMatch ( libelle ( erreur . code , erreur . details ) , /import/ ) ;
} ) ;
test ( "un état qu'écrire ailleurs ne pose pas y transporte la séance, avec le journal écrit : un nouvel essai n'y dérive pas une seconde base" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
// La deuxième écriture atomique dans les Documents, l'état qui suit le
// journal, voit refusés tous ses renommages.
const ecrireAtomique = fs . ecrireAtomique ;
let ecritures = 0 ;
fs . ecrireAtomique = async ( racine , chemin , texte ) => {
if ( racine . id === 'documents' ) {
ecritures += 1 ;
if ( ecritures === 2 ) fs . pannes . renommage ( 'documents' , 11 ) ;
}
return ecrireAtomique ( racine , chemin , texte ) ;
} ;
const id = seance . charge . evenement . id ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . racine , seance . ouvert . racine , seance . ouvert . base ] , [ DOC , DOC , BASE ] ) ;
const refusSupport = { echec : 'ETAT_NON_ECRIT' , chemin : ETAT , dossier : DOCUMENTS , cause : 'EBUSY' } ;
assert . deepEqual ( [ seance . mode , seance . supportRompu , seance . refusSupport ] , [ 'lecture' , true , refusSupport ] ) ;
assert . deepEqual ( seance . avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( noms ( seance . charge ) , [ 'Ancolie' ] ) ;
assert . deepEqual ( libellesDe ( journalDe ( fs ) ) , [ CREATION , 'Ajouté Ancolie' ] ) ;
assert . equal ( fs . contenu ( 'documents' , ETAT ) , null ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
await refus ( seance . ecrireAilleurs ( PORTABLE ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
const fichiers = ( await fs . lister ( DOC , '' ) ) . map ( ( { nom } ) => nom ) ;
assert . deepEqual ( fichiers , [ JOURNAL ] ) ;
} ) ;
// L'événement d'essai des Documents, dont l'état devient un JSON tronqué et
// dont le journal reste lisible : ouvrir le refuse, la séance l'ouvre par
// le secours du journal et passe en écriture, puis l'état d'un geste ne
// s'écrit pas — le geste est au journal, le support est rompu.
async function illisibleRompu ( ) {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
await deposerEssai ( fs ) ;
fs . deposer ( 'documents' , ETAT , fs . contenu ( 'documents' , ETAT ) . slice ( 0 , 40 ) ) ;
const montage = monter ( { fs } ) ;
const { seance } = montage ;
const illisible = await refus ( seance . ouvrir ( BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( [ illisible . details . raison , illisible . details . secours ] , [ 'JSON' , { precedent : null , journal : 1 } ] ) ;
await seance . ouvrirSecours ( BASE , 'journal' ) ;
await seance . passerEnEcriture ( ) ;
fs . pannes . renommage ( 'documents' , 11 ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Ancolie' } } ) , 'ETAT_NON_ECRIT' ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
return montage ;
}
// Les noms des fichiers d'une racine.
const fichiersDe = async ( fs , racine ) => ( await fs . lister ( racine , '' ) ) . map ( ( { nom } ) => nom ) ;
test ( "écrire ailleurs refuse le dossier où l'événement n'a qu'un état illisible, que son journal apparie : aucune seconde paire, et ce journal reste son secours" , async ( ) => {
const { fs , seance } = await illisibleRompu ( ) ;
const id = seance . charge . evenement . id ;
const avant = fs . compteurs . ecritures ;
const fichiers = await fichiersDe ( fs , DOC ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( await fichiersDe ( fs , DOC ) , fichiers ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , DOC ] ) ;
const illisible = await refus ( ouvrirDansLeDepot ( fs , DOC , BASE ) , 'ETAT_ILLISIBLE' ) ;
assert . deepEqual ( illisible . details . secours , { precedent : null , journal : 2 } ) ;
} ) ;
test ( "écrire ailleurs refuse de revenir au dossier d'où l'événement est parti, quand son état y est illisible" , async ( ) => {
const { fs , seance } = await illisibleRompu ( ) ;
const id = seance . charge . evenement . id ;
await seance . ecrireAilleurs ( PORTABLE ) ;
assert . deepEqual ( [ seance . racine , seance . supportRompu , seance . mode ] , [ PORTABLE , false , 'ecriture' ] ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const avant = fs . compteurs . ecritures ;
const fichiers = await fichiersDe ( fs , DOC ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'IDENTIFIANT_PRESENT' , { id , base : BASE } ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( await fichiersDe ( fs , DOC ) , fichiers ) ;
assert . deepEqual ( [ seance . supportRompu , seance . racine ] , [ true , PORTABLE ] ) ;
} ) ;
[FIX] storage, session: strict UTF-8, lost journal, lock, exclusions
The whole-iteration review found ways to lose or corrupt data. A state
saved in another encoding opened with its accents turned to U+FFFD and was
then overwritten; decoding is now strict, the state opens read-only, and a
gesture from its secours first moves it to the trash. A journal decodes line
by line, so one bad byte drops only the lines after it. A vanished journal
is rewritten whole, never appended to. Deleting refuses an event another
session holds; switching to writing rereads the disk; an import that
changes exclu says which reservations it suspends.
Checked: 1504 node, 35 browser, 29 node-long; depot.js, journal.js 100 %.
--- FR ---
[FIX] stockage, séance : UTF-8 strict, journal perdu, verrou, exclusions
La revue d'ensemble a trouvé des façons de perdre ou d'abîmer des données.
Un état enregistré dans un autre encodage s'ouvrait, ses accents changés en
U+FFFD, puis s'écrasait ; le décodage est strict, l'état s'ouvre en lecture
seule, et un geste depuis son secours le range d'abord à la corbeille. Le
journal se décode ligne par ligne : un octet fautif n'écarte que la suite.
Un journal disparu se réécrit en entier, jamais par ajout. Supprimer refuse
un événement qu'une autre séance tient ; passer en écriture relit le disque ;
un import qui change exclu dit quelles réservations il suspend.
Vérifié : 1504 node, 35 navigateur, 29 node-long ; depot.js, journal.js 100 %.
Assisted-by: Claude Opus 5.5
2026-10-07 01:20:42 -04:00
test ( "écrire ailleurs sur un support amovible l'annonce, comme la règle du dossier de travail au démarrage (§ 8.8)" , async ( ) => {
const { fs , seance } = await surPortable ( { support : { documents : 'amovible' } } ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const amovible = { code : 'SUPPORT_AMOVIBLE' , details : { dossier : DOCUMENTS } } ;
const { avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
amovible ,
] ) ;
assert . ok ( seance . avertissements . some ( ( { code } ) => code === 'SUPPORT_AMOVIBLE' ) ) ;
assert . match ( libelle ( amovible . code , amovible . details ) , /Gestion table tournante Libre/ ) ;
} ) ;
const fautesDuPont = [
[ 'stockage' , ( ) => new ErreurStockage ( 'LECTURE' , { chemin : '' , dossier : DOCUMENTS , cause : 'EIO' } ) ] ,
[ 'pont' , ( ) => new TypeError ( 'réponse du pont illisible pour typeSupport' ) ] ,
] ;
for ( const [ nature , faute ] of fautesDuPont ) {
test ( ` écrire ailleurs réussit quand le type du support ne se dit pas (faute ${ nature } ) : le support vaut inconnu, rien n'est annoncé ` , async ( ) => {
const { fs , seance , planificateur } = await surPortable ( { support : { documents : 'amovible' } } ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
fs . typeSupport = async ( ) => {
throw faute ( ) ;
} ;
const { base , avertissements } = await seance . ecrireAilleurs ( DOC ) ;
assert . equal ( base , BASE ) ;
assert . deepEqual ( avertissements , [
{ code : 'VERROU_NON_RENDU' , details : { chemin : VERROU , dossier : PORTABLE . chemin , cause : 'EROFS' } } ,
] ) ;
assert . deepEqual ( seance . avertissements , avertissements ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . racine , planificateur . actives ] , [ false , 'ecriture' , DOC , 1 ] ) ;
} ) ;
}
test ( "tant que le support est rompu, ouvrir, ouvrir un secours, créer et fermer sont refusés SUPPORT_ROMPU_NON_SAUVE, sans rien changer" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const refusSupport = { echec : 'ECRITURE' , chemin : JOURNAL , dossier : PORTABLE . chemin , cause : 'EROFS' } ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ouvrir ( BASE ) , 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) ;
await refus ( seance . ouvrirSecours ( BASE , 'journal' ) , 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) ;
await refus ( seance . creer ( { nom : 'Soirée des Grillons' , date : null , siegesParDefaut : 2 , tours : 2 } ) , 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) ;
await refus ( seance . fermer ( ) , 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
assert . deepEqual ( [ seance . supportRompu , seance . refusSupport , noms ( seance . charge ) ] , [ true , refusSupport , [ 'Ancolie' ] ] ) ;
assert . match ( libelle ( 'SUPPORT_ROMPU_NON_SAUVE' , refusSupport ) , /« Écrire ailleurs »/ ) ;
await seance . ecrireAilleurs ( DOC ) ;
await seance . fermer ( ) ;
assert . equal ( seance . ouvert , null ) ;
} ) ;
test ( "le support rompu se quitte sur abandon explicite : ouvrir, ou fermer, avec {abandonner: true}" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
const rendu = await seance . ouvrir ( BASE , { abandonner : true } ) ;
assert . equal ( rendu . base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , noms ( seance . charge ) ] , [ false , 'lecture' , [ 'Ancolie' ] ] ) ;
await refus ( seance . passerEnEcriture ( ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
await seance . fermer ( { abandonner : true } ) ;
assert . deepEqual ( [ seance . ouvert , seance . supportRompu ] , [ null , false ] ) ;
} ) ;
test ( "le support rompu se quitte sur abandon explicite : créer, ou ouvrir un secours, avec {abandonner: true}" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
// Un état que le support refuse rompt le support ; les renommages
// rétablis ensuite, ce qui suit s'écrit.
const rompre = async ( nom ) => {
await seance . passerEnEcriture ( ) ;
fs . pannes . renommage ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom } } ) , 'ETAT_NON_ECRIT' ) ;
fs . pannes . renommage ( 'portable' , 0 ) ;
assert . equal ( seance . supportRompu , true ) ;
} ;
await rompre ( 'Bruyère' ) ;
const grillons = 'Soirée des Grillons' ;
const cree = await seance . creer ( { nom : grillons , date : null , siegesParDefaut : 2 , tours : 2 } , { abandonner : true } ) ;
assert . equal ( cree . base , grillons ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , seance . charge . evenement . nom ] , [ false , 'lecture' , grillons ] ) ;
await rompre ( 'Cerfeuil' ) ;
const secours = await seance . ouvrirSecours ( BASE , 'journal' , { abandonner : true } ) ;
assert . equal ( secours . base , BASE ) ;
assert . deepEqual ( [ seance . supportRompu , seance . mode , noms ( seance . charge ) ] , [ false , 'lecture' , [ 'Ancolie' , 'Bruyère' ] ] ) ;
} ) ;
[ADD] application: session, named commands, French label table
The session holds one event, opens it read-only and writes only after
"Modifier" takes the lock (§ 8.4, § 8.8); calls run one after another, so a
double click writes two entries in order. Every change is a pure named
command with a frozen French label (§ 8.2); undo, redo and return write
entries and never truncate the journal (§ 8.3). A failed write — a gesture,
or the lock on a removed key — breaks the medium until the event is written
elsewhere (§ 8.6). The label table covers every code storage, CSV and the
session emit, and its test sweeps the sources so a new code needs a text.
Checked: 1416 node tests; depot.js at 100 % with the identifier lister keeps.
--- FR ---
[ADD] application : séance, commandes nommées, table des libellés
La séance tient un événement, l'ouvre en lecture et n'écrit qu'après que
« Modifier » a pris le verrou (§ 8.4, § 8.8) ; les appels passent l'un après
l'autre : un double clic écrit deux entrées, dans l'ordre. Toute modification
est une commande nommée pure au libellé français figé (§ 8.2) ; défaire,
refaire et revenir écrivent des entrées sans tronquer le journal (§ 8.3). Une
écriture refusée — un geste, ou le verrou sur une clé retirée — rompt le
support jusqu'à l'écriture ailleurs (§ 8.6). La table des libellés couvre
chaque code du stockage, du CSV et de la séance ; son épreuve balaie les
sources, et un code neuf exige un texte.
Vérifié : 1416 épreuves node ; depot.js à 100 %, lister gardant l'identifiant.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:36 -04:00
test ( "écrire ailleurs qui échoue laisse le support rompu ; sans support rompu, il est refusé SUPPORT_INTACT" , async ( ) => {
const { fs , seance } = await surPortable ( ) ;
const avant = fs . compteurs . ecritures ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'SUPPORT_INTACT' ) ;
assert . equal ( fs . compteurs . ecritures , avant ) ;
fs . pannes . ecriture ( 'portable' ) ;
await refus ( seance . executer ( 'ajouterParticipant' , { champs : { nom : 'Bruyère' } } ) , 'ECRITURE' ) ;
fs . pannes . ecriture ( 'documents' ) ;
await refus ( seance . ecrireAilleurs ( DOC ) , 'ECRITURE' ) ;
assert . equal ( seance . supportRompu , true ) ;
assert . deepEqual ( seance . racine , PORTABLE ) ;
} ) ;
} ) ;
describe ( "séance : ni horloge ni minuterie lues en direct (§ 14.7)" , ( ) => {
// Commentaires et chaînes blanchis : seul le code compte.
const JETONS = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\[\s\S]|[^`\\])*`/g ;
const code = ( texte ) => texte . replace ( JETONS , ( jeton ) => jeton . replace ( /[^\n]/g , ' ' ) ) ;
test ( "ni la séance, ni les commandes, ni la table des libellés ne lisent l'horloge, un aléa ou une minuterie" , ( ) => {
for ( const fichier of [ 'seance.js' , 'commandes.js' , 'libelles.js' ] ) {
const texte = code ( readFileSync ( new URL ( ` ./ ${ fichier } ` , import . meta . url ) , 'utf8' ) ) ;
assert . ok ( texte . trim ( ) . length > 0 , ` ${ fichier } vide ` ) ;
for ( const motif of [ /\bDate\b/ , /\bsetTimeout\b/ , /\bsetInterval\b/ , /\bclearTimeout\b/ , /\bperformance\b/ , /\bMath\s*\.\s*random\b/ , /\bcrypto\b/ ] ) {
assert . doesNotMatch ( texte , motif , ` ${ fichier } : ${ motif } ` ) ;
}
}
} ) ;
} ) ;