[ADD] storage: file-system interface, failing test double, contract
Storage code receives its file system as a parameter (§ 13.4): one
interface, and here the implementation tests inject. It keeps files in
memory, shares a disk between two instances to replay a restart, and
fails on demand: a refused rename that leaves the target intact, writes
refused from a moment on, a process killed after n writes, a failing
probe, a removable medium. One contract suite, its assertions passed in,
will run unchanged on the Electron and web implementations.
Checked: 87 tests, the contract suite run on the test double.
--- FR ---
[ADD] stockage : interface du système de fichiers, double, contrat
Le code du stockage reçoit son système de fichiers en paramètre
(§ 13.4) : une interface, et ici l'implémentation que les épreuves
injectent. Elle garde les fichiers en mémoire, partage un disque entre
deux instances pour rejouer un redémarrage, et tombe en panne à la
demande : renommage refusé qui laisse la cible intacte, écritures
refusées à partir d'un instant, processus tué après n écritures, sonde
qui échoue, support amovible. Une suite de contrat, ses assertions
reçues en paramètre, servira telle quelle sous Electron et sous web.
Vérifié : 87 épreuves, la suite de contrat jouée sur le double.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:25 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Épreuves du système de fichiers d'épreuve (§ 13.4, § 14.10). La suite de
// contrat s'y joue sur trois réglages : Windows, Unix, et celui de la
// plateforme web, sans renommage atomique ni verrou. Suit ce qui n'appartient
// qu'à lui : ses réglages, qui en ressortent tels qu'il les reçoit, ses
// racines et emplacements, la casse des noms, les pannes qu'il simule —
// renommage refusé, support qui refuse l'écriture, processus coupé puis
// rouvert sur le même disque —, le verrou entre processus, les compteurs, et
// ce que l'épreuve pose ou lit sans compter. Les noms d'épreuve sont inventés.
import assert from 'node:assert/strict' ;
import { ErreurStockage } from '../src/stockage/erreurs.js' ;
import { eprouverContrat } from './contrat_fichiers.js' ;
import { PanneSimulee , creerFichiersSimules } from './fichiers_simules.js' ;
import { describe , test } from './lanceur.js' ;
// Rend la raison du rejet de la promesse ; échoue quand elle se résout.
async function rejette ( promesse , message ) {
let raison ;
await assert . rejects (
promesse ,
( erreur ) => {
raison = erreur ;
return true ;
} ,
message ,
) ;
return raison ;
}
// Attend le rejet d'une ErreurStockage de ce code et de ces détails.
async function echoue ( promesse , code , details ) {
const erreur = await rejette ( promesse , code ) ;
assert . ok ( erreur instanceof ErreurStockage , String ( erreur ) ) ;
assert . equal ( erreur . code , code ) ;
assert . deepEqual ( erreur . details , details ) ;
}
// Rend l'erreur que lève la fonction ; échoue quand elle n'en lève pas.
function erreurDe ( fonction ) {
try {
fonction ( ) ;
} catch ( erreur ) {
return erreur ;
}
assert . fail ( 'aucune erreur levée' ) ;
}
const noms = ( entrees ) => entrees . map ( ( entree ) => entree . nom ) ;
const DOCUMENTS = 'C:\\Users\\Exemple\\Documents\\Gestion table tournante Libre' ;
const DOC = { id : 'documents' , chemin : DOCUMENTS } ;
const DATA = 'E:\\soirees\\data' ;
const PORTABLE = { id : 'portable' , chemin : DATA } ;
// Un système d'épreuve dont la racine documents existe, posée sans compter.
function preparer ( options = { } ) {
const fs = creerFichiersSimules ( options ) ;
fs . deposerDossier ( 'documents' ) ;
return fs ;
}
// Les trois réglages sur lesquels la suite de contrat se joue.
const REGLAGES = [
[ 'épreuve, sous Windows' , { } ] ,
[
'épreuve, sous Unix' ,
{
separateur : '/' ,
insensibleCasse : false ,
documents : '/home/exemple/Documents/Gestion table tournante Libre' ,
donneesApplicatives : [ '/home/exemple/.config' , '/tmp' ] ,
} ,
] ,
[
'épreuve, comme la plateforme web' ,
{
nature : 'web' ,
renommageAtomique : false ,
verrouDisponible : false ,
separateur : '/' ,
insensibleCasse : false ,
documents : '/gestion_table_tournante_libre' ,
} ,
] ,
] ;
for ( const [ nom , options ] of REGLAGES ) {
eprouverContrat (
nom ,
async ( ) => {
const fs = preparer ( options ) ;
return { fs , racine : ( await fs . racines ( ) ) . documents } ;
} ,
{ describe , test , egal : assert . deepStrictEqual , vrai : assert . ok , rejette } ,
) ;
}
describe ( 'fichiers simulés : racines et emplacements' , ( ) => {
test ( 'par défaut : Windows, sans égard à la casse, sans exécutable publié, la seule racine documents' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
assert . equal ( fs . nature , 'epreuve' ) ;
assert . equal ( fs . renommageAtomique , true ) ;
assert . equal ( fs . verrouDisponible , true ) ;
assert . deepEqual ( await fs . emplacements ( ) , {
executable : null ,
donneesApplicatives : [ 'C:\\Users\\Exemple\\AppData\\Roaming' , 'C:\\Users\\Exemple\\AppData\\Local' ] ,
insensibleCasse : true ,
separateur : '\\' ,
} ) ;
assert . deepEqual ( await fs . racines ( ) , { portable : null , documents : DOC } ) ;
await echoue ( fs . lireTexte ( PORTABLE , 'soiree.gtt.json' ) , 'CHEMIN_REFUSE' , { chemin : 'soiree.gtt.json' } ) ;
} ) ;
test ( 'les réglages donnés en ressortent tels quels : nature, garanties, hôte, emplacements, racine documents' , async ( ) => {
// Chaque réglage diffère de sa valeur par défaut : une valeur que le
// double figerait au lieu de la reprendre fait échouer l'épreuve.
// donneesApplicatives est la seule voie par laquelle il simule un
// dossier de travail sous un emplacement de données applicatives
// (§ 8.6, point 2).
const documents = '/home/exemple/Documents/Gestion table tournante Libre' ;
const fs = creerFichiersSimules ( {
documents ,
donneesApplicatives : [ '/home/exemple/.config' , '/tmp' ] ,
separateur : '/' ,
insensibleCasse : false ,
nature : 'web' ,
renommageAtomique : false ,
verrouDisponible : false ,
hote : 'autre-poste' ,
} ) ;
assert . equal ( fs . nature , 'web' ) ;
assert . equal ( fs . renommageAtomique , false ) ;
assert . equal ( fs . verrouDisponible , false ) ;
assert . equal ( fs . hote , 'autre-poste' ) ;
assert . deepEqual ( await fs . emplacements ( ) , {
executable : null ,
donneesApplicatives : [ '/home/exemple/.config' , '/tmp' ] ,
insensibleCasse : false ,
separateur : '/' ,
} ) ;
assert . deepEqual ( await fs . racines ( ) , { portable : null , documents : { id : 'documents' , chemin : documents } } ) ;
} ) ;
test ( "un exécutable publié ouvre la racine portable, data/ à côté de lui, au séparateur du système" , async ( ) => {
const cas = [
[ { executable : 'E:\\soirees' } , 'E:\\soirees\\data' ] ,
[ { executable : 'E:\\' } , 'E:\\data' ] ,
[ { executable : '/media/cle/soirees' , separateur : '/' , insensibleCasse : false } , '/media/cle/soirees/data' ] ,
] ;
for ( const [ options , chemin ] of cas ) {
const fs = creerFichiersSimules ( options ) ;
assert . deepEqual ( ( await fs . racines ( ) ) . portable , { id : 'portable' , chemin } ) ;
assert . equal ( ( await fs . emplacements ( ) ) . executable , options . executable ) ;
}
} ) ;
test ( 'typeSupport rend le support déclaré pour une racine, fixe par défaut' , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' , support : { portable : 'amovible' } } ) ;
assert . equal ( await fs . typeSupport ( PORTABLE ) , 'amovible' ) ;
assert . equal ( await fs . typeSupport ( DOC ) , 'fixe' ) ;
} ) ;
test ( "une racine n'existe qu'une fois créée : avant, lister lève ABSENT et écrire ECRITURE (ENOENT) ; sonder la crée" , async ( ) => {
const fs = creerFichiersSimules ( ) ;
const absent = ( chemin ) => ( { chemin , dossier : DOCUMENTS , cause : 'ENOENT' } ) ;
await echoue ( fs . lister ( DOC , '' ) , 'ABSENT' , { chemin : '' } ) ;
await echoue ( fs . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'x' ) , 'ECRITURE' , absent ( 'soiree.gtt.json' ) ) ;
await echoue ( fs . ajouterLigne ( DOC , 'soiree.gtt-journal.jsonl' , 'x' ) , 'ECRITURE' , absent ( 'soiree.gtt-journal.jsonl' ) ) ;
await echoue ( fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) , 'ECRITURE' , absent ( 'soiree.gtt.verrou' ) ) ;
assert . deepEqual ( await fs . sonder ( DOC ) , { inscriptible : true , cause : null } ) ;
assert . deepEqual ( await fs . lister ( DOC , '' ) , [ ] ) ;
} ) ;
test ( 'deplacer vers un dossier absent nomme la cible et son dossier, et laisse la source' , async ( ) => {
const fs = preparer ( ) ;
fs . deposer ( 'documents' , 'soiree.gtt.json' , 'état' ) ;
await echoue ( fs . deplacer ( DOC , 'soiree.gtt.json' , 'corbeille/soiree.gtt.json' ) , 'ECRITURE' , {
chemin : 'corbeille/soiree.gtt.json' ,
dossier : ` ${ DOCUMENTS } \\ corbeille ` ,
cause : 'ENOENT' ,
} ) ;
assert . equal ( fs . contenu ( 'documents' , 'soiree.gtt.json' ) , 'état' ) ;
} ) ;
test ( 'une sonde déclarée en échec rend sa cause, et sa racine refuse toute écriture' , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' , sonde : { portable : 'EACCES' } } ) ;
assert . deepEqual ( await fs . sonder ( PORTABLE ) , { inscriptible : false , cause : 'EACCES' } ) ;
await echoue ( fs . creerDossier ( PORTABLE , '' ) , 'ECRITURE' , { chemin : '' , dossier : DATA , cause : 'EACCES' } ) ;
assert . deepEqual ( await fs . sonder ( DOC ) , { inscriptible : true , cause : null } ) ;
} ) ;
test ( "un fichier qui occupe le chemin de la racine fait échouer la sonde, sans lever (EEXIST)" , async ( ) => {
// L'exécutable est publié dans E:\soirees, que la racine documents
// désigne aussi : un fichier data y occupe le chemin de la racine portable.
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' , documents : 'E:\\soirees' } ) ;
fs . deposer ( 'documents' , 'data' , 'un fichier, pas un dossier' ) ;
assert . deepEqual ( await fs . sonder ( PORTABLE ) , { inscriptible : false , cause : 'EEXIST' } ) ;
assert . equal ( fs . contenu ( 'documents' , 'data' ) , 'un fichier, pas un dossier' ) ;
} ) ;
test ( "les dialogues natifs rendent null, comme quand l'opérateur annule ; ouvrir une racine inconnue est refusé" , async ( ) => {
const fs = preparer ( ) ;
assert . equal ( await fs . choisirDossier ( ) , null ) ;
assert . equal ( await fs . choisirFichierAImporter ( ) , null ) ;
assert . equal ( await fs . enregistrerSous ( 'liste.csv' , new Uint8Array ( [ 0x61 ] ) ) , null ) ;
assert . equal ( await fs . ouvrirDansExplorateur ( DOC ) , undefined ) ;
await echoue ( fs . ouvrirDansExplorateur ( PORTABLE ) , 'CHEMIN_REFUSE' , { chemin : '' } ) ;
} ) ;
} ) ;
describe ( 'fichiers simulés : casse des noms' , ( ) => {
test ( 'sans égard à la casse, une autre casse désigne le même fichier, et lister rend la casse de sa création' , async ( ) => {
const fs = preparer ( ) ;
await fs . ecrireAtomique ( DOC , 'Soir\u{E9}e.gtt.json' , 'état' ) ;
assert . equal ( await fs . lireTexte ( DOC , 'SOIR\u{C9}E.GTT.JSON' ) , 'état' ) ;
await fs . ajouterLigne ( DOC , 'Soir\u{E9}e.gtt-journal.jsonl' , 'a' ) ;
await fs . ajouterLigne ( DOC , 'soir\u{E9}e.GTT-JOURNAL.jsonl' , 'b' ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [
'Soir\u{E9}e.gtt-journal.jsonl' ,
'Soir\u{E9}e.gtt.json' ,
] ) ;
assert . equal ( fs . contenu ( 'documents' , 'SOIR\u{C9}E.gtt-journal.jsonl' ) , 'a\nb\n' ) ;
} ) ;
test ( 'la casse se replie caractère par caractère, comme Windows : un sigma final rejoint sa majuscule, un eszett ne devient pas SS' , async ( ) => {
const fs = preparer ( ) ;
await fs . ecrireAtomique ( DOC , '\u{3A3}\u{391}\u{3A3}.txt' , 'majuscules' ) ;
assert . equal ( await fs . lireTexte ( DOC , '\u{3C3}\u{3B1}\u{3C2}.txt' ) , 'majuscules' ) ;
await fs . ecrireAtomique ( DOC , 'stra\u{DF}e.txt' , 'eszett' ) ;
await fs . ecrireAtomique ( DOC , 'STRASSE.txt' , 'deux s' ) ;
assert . equal ( await fs . lireTexte ( DOC , 'stra\u{DF}e.txt' ) , 'eszett' ) ;
assert . equal ( await fs . lireTexte ( DOC , 'STRASSE.txt' ) , 'deux s' ) ;
} ) ;
test ( 'renommer par-dessus donne au fichier la casse du nouveau nom ; deplacer vers une autre casse du même nom lève EXISTE' , async ( ) => {
const fs = preparer ( ) ;
await fs . ecrireAtomique ( DOC , 'Soir\u{E9}e.gtt.json' , 'un' ) ;
await fs . ecrireAtomique ( DOC , 'soir\u{E9}e.gtt.json' , 'deux' ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'soir\u{E9}e.gtt.json' ] ) ;
assert . equal ( fs . contenu ( 'documents' , 'SOIR\u{C9}E.gtt.json' ) , 'deux' ) ;
await echoue ( fs . deplacer ( DOC , 'soir\u{E9}e.gtt.json' , 'SOIR\u{C9}E.gtt.json' ) , 'EXISTE' , {
chemin : 'SOIR\u{C9}E.gtt.json' ,
} ) ;
} ) ;
test ( 'sensible à la casse, deux casses font deux fichiers' , async ( ) => {
const racine = '/home/exemple/Documents/Gestion table tournante Libre' ;
const fs = preparer ( { separateur : '/' , insensibleCasse : false , documents : racine } ) ;
const doc = { id : 'documents' , chemin : racine } ;
await fs . ecrireAtomique ( doc , 'soir\u{E9}e.gtt.json' , 'minuscule' ) ;
await fs . ecrireAtomique ( doc , 'Soir\u{E9}e.gtt.json' , 'majuscule' ) ;
assert . deepEqual ( noms ( await fs . lister ( doc , '' ) ) , [ 'Soir\u{E9}e.gtt.json' , 'soir\u{E9}e.gtt.json' ] ) ;
await echoue ( fs . lireTexte ( doc , 'SOIR\u{C9}E.GTT.JSON' ) , 'ABSENT' , { chemin : 'SOIR\u{C9}E.GTT.JSON' } ) ;
} ) ;
test ( 'un disque partagé garde la casse et le séparateur de sa première instance' , ( ) => {
const disque = new Map ( ) ;
creerFichiersSimules ( { disque } ) ;
assert . throws ( ( ) => creerFichiersSimules ( { disque , insensibleCasse : false } ) ) ;
assert . throws ( ( ) => creerFichiersSimules ( { disque , separateur : '/' } ) ) ;
creerFichiersSimules ( { disque } ) ;
} ) ;
} ) ;
describe ( 'fichiers simulés : renommage refusé (§ 8.8, § 14.10)' , ( ) => {
test ( "un renommage refusé laisse la cible intacte, retire le fichier d'écriture et lève ECRITURE en nommant fichier et dossier" , async ( ) => {
const fs = preparer ( ) ;
fs . deposer ( 'documents' , 'soiree.gtt.json' , 'avant' ) ;
fs . pannes . renommage ( 'documents' ) ;
await echoue ( fs . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'après' ) , 'ECRITURE' , {
chemin : 'soiree.gtt.json' ,
dossier : DOCUMENTS ,
cause : 'EBUSY' ,
} ) ;
assert . equal ( fs . contenu ( 'documents' , 'soiree.gtt.json' ) , 'avant' ) ;
assert . equal ( fs . contenu ( 'documents' , 'soiree.gtt.json.ecriture' ) , null ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'soiree.gtt.json' ] ) ;
} ) ;
test ( "l'écriture réessaie le renommage : dix refus et elle passe, onze et elle échoue" , async ( ) => {
const fs = preparer ( ) ;
fs . pannes . renommage ( 'documents' , 10 , 'EPERM' ) ;
await fs . ecrireAtomique ( DOC , 'etat.json' , 'après dix refus' ) ;
assert . equal ( fs . contenu ( 'documents' , 'etat.json' ) , 'après dix refus' ) ;
fs . pannes . renommage ( 'documents' , 11 , 'EPERM' ) ;
await echoue ( fs . ecrireAtomique ( DOC , 'etat.json' , 'après onze refus' ) , 'ECRITURE' , {
chemin : 'etat.json' ,
dossier : DOCUMENTS ,
cause : 'EPERM' ,
} ) ;
assert . equal ( fs . contenu ( 'documents' , 'etat.json' ) , 'après dix refus' ) ;
} ) ;
test ( 'la panne de renommage ne touche que sa racine, et refuse aussi deplacer' , async ( ) => {
const fs = preparer ( { executable : 'E:\\soirees' } ) ;
fs . deposer ( 'portable' , 'soiree.gtt.json' , 'état' ) ;
fs . pannes . renommage ( 'portable' ) ;
await fs . ecrireAtomique ( DOC , 'ailleurs.json' , 'passe' ) ;
assert . equal ( fs . contenu ( 'documents' , 'ailleurs.json' ) , 'passe' ) ;
await echoue ( fs . deplacer ( PORTABLE , 'soiree.gtt.json' , 'soiree-2.gtt.json' ) , 'ECRITURE' , {
chemin : 'soiree.gtt.json' ,
dossier : DATA ,
cause : 'EBUSY' ,
} ) ;
assert . equal ( fs . contenu ( 'portable' , 'soiree.gtt.json' ) , 'état' ) ;
assert . equal ( fs . contenu ( 'portable' , 'soiree-2.gtt.json' ) , null ) ;
} ) ;
test ( "sans renommage atomique, ecrireAtomique écrit droit sur la cible : une panne de renommage ne l'arrête pas" , async ( ) => {
const fs = preparer ( { renommageAtomique : false } ) ;
fs . deposer ( 'documents' , 'etat.json' , 'avant' ) ;
fs . pannes . renommage ( 'documents' ) ;
await fs . ecrireAtomique ( DOC , 'etat.json' , 'après' ) ;
assert . equal ( fs . contenu ( 'documents' , 'etat.json' ) , 'après' ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'etat.json' ] ) ;
} ) ;
} ) ;
describe ( "fichiers simulés : support qui refuse l'écriture" , ( ) => {
test ( "après pannes.ecriture, chaque primitive d'écriture de la racine lève ECRITURE avec la cause, la sonde échoue, et rien ne change" , async ( ) => {
const fs = preparer ( { executable : 'E:\\soirees' } ) ;
fs . deposer ( 'portable' , 'soiree.gtt.json' , 'état' ) ;
fs . deposer ( 'portable' , 'soiree.gtt-journal.jsonl' , 'ligne\n' ) ;
fs . deposer ( 'portable' , 'soiree.gtt.verrou' , '{"seance":"seance-a"}' ) ;
fs . pannes . ecriture ( 'portable' ) ;
const refus = ( chemin ) => ( { chemin , dossier : DATA , cause : 'EROFS' } ) ;
await echoue ( fs . ecrireAtomique ( PORTABLE , 'soiree.gtt.json' , 'nouveau' ) , 'ECRITURE' , refus ( 'soiree.gtt.json' ) ) ;
await echoue (
fs . ajouterLigne ( PORTABLE , 'soiree.gtt-journal.jsonl' , 'suite' ) ,
'ECRITURE' ,
refus ( 'soiree.gtt-journal.jsonl' ) ,
) ;
await echoue ( fs . creerDossier ( PORTABLE , 'corbeille' ) , 'ECRITURE' , refus ( 'corbeille' ) ) ;
await echoue ( fs . deplacer ( PORTABLE , 'soiree.gtt.json' , 'autre.gtt.json' ) , 'ECRITURE' , refus ( 'soiree.gtt.json' ) ) ;
await echoue ( fs . supprimer ( PORTABLE , 'soiree.gtt.json' ) , 'ECRITURE' , refus ( 'soiree.gtt.json' ) ) ;
await echoue ( fs . verrouiller ( PORTABLE , 'neuf.gtt.verrou' , 'seance-b' ) , 'ECRITURE' , refus ( 'neuf.gtt.verrou' ) ) ;
await echoue ( fs . deverrouiller ( PORTABLE , 'soiree.gtt.verrou' , 'seance-a' ) , 'ECRITURE' , refus ( 'soiree.gtt.verrou' ) ) ;
assert . deepEqual ( await fs . sonder ( PORTABLE ) , { inscriptible : false , cause : 'EROFS' } ) ;
assert . deepEqual ( noms ( await fs . lister ( PORTABLE , '' ) ) , [
'soiree.gtt-journal.jsonl' ,
'soiree.gtt.json' ,
'soiree.gtt.verrou' ,
] ) ;
assert . equal ( await fs . lireTexte ( PORTABLE , 'soiree.gtt.json' ) , 'état' ) ;
assert . equal ( await fs . lireTexte ( PORTABLE , 'soiree.gtt-journal.jsonl' ) , 'ligne\n' ) ;
} ) ;
test ( "une cause donnée remplace EROFS, et l'autre racine écrit encore" , async ( ) => {
const fs = preparer ( { executable : 'E:\\soirees' } ) ;
fs . deposerDossier ( 'portable' ) ;
fs . pannes . ecriture ( 'documents' , 'ENOSPC' ) ;
await echoue ( fs . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'x' ) , 'ECRITURE' , {
chemin : 'soiree.gtt.json' ,
dossier : DOCUMENTS ,
cause : 'ENOSPC' ,
} ) ;
await fs . ecrireAtomique ( PORTABLE , 'soiree.gtt.json' , 'écrit ailleurs' ) ;
assert . equal ( fs . contenu ( 'portable' , 'soiree.gtt.json' ) , 'écrit ailleurs' ) ;
} ) ;
} ) ;
describe ( 'fichiers simulés : processus coupé (couperApres)' , ( ) => {
test ( 'après n écritures réussies, toute primitive lève PanneSimulee ; une nouvelle instance sur le même disque relit les n écritures et écrit à son tour' , async ( ) => {
const disque = new Map ( ) ;
const premiere = creerFichiersSimules ( { disque } ) ;
premiere . deposerDossier ( 'documents' ) ;
premiere . deposer ( 'documents' , 'soiree.gtt.json' , 'révision 1' ) ;
premiere . pannes . couperApres ( 2 ) ;
await premiere . ajouterLigne ( DOC , 'soiree.gtt-journal.jsonl' , '{"revision":2}' ) ;
await premiere . ecrireAtomique ( DOC , 'soiree.gtt.json.precedent' , 'révision 1' ) ;
const panne = await rejette ( premiere . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'révision 2' ) ) ;
assert . ok ( panne instanceof PanneSimulee , String ( panne ) ) ;
assert . ok ( ! ( panne instanceof ErreurStockage ) ) ;
await assert . rejects ( premiere . lireTexte ( DOC , 'soiree.gtt.json' ) , PanneSimulee ) ;
await assert . rejects ( premiere . lister ( DOC , '' ) , PanneSimulee ) ;
await assert . rejects ( premiere . emplacements ( ) , PanneSimulee ) ;
await assert . rejects ( premiere . sonder ( DOC ) , PanneSimulee ) ;
const reouverte = creerFichiersSimules ( { disque } ) ;
assert . equal ( await reouverte . lireTexte ( DOC , 'soiree.gtt-journal.jsonl' ) , '{"revision":2}\n' ) ;
assert . equal ( await reouverte . lireTexte ( DOC , 'soiree.gtt.json.precedent' ) , 'révision 1' ) ;
assert . equal ( await reouverte . lireTexte ( DOC , 'soiree.gtt.json' ) , 'révision 1' ) ;
await reouverte . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'révision 2' ) ;
assert . equal ( premiere . contenu ( 'documents' , 'soiree.gtt.json' ) , 'révision 2' ) ;
} ) ;
test ( 'une écriture refusée ne compte pas parmi les n' , async ( ) => {
const fs = preparer ( ) ;
fs . pannes . renommage ( 'documents' ) ;
fs . pannes . couperApres ( 1 ) ;
const refus = await rejette ( fs . ecrireAtomique ( DOC , 'soiree.gtt.json' , 'x' ) ) ;
assert . equal ( refus . code , 'ECRITURE' ) ;
await fs . creerDossier ( DOC , 'corbeille' ) ;
await assert . rejects ( fs . lister ( DOC , '' ) , PanneSimulee ) ;
} ) ;
test ( 'couperApres(0) arrête le processus sur-le-champ' , async ( ) => {
const fs = preparer ( ) ;
fs . pannes . couperApres ( 0 ) ;
await assert . rejects ( fs . lister ( DOC , '' ) , PanneSimulee ) ;
} ) ;
test ( "les pannes refusent un compte qui n'est pas un entier positif ou nul" , ( ) => {
const fs = preparer ( ) ;
assert . throws ( ( ) => fs . pannes . renommage ( 'documents' , - 1 ) , TypeError ) ;
assert . throws ( ( ) => fs . pannes . renommage ( 'documents' , 1.5 ) , TypeError ) ;
assert . throws ( ( ) => fs . pannes . couperApres ( - 1 ) , TypeError ) ;
assert . throws ( ( ) => fs . pannes . couperApres ( Infinity ) , TypeError ) ;
} ) ;
} ) ;
describe ( 'fichiers simulés : verrou' , ( ) => {
test ( "le fichier de verrou porte la séance, le pid et l'hôte du processus, et l'instant de sa prise" , async ( ) => {
const fs = preparer ( ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) , { pris : true } ) ;
const texte = fs . contenu ( 'documents' , 'soiree.gtt.verrou' ) ;
assert . match (
texte ,
/^\{"seance":"seance-a","pid":\d+,"hote":"poste-epreuve","depuis":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00"\}\n$/ ,
) ;
const { pid , depuis } = JSON . parse ( texte ) ;
assert . equal ( pid , fs . pid ) ;
assert . equal ( fs . hote , 'poste-epreuve' ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-b' ) , {
pris : false ,
seance : 'seance-a' ,
depuis ,
vivant : true ,
} ) ;
} ) ;
test ( 'la création est exclusive, même pour la séance qui tient le verrou' , async ( ) => {
const fs = preparer ( ) ;
await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) ;
const second = await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) ;
assert . deepEqual ( [ second . pris , second . seance , second . vivant ] , [ false , 'seance-a' , true ] ) ;
} ) ;
test ( 'vivant : vrai pour un processus en vie sur la même machine, faux pour un processus coupé, null pour une autre machine' , async ( ) => {
const disque = new Map ( ) ;
const a = creerFichiersSimules ( { disque } ) ;
const b = creerFichiersSimules ( { disque } ) ;
const ailleurs = creerFichiersSimules ( { disque , hote : 'autre-poste' } ) ;
a . deposerDossier ( 'documents' ) ;
assert . notEqual ( a . pid , b . pid ) ;
await a . verrouiller ( DOC , 'a.gtt.verrou' , 'seance-a' ) ;
const vuDeB = await b . verrouiller ( DOC , 'a.gtt.verrou' , 'seance-b' ) ;
assert . deepEqual ( [ vuDeB . seance , vuDeB . vivant ] , [ 'seance-a' , true ] ) ;
await ailleurs . verrouiller ( DOC , 'c.gtt.verrou' , 'seance-c' ) ;
const vuDeA = await a . verrouiller ( DOC , 'c.gtt.verrou' , 'seance-x' ) ;
assert . deepEqual ( [ vuDeA . seance , vuDeA . vivant ] , [ 'seance-c' , null ] ) ;
// Le pid d'un processus de l'autre machine ne désigne aucun processus de
// celle-ci.
const usurpe = { seance : 'seance-u' , pid : ailleurs . pid , hote : 'poste-epreuve' , depuis : '2026-01-01T08:00:00+00:00' } ;
a . deposer ( 'documents' , 'u.gtt.verrou' , JSON . stringify ( usurpe ) ) ;
assert . equal ( ( await a . verrouiller ( DOC , 'u.gtt.verrou' , 'seance-x' ) ) . vivant , false ) ;
a . pannes . couperApres ( 0 ) ;
const apresCoupure = await b . verrouiller ( DOC , 'a.gtt.verrou' , 'seance-b' ) ;
assert . deepEqual ( [ apresCoupure . seance , apresCoupure . vivant ] , [ 'seance-a' , false ] ) ;
} ) ;
test ( "un verrou illisible ne se lit ni ne s'efface ; un pid absent de la machine est mort" , async ( ) => {
const fs = preparer ( ) ;
const illisible = { pris : false , seance : null , depuis : null , vivant : null } ;
fs . deposer ( 'documents' , 'tronque.gtt.verrou' , '{"seance":' ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'tronque.gtt.verrou' , 'seance-a' ) , illisible ) ;
await fs . deverrouiller ( DOC , 'tronque.gtt.verrou' , 'seance-a' ) ;
assert . equal ( fs . contenu ( 'documents' , 'tronque.gtt.verrou' ) , '{"seance":' ) ;
fs . deposer ( 'documents' , 'forme.gtt.verrou' , '{"seance":"seance-a","pid":"7","hote":"poste-epreuve","depuis":"x"}' ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'forme.gtt.verrou' , 'seance-b' ) , illisible ) ;
const ancien = { seance : 'ancienne' , pid : 4242 , hote : 'poste-epreuve' , depuis : '2026-01-01T08:00:00+00:00' } ;
fs . deposer ( 'documents' , 'mort.gtt.verrou' , JSON . stringify ( ancien ) ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'mort.gtt.verrou' , 'seance-b' ) , {
pris : false ,
seance : 'ancienne' ,
depuis : '2026-01-01T08:00:00+00:00' ,
vivant : false ,
} ) ;
} ) ;
test ( "sans verrou disponible, verrouiller rend pris sans rien écrire et deverrouiller n'efface rien" , async ( ) => {
const fs = preparer ( { verrouDisponible : false } ) ;
fs . deposer ( 'documents' , 'soiree.gtt.verrou' , '{"seance":"seance-a"}' ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'neuf.gtt.verrou' , 'seance-a' ) , { pris : true } ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'neuf.gtt.verrou' , 'seance-b' ) , { pris : true } ) ;
await fs . deverrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'soiree.gtt.verrou' ] ) ;
} ) ;
} ) ;
describe ( 'fichiers simulés : compteurs, préparation, horloge' , ( ) => {
test ( "chaque appel d'une primitive de lecture compte une lecture et chaque appel d'une primitive d'écriture une écriture, refusé compris ; les autres ne comptent pas" , async ( ) => {
const fs = preparer ( ) ;
await fs . ecrireAtomique ( DOC , 'a.txt' , 'a' ) ;
await fs . ajouterLigne ( DOC , 'journal.jsonl' , 'ligne' ) ;
await fs . creerDossier ( DOC , 'corbeille' ) ;
await fs . deplacer ( DOC , 'a.txt' , 'corbeille/a.txt' ) ;
await fs . supprimer ( DOC , 'corbeille/a.txt' ) ;
await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) ;
await fs . deverrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-a' ) ;
await fs . sonder ( DOC ) ;
await assert . rejects ( fs . ecrireAtomique ( DOC , '../a.txt' , 'a' ) ) ;
await fs . lireTexte ( DOC , 'journal.jsonl' ) ;
await fs . lister ( DOC , '' ) ;
await assert . rejects ( fs . lireTexte ( DOC , 'absent.txt' ) ) ;
await fs . emplacements ( ) ;
await fs . racines ( ) ;
await fs . typeSupport ( DOC ) ;
await fs . choisirDossier ( ) ;
await fs . ouvrirDansExplorateur ( DOC ) ;
await fs . choisirFichierAImporter ( ) ;
await fs . enregistrerSous ( 'liste.csv' , new Uint8Array ( [ 0x61 ] ) ) ;
fs . contenu ( 'documents' , 'journal.jsonl' ) ;
fs . deposer ( 'documents' , 'pose.txt' , 'posé' ) ;
assert . deepEqual ( fs . compteurs , { ecritures : 9 , lectures : 3 } ) ;
} ) ;
test ( 'deposer et deposerDossier posent sans compter, dossiers parents compris ; contenu lit sans compter et rend null hors d\u{2019}un fichier' , async ( ) => {
const fs = creerFichiersSimules ( ) ;
fs . deposerDossier ( 'documents' , 'corbeille/2026-01-02_03-04-05' ) ;
fs . deposer ( 'documents' , 'archives/2025/soiree.gtt.json' , 'état' , 5 ) ;
assert . equal ( fs . contenu ( 'documents' , 'archives/2025/soiree.gtt.json' ) , 'état' ) ;
assert . equal ( fs . contenu ( 'documents' , 'ARCHIVES/2025/SOIREE.GTT.JSON' ) , 'état' ) ;
assert . equal ( fs . contenu ( 'documents' , 'archives/2025' ) , null ) ;
assert . equal ( fs . contenu ( 'documents' , 'absent.txt' ) , null ) ;
assert . deepEqual ( fs . compteurs , { ecritures : 0 , lectures : 0 } ) ;
assert . deepEqual ( await fs . lister ( DOC , 'archives/2025' ) , [
{ nom : 'soiree.gtt.json' , type : 'fichier' , taille : 5 , modifie : 5 } ,
] ) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'archives' , 'corbeille' ] ) ;
assert . equal ( erreurDe ( ( ) => fs . deposer ( 'inconnue' , 'x.txt' , 'x' ) ) . code , 'CHEMIN_REFUSE' ) ;
assert . equal ( erreurDe ( ( ) => fs . pannes . ecriture ( 'inconnue' ) ) . code , 'CHEMIN_REFUSE' ) ;
} ) ;
test ( "modifie suit l'horloge du disque : chaque écriture l'avance ; deposer pose l'instant courant, ou celui qu'on lui donne" , async ( ) => {
const fs = preparer ( ) ;
fs . deposer ( 'documents' , 'a.txt' , 'a' ) ;
fs . deposer ( 'documents' , 'b.txt' , 'b' ) ;
fs . deposer ( 'documents' , 'c.txt' , 'c' , 7 ) ;
await fs . ecrireAtomique ( DOC , 'd.txt' , 'd' ) ;
await fs . ecrireAtomique ( DOC , 'e.txt' , 'e' ) ;
const [ a , b , c , d , e ] = await fs . lister ( DOC , '' ) ;
assert . equal ( a . modifie , b . modifie ) ;
assert . equal ( c . modifie , 7 ) ;
assert . ok ( d . modifie > a . modifie , ` ${ d . modifie } > ${ a . modifie } ` ) ;
assert . ok ( e . modifie > d . modifie , ` ${ e . modifie } > ${ d . modifie } ` ) ;
} ) ;
} ) ;
[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 ( 'fichiers simulés : octets déposés et décodage strict' , ( ) => {
// « Selvi » puis l'octet E8, un « è » enregistré en windows-1252 : une suite
// qui n'est pas de l'UTF-8.
const HORS _UTF8 = new Uint8Array ( [ 0x7b , 0x22 , 0x53 , 0x65 , 0x6c , 0x76 , 0x69 , 0xe8 , 0x22 , 0x7d ] ) ;
test ( "lireTexte refuse des octets qui ne sont pas de l'UTF-8 : LECTURE {chemin, dossier, cause: UTF8_INVALIDE, octets}, comptée, ses octets une copie dont le message ne dit que le nombre ; lister en donne la taille, octets les rend tels quels" , async ( ) => {
const fs = preparer ( ) ;
fs . deposerOctets ( 'documents' , 'archives/soiree.gtt.json' , HORS _UTF8 ) ;
await echoue ( fs . lireTexte ( DOC , 'archives/soiree.gtt.json' ) , 'LECTURE' , {
chemin : 'archives/soiree.gtt.json' ,
dossier : ` ${ DOCUMENTS } \\ archives ` ,
cause : 'UTF8_INVALIDE' ,
octets : HORS _UTF8 ,
} ) ;
const refus = await rejette ( fs . lireTexte ( DOC , 'archives/soiree.gtt.json' ) , 'LECTURE' ) ;
assert . equal (
refus . message ,
` LECTURE {"chemin":"archives/soiree.gtt.json","dossier": ${ JSON . stringify ( ` ${ DOCUMENTS } \\ archives ` ) } ,"cause":"UTF8_INVALIDE","octets":"10 octets"} ` ,
) ;
refus . details . octets [ 0 ] = 0 ;
assert . deepEqual ( fs . octets ( 'documents' , 'archives/soiree.gtt.json' ) , HORS _UTF8 ) ;
assert . deepEqual ( fs . compteurs , { ecritures : 0 , lectures : 2 } ) ;
assert . deepEqual ( await fs . lister ( DOC , 'archives' ) , [
{ nom : 'soiree.gtt.json' , type : 'fichier' , taille : 10 , modifie : ( await fs . lister ( DOC , 'archives' ) ) [ 0 ] . modifie } ,
] ) ;
assert . deepEqual ( fs . octets ( 'documents' , 'archives/soiree.gtt.json' ) , HORS _UTF8 ) ;
assert . equal ( erreurDe ( ( ) => fs . contenu ( 'documents' , 'archives/soiree.gtt.json' ) ) . name , 'TypeError' ) ;
} ) ;
test ( "des octets UTF-8 déposés se lisent tels quels, marque d'ordre d'octets comprise ; un ajout en fin garde les octets d'avant, une écriture atomique les remplace" , async ( ) => {
const fs = preparer ( ) ;
fs . deposerOctets ( 'documents' , 'bom.csv' , new Uint8Array ( [ 0xef , 0xbb , 0xbf , 0xc3 , 0xa9 , 0x0d , 0x0a ] ) ) ;
assert . equal ( await fs . lireTexte ( DOC , 'bom.csv' ) , '\u{FEFF}\u{E9}\r\n' ) ;
assert . equal ( fs . contenu ( 'documents' , 'bom.csv' ) , '\u{FEFF}\u{E9}\r\n' ) ;
fs . deposerOctets ( 'documents' , 'journal.jsonl' , HORS _UTF8 ) ;
await fs . ajouterLigne ( DOC , 'journal.jsonl' , '\u{E9}' ) ;
assert . deepEqual ( fs . octets ( 'documents' , 'journal.jsonl' ) , new Uint8Array ( [ ... HORS _UTF8 , 0xc3 , 0xa9 , 0x0a ] ) ) ;
await fs . ecrireAtomique ( DOC , 'journal.jsonl' , 'neuf' ) ;
assert . equal ( await fs . lireTexte ( DOC , 'journal.jsonl' ) , 'neuf' ) ;
assert . deepEqual ( fs . octets ( 'documents' , 'journal.jsonl' ) , new Uint8Array ( [ 0x6e , 0x65 , 0x75 , 0x66 ] ) ) ;
assert . equal ( fs . octets ( 'documents' , 'absent.txt' ) , null ) ;
assert . equal ( erreurDe ( ( ) => fs . deposerOctets ( 'documents' , 'x.txt' , 'x' ) ) . name , 'TypeError' ) ;
} ) ;
test ( "un verrou aux octets hors UTF-8 se lit comme un verrou illisible" , async ( ) => {
const fs = preparer ( ) ;
fs . deposerOctets ( 'documents' , 'soiree.gtt.verrou' , HORS _UTF8 ) ;
assert . deepEqual ( await fs . verrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-b' ) , {
pris : false ,
seance : null ,
depuis : null ,
vivant : null ,
} ) ;
await fs . deverrouiller ( DOC , 'soiree.gtt.verrou' , 'seance-b' ) ;
assert . deepEqual ( fs . octets ( 'documents' , 'soiree.gtt.verrou' ) , HORS _UTF8 ) ;
} ) ;
} ) ;
[ADD] storage: file-system interface, failing test double, contract
Storage code receives its file system as a parameter (§ 13.4): one
interface, and here the implementation tests inject. It keeps files in
memory, shares a disk between two instances to replay a restart, and
fails on demand: a refused rename that leaves the target intact, writes
refused from a moment on, a process killed after n writes, a failing
probe, a removable medium. One contract suite, its assertions passed in,
will run unchanged on the Electron and web implementations.
Checked: 87 tests, the contract suite run on the test double.
--- FR ---
[ADD] stockage : interface du système de fichiers, double, contrat
Le code du stockage reçoit son système de fichiers en paramètre
(§ 13.4) : une interface, et ici l'implémentation que les épreuves
injectent. Elle garde les fichiers en mémoire, partage un disque entre
deux instances pour rejouer un redémarrage, et tombe en panne à la
demande : renommage refusé qui laisse la cible intacte, écritures
refusées à partir d'un instant, processus tué après n écritures, sonde
qui échoue, support amovible. Une suite de contrat, ses assertions
reçues en paramètre, servira telle quelle sous Electron et sous web.
Vérifié : 87 épreuves, la suite de contrat jouée sur le double.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:25 -04:00
describe ( 'fichiers simulés : préconditions et dossiers' , ( ) => {
test ( "ajouterLigne refuse une ligne qui porte une fin de ligne ; ecrireAtomique, un texte qui n'est pas une chaîne ; verrouiller, une séance vide" , async ( ) => {
const fs = preparer ( ) ;
await assert . rejects ( fs . ajouterLigne ( DOC , 'journal.jsonl' , 'a\nb' ) , TypeError ) ;
await assert . rejects ( fs . ajouterLigne ( DOC , 'journal.jsonl' , 'a\r' ) , TypeError ) ;
await assert . rejects ( fs . ecrireAtomique ( DOC , 'etat.json' , new Uint8Array ( [ 0x61 ] ) ) , TypeError ) ;
await assert . rejects ( fs . verrouiller ( DOC , 'soiree.gtt.verrou' , '' ) , TypeError ) ;
assert . deepEqual ( await fs . lister ( DOC , '' ) , [ ] ) ;
} ) ;
test ( 'un dossier ne se lit, ne s\u{2019}écrit, ne se déplace ni ne se supprime comme un fichier, et un fichier ne se liste pas' , async ( ) => {
const fs = preparer ( ) ;
fs . deposerDossier ( 'documents' , 'corbeille' ) ;
fs . deposer ( 'documents' , 'soiree.gtt.json' , 'état' ) ;
const refus = ( chemin , cause , dossier = DOCUMENTS ) => ( { chemin , dossier , cause } ) ;
await echoue ( fs . lireTexte ( DOC , 'corbeille' ) , 'ABSENT' , { chemin : 'corbeille' } ) ;
await echoue ( fs . lister ( DOC , 'soiree.gtt.json' ) , 'ABSENT' , { chemin : 'soiree.gtt.json' } ) ;
await echoue ( fs . ecrireAtomique ( DOC , 'corbeille' , 'x' ) , 'ECRITURE' , refus ( 'corbeille' , 'EISDIR' ) ) ;
await echoue ( fs . ajouterLigne ( DOC , 'corbeille' , 'x' ) , 'ECRITURE' , refus ( 'corbeille' , 'EISDIR' ) ) ;
await echoue ( fs . verrouiller ( DOC , 'corbeille' , 'seance-a' ) , 'ECRITURE' , refus ( 'corbeille' , 'EISDIR' ) ) ;
await echoue ( fs . deplacer ( DOC , 'corbeille' , 'ailleurs' ) , 'ECRITURE' , refus ( 'corbeille' , 'EISDIR' ) ) ;
await echoue ( fs . supprimer ( DOC , 'corbeille' ) , 'ECRITURE' , refus ( 'corbeille' , 'EISDIR' ) ) ;
await echoue ( fs . creerDossier ( DOC , 'soiree.gtt.json' ) , 'ECRITURE' , refus ( 'soiree.gtt.json' , 'EEXIST' ) ) ;
await echoue (
fs . creerDossier ( DOC , 'soiree.gtt.json/sous' ) ,
'ECRITURE' ,
refus ( 'soiree.gtt.json/sous' , 'ENOTDIR' , ` ${ DOCUMENTS } \\ soiree.gtt.json ` ) ,
) ;
assert . deepEqual ( noms ( await fs . lister ( DOC , '' ) ) , [ 'corbeille' , 'soiree.gtt.json' ] ) ;
} ) ;
[ADD] storage: copy, rename, import, delete events; local settings
§ 8.7 operations reuse the event store's pairing and alignment instead of
copying them. Renaming writes the complete pair under the new name — the
store now writes a gesture with the pair it writes elsewhere — before
deleting the old one, so no cut leaves zero pairs; an unchanged name writes
nothing. Deleting moves state, .precedent, journal, lock and leftovers into a
dated trash, matching names as the case-insensitive disk does. creerDossier
creates only the root it is aimed at, so a vanished working folder never
comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings;
its deferred writer always keeps the last demand.
Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %.
--- FR ---
[ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux
Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au
lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom —
le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant
de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom
inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et
restes dans une corbeille datée, en comparant les noms comme le disque
insensible à la casse. creerDossier ne crée que la racine visée : un dossier
de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux
réglages du § 8.5 ; son écrivain différé garde la dernière demande.
Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
test ( "creerDossier de '' crée la racine, parents compris ; sous une racine absente, un autre chemin lève ECRITURE, ENOENT, sans rien créer" , async ( ) => {
const fs = creerFichiersSimules ( { executable : 'E:\\soirees' } ) ;
await echoue ( fs . creerDossier ( PORTABLE , 'corbeille/2026-01-02_03-04-05' ) , 'ECRITURE' , {
chemin : 'corbeille/2026-01-02_03-04-05' ,
dossier : ` ${ DATA } \\ corbeille ` ,
cause : 'ENOENT' ,
} ) ;
await echoue ( fs . lister ( PORTABLE , '' ) , 'ABSENT' , { chemin : '' } ) ;
await fs . creerDossier ( PORTABLE , '' ) ;
assert . deepEqual ( await fs . lister ( PORTABLE , '' ) , [ ] ) ;
await fs . creerDossier ( PORTABLE , 'corbeille/2026-01-02_03-04-05' ) ;
assert . deepEqual ( noms ( await fs . lister ( PORTABLE , 'corbeille' ) ) , [ '2026-01-02_03-04-05' ] ) ;
} ) ;
[ADD] storage: file-system interface, failing test double, contract
Storage code receives its file system as a parameter (§ 13.4): one
interface, and here the implementation tests inject. It keeps files in
memory, shares a disk between two instances to replay a restart, and
fails on demand: a refused rename that leaves the target intact, writes
refused from a moment on, a process killed after n writes, a failing
probe, a removable medium. One contract suite, its assertions passed in,
will run unchanged on the Electron and web implementations.
Checked: 87 tests, the contract suite run on the test double.
--- FR ---
[ADD] stockage : interface du système de fichiers, double, contrat
Le code du stockage reçoit son système de fichiers en paramètre
(§ 13.4) : une interface, et ici l'implémentation que les épreuves
injectent. Elle garde les fichiers en mémoire, partage un disque entre
deux instances pour rejouer un redémarrage, et tombe en panne à la
demande : renommage refusé qui laisse la cible intacte, écritures
refusées à partir d'un instant, processus tué après n écritures, sonde
qui échoue, support amovible. Une suite de contrat, ses assertions
reçues en paramètre, servira telle quelle sous Electron et sous web.
Vérifié : 87 épreuves, la suite de contrat jouée sur le double.
Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 13:12:25 -04:00
} ) ;