Files
mrleo1nidandClaude Sonnet 5.5 47fc121e7d
CI / frontend (push) Successful in 21s
CI / backend (push) Successful in 47s
feat: local browser saves with opt-in cloud sync behind saves.cloud permission
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 12:44:42 +03:00

75 lines
3.3 KiB
JavaScript

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}'));
});