import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { beforeEach, test } from "node:test"; import ts from "typescript"; // No indexedDB in node: the module falls back to its in-memory store, which is what we exercise. const source = readFileSync(new URL("../src/player/saves.ts", import.meta.url), "utf8"); const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }); const saves = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`); let remote, calls; beforeEach(async () => { for (const s of await saves.localAll()) await saves.localRemove(s.versionId, s.slot); remote = new Map(); calls = []; globalThis.fetch = async (url, init = {}) => { const method = init.method ?? "GET"; calls.push(`${method} ${url}`); const m = /saves\/(\w+)$/.exec(url); const json = (body, ok = true) => ({ ok, json: async () => body }); if (!m) return json([...remote.entries()].map(([slot, s]) => ({ slot, title: s.title, updated_at: s.updated_at }))); if (method === "PUT") { const body = JSON.parse(init.body); const row = { ...body, updated_at: "2030-01-01T00:00:00Z" }; remote.set(m[1], row); return json(row); } return remote.has(m[1]) ? json(remote.get(m[1])) : json({}, false); }; }); test("without the cloud nothing touches the network", async () => { const api = saves.makeSaveApi(1, false); await api.put("a", "t", { x: 1 }); assert.deepEqual(await api.get("a"), { x: 1 }); assert.equal((await api.list()).length, 1); assert.deepEqual(calls, []); await api.remove("a"); assert.equal(await api.get("a"), null); }); test("saves are separated by game version", async () => { await saves.makeSaveApi(1, false).put("a", "one", { v: 1 }); await saves.makeSaveApi(2, false).put("a", "two", { v: 2 }); assert.deepEqual(await saves.makeSaveApi(1, false).get("a"), { v: 1 }); assert.equal((await saves.makeSaveApi(2, false).list())[0].title, "two"); }); test("cloud put keeps a local copy and survives a server outage", async () => { const api = saves.makeSaveApi(1, true); await api.put("a", "t", { x: 1 }); assert.deepEqual(remote.get("a").data, { x: 1 }); globalThis.fetch = async () => { throw new Error("offline"); }; await api.put("b", "t", { y: 2 }); assert.deepEqual(await api.get("b"), { y: 2 }); assert.equal((await api.list()).length, 2); }); test("the newer copy wins when loading", async () => { remote.set("a", { title: "cloud", data: { from: "cloud" }, updated_at: "2031-01-01T00:00:00Z" }); await saves.localPut({ versionId: 1, slot: "a", title: "local", data: { from: "local" }, updated_at: "2020-01-01T00:00:00Z" }); assert.deepEqual(await saves.makeSaveApi(1, true).get("a"), { from: "cloud" }); assert.equal((await saves.localGet(1, "a")).title, "cloud"); }); test("export and import round-trip and reject foreign files", async () => { await saves.makeSaveApi(3, false).put("quick", "q", { z: 3 }); const file = await saves.exportSaves(); await saves.localRemove(3, "quick"); assert.equal(await saves.importSaves(file), 1); assert.deepEqual(await saves.makeSaveApi(3, false).get("quick"), { z: 3 }); assert.equal(await saves.importSaves(file), 0); await assert.rejects(saves.importSaves('{"a":1}')); });