// Rôle : teste les écritures confirmées, échecs successifs et imports concurrents. import { test } from "node:test"; import assert from "node:assert/strict"; import { createPersistenceQueue } from "../website/src/features/toolboxes/storage/persistenceQueue.js"; import { useToolboxActions } from "../website/src/features/toolboxes/useToolboxActions.js"; import { applyModuleDataValue, removeModuleDataKeys } from "../website/src/features/toolboxes/storage/optimisticModuleData.js"; function deferred() { let resolve, reject; const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; }); return { promise, resolve, reject }; } for (const field of ["toolboxes", "links", "moduleData", "toastPosition"]) { for (const outcomes of [[true, true], [true, false], [false, true], [false, false]]) { test(`${field}: confirms only successful writes ${outcomes}`, async () => { let disk = { [field]: "A" }; const errors = []; const queue = createPersistenceQueue(disk, () => {}); const gates = [deferred(), deferred()]; let active = 0; const write = (index) => queue.enqueue( (current) => ({ ...current, [field]: index ? "C" : "B" }), async (next) => { assert.equal(++active, 1); try { await gates[index].promise; disk = next; } finally { active--; } }, (error) => errors.push(error.message) ); const first = write(0); const second = write(1); assert.equal(queue.getCurrent()[field], "C"); outcomes[0] ? gates[0].resolve() : gates[0].reject(new Error("first")); assert.equal(await first, outcomes[0]); assert.equal(queue.getCurrent()[field], "C"); outcomes[1] ? gates[1].resolve() : gates[1].reject(new Error("second")); assert.equal(await second, outcomes[1]); assert.deepEqual(queue.getCurrent(), disk); assert.equal(disk[field], outcomes[1] ? "C" : outcomes[0] ? "B" : "A"); assert.equal(errors.length, outcomes.filter((ok) => !ok).length); }); } } test("failed snapshot cannot undo a later edit or reintroduce a failed earlier write", async () => { const initial = { toolboxes: ["existing"], links: {}, moduleData: { note: "A" } }; let disk = initial; const queue = createPersistenceQueue(initial, () => {}); const gate = deferred(); const earlier = queue.enqueue((s) => ({ ...s, moduleData: { note: "B" } }), () => gate.promise); const snapshot = queue.enqueue( (s) => ({ ...s, toolboxes: ["imported", ...s.toolboxes] }), async () => { throw new Error("abort"); } ); const edit = queue.enqueue((s) => ({ ...s, moduleData: { note: "C" } }), async (next) => { disk = next; }); gate.reject(new Error("quota")); assert.deepEqual(await Promise.all([earlier, snapshot, edit]), [false, false, true]); assert.deepEqual(queue.getCurrent(), { ...initial, moduleData: { note: "C" } }); assert.deepEqual(disk, queue.getCurrent()); }); test("imports merge after file reading and preserve edits queued during the transaction", async () => { const initial = { toolboxes: [{ id: "existing", modules: [] }], links: {}, moduleData: { note: "A" } }; let disk = initial; const queue = createPersistenceQueue(initial, () => {}); const transaction = deferred(); const fileRead = deferred(); const notices = []; const actions = useToolboxActions({ store: { ...initial, persistToolboxSnapshot: (apply) => queue.enqueue(apply, async (next) => { await transaction.promise; disk = next; }) }, notify: (value) => notices.push(value), setConfirmModal: (value) => assert.fail(value.message) }); const imported = actions.importToolbox({ text: () => fileRead.promise }, "game"); await queue.enqueue((s) => ({ ...s, moduleData: { note: "B" } }), async (next) => { disk = next; }); fileRead.resolve(JSON.stringify({ toolbox: { id: "source", name: "Import", modules: [] }, modules: {} })); await Promise.resolve(); assert.equal(notices.length, 0); const edit = queue.enqueue((s) => ({ ...s, moduleData: { ...s.moduleData, note: "C" } }), async (next) => { disk = next; }); transaction.resolve(); const toolbox = await imported; await edit; assert.equal(disk.moduleData.note, "C"); assert.deepEqual(disk.toolboxes.map((t) => t.id), [toolbox.id, "existing"]); assert.equal(disk.links.game, toolbox.id); assert.deepEqual(queue.getCurrent(), disk); assert.equal(notices.length, 1); }); test("failed module removal restores the confirmed value after an earlier failed edit", async () => { const initial = { moduleData: { note: { text: "A" }, other: { text: "Other" } } }; const queue = createPersistenceQueue(initial, () => {}); const first = queue.enqueue( (s) => ({ ...s, moduleData: applyModuleDataValue(s.moduleData, "note", { text: "B" }) }), async () => { throw new Error("quota"); } ); const second = queue.enqueue( (s) => ({ ...s, moduleData: removeModuleDataKeys(s.moduleData, ["note"]) }), async () => { throw new Error("abort"); } ); assert.deepEqual(await Promise.all([first, second]), [false, false]); assert.deepEqual(queue.getCurrent(), initial); }); test("global imports started from the same render both survive", async () => { const initial = { toolboxes: [], links: {}, moduleData: {} }; let disk = initial; const queue = createPersistenceQueue(initial, () => {}); const actions = useToolboxActions({ store: { ...initial, persistToolboxSnapshot: (apply) => queue.enqueue(apply, async (next) => { disk = next; }) }, notify() {}, setConfirmModal: (value) => assert.fail(value.message) }); const file = (id) => ({ text: async () => JSON.stringify({ toolboxes: [{ id, name: id, modules: [{ id: "note", type: "notepad" }] }], links: { [id]: id }, modules: { [`${id}:note`]: { text: id } } }) }); assert.deepEqual(await Promise.all([ actions.importAllToolboxes(file("one")), actions.importAllToolboxes(file("two")) ]), [true, true]); assert.equal(disk.toolboxes.length, 2); assert.equal(Object.keys(disk.moduleData).length, 2); assert.deepEqual(Object.keys(disk.links).sort(), ["one", "two"]); assert.deepEqual(queue.getCurrent(), disk); }); test("failed toolbox deletion preserves registry links and modules together", async () => { const initial = { toolboxes: [{ id: "existing", modules: [{ id: "note", type: "notepad" }] }], links: { game: "existing" }, moduleData: { "existing:note": { text: "Keep" } } }; const queue = createPersistenceQueue(initial, () => {}); const actions = useToolboxActions({ store: { ...initial, persistToolboxSnapshot: (apply) => queue.enqueue(apply, async () => { throw new Error("abort"); }) } }); const deleted = actions.deleteToolbox("existing"); assert.deepEqual(queue.getCurrent(), { toolboxes: [], links: {}, moduleData: {} }); assert.equal(await deleted, false); assert.deepEqual(queue.getCurrent(), initial); });