sokko-g/tests/persistence-queue.test.mjs
Shinuwa 0acf5ecf21
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
Add toolbox spaces and recover from stale asset bundles
2026-09-29 21:31:49 +02:00

184 lines
7.7 KiB
JavaScript

// 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: {
ready: true,
...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: {
ready: true,
...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("v2 imports reject more than five spaces before writing", async () => {
let writes = 0;
let errorMessage = "";
const actions = useToolboxActions({
store: {
ready: true,
persistToolboxSnapshot: () => { writes += 1; return Promise.resolve(true); }
},
notify() {},
setConfirmModal: (value) => { errorMessage = value.message; }
});
const result = await actions.importToolbox({ text: async () => JSON.stringify({
version: 2,
toolbox: {
id: "overflow",
name: "Overflow",
modules: [],
spaces: Array.from({ length: 6 }, (_, index) => ({ id: `space-${index}`, name: `Space ${index}`, moduleOrder: { one: [], two: [[], []] } }))
},
modules: {}
}) });
assert.equal(result, null);
assert.equal(writes, 0);
assert.match(errorMessage, /plus de 5 espaces/);
});
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: {
ready: true,
...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);
});