Files
gacha-event-tracker/test/reader-open.test.ts
T
Lucas WintherandClaude Opus 5.5 94fba4d7b5 fix(reader): open the save under the reader lock, and cap automatic backups
Follow-up to the 2026-10-18 review of the v1→v2 migration. Start-up
(finishing a pending restore, then migrating or reconciling) now runs under
the lock every save takes, so a second tab's save cannot be overwritten;
where no lock exists it opens unlocked, as before. The automatic backups get
a size cap like Back up now's: past it the oldest are dropped, never the
newest, leaving the main save room to grow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 05:09:58 +02:00

74 lines
2.7 KiB
TypeScript

import { expect, test } from "bun:test";
import { openReader } from "../src/client/state/reader/open.ts";
import { READER_KEYS } from "../src/client/state/reader/schema.ts";
import { MemoryStorage } from "./helpers/reader.ts";
const at = "2026-10-18T12:00:00.000Z";
test("start-up reads and writes the save inside the reader lock", async () => {
const s = new MemoryStorage();
const seen: string[] = [];
const locks = {
request: async (name: string, run: () => unknown) => {
seen.push(`lock ${name}`);
const value = await run();
seen.push(`release, saved: ${s.getItem(READER_KEYS.reader) !== null}`);
return value;
},
};
const original = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", {
value: { locks },
configurable: true,
});
try {
const opened = await openReader(s, ["genshin"], at, () => "account:one");
expect(opened.ok).toBe(true);
} finally {
if (original) Object.defineProperty(globalThis, "navigator", original);
else delete (globalThis as { navigator?: unknown }).navigator;
}
expect(seen).toEqual([
"lock gacha-tracker:v2:reader",
"release, saved: true",
]);
});
test("an interrupted restore that cannot be read stops start-up, keeping the save", async () => {
const s = new MemoryStorage();
s.setItem(READER_KEYS.pending, JSON.stringify({ at, payload: {} }));
const opened = await openReader(s, ["genshin"], at, () => "account:one");
expect(opened).toEqual({ ok: false, reason: "restore" });
expect(s.getItem(READER_KEYS.pending)).not.toBeNull();
expect(s.getItem(READER_KEYS.reader)).toBeNull();
});
test("an unreadable save is reported, not replaced", async () => {
const s = new MemoryStorage();
s.setItem(READER_KEYS.reader, JSON.stringify({ version: 2 }));
const opened = await openReader(s, ["genshin"], at, () => "account:one");
expect(opened).toEqual({ ok: false, reason: "open" });
expect(s.getItem(READER_KEYS.reader)).toBe(JSON.stringify({ version: 2 }));
});
test("with no lock available, start-up still opens the save", async () => {
const s = new MemoryStorage();
const original = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", {
value: {
locks: {
request: () => Promise.reject(new Error("unavailable")),
},
},
configurable: true,
});
try {
const opened = await openReader(s, ["genshin"], at, () => "account:one");
expect(opened.ok).toBe(true);
} finally {
if (original) Object.defineProperty(globalThis, "navigator", original);
else delete (globalThis as { navigator?: unknown }).navigator;
}
expect(s.getItem(READER_KEYS.reader)).not.toBeNull();
});