Undoing back before a generation restored the whole charge, its prochainsIds included: the next generation numbered its propositions 1 to 3 again, and a label of the abandoned thread such as "retained proposition 2" named another proposition. § 8.9 forbids it: a counter never goes back. A retour now restores the content of the instant and keeps each counter at the larger of its restored and current value, so the next id is fresh. Checked: 1416 node tests; the retour test is red on the previous seance.js. --- FR --- [FIX] séance : un retour ne fait jamais reculer les compteurs Défaire jusqu'avant une génération restituait la charge entière, prochainsIds compris : la génération suivante renumérotait ses propositions de 1 à 3, et un libellé du fil abandonné, comme « retenu la proposition 2 », désignait une autre proposition. Le § 8.9 l'interdit : un compteur ne recule jamais. Un retour restitue désormais le contenu de l'instant et porte chaque compteur au plus grand de sa valeur restituée et de sa valeur courante. Vérifié : 1416 épreuves node ; l'épreuve du retour est rouge sur l'ancien code. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
1209 lines
61 KiB
JavaScript
1209 lines
61 KiB
JavaScript
// © 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: {} }]);
|
|
assert.deepEqual(seance.avertissements, enRetenu.avertissements);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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 »'],
|
|
],
|
|
);
|
|
// 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']],
|
|
);
|
|
});
|
|
|
|
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.
|
|
async function surPortable() {
|
|
const fs = creerFichiersSimules({ executable: 'E:\\soirees' });
|
|
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]);
|
|
});
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
});
|
|
});
|