Files
gacha-event-tracker/test/reader-store.test.ts
T
Lucas WintherandClaude Opus 5.5 50c6e91b00 feat: the redesigned Event Clock
The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 01:55:16 +02:00

218 lines
6.6 KiB
TypeScript

import { MemoryStorage } from "./helpers/reader.ts";
import { expect, test } from "bun:test";
import {
createReaderStore,
initializeReader,
} from "../src/client/state/reader/store.ts";
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
import { READER_KEYS } from "../src/client/state/reader/schema.ts";
const at = "2026-10-16T12:00:00.000Z";
test("two stores rebase unrelated field changes and reject older same-field writes", async () => {
const storage = new MemoryStorage();
const initial = migrateLegacy({}, ["genshin"], at, () => "account:one");
const a = createReaderStore(storage, initial),
b = createReaderStore(storage, initial);
expect(
await a.dispatch({
type: "progress",
accountId: "account:one",
itemId: "genshin:a:2026-10-01",
patch: { status: "doing" },
at,
opId: "a",
}),
).toEqual({ ok: true });
expect(
await b.dispatch({
type: "progress",
accountId: "account:one",
itemId: "genshin:a:2026-10-01",
patch: { note: "Keep me" },
at,
opId: "b",
}),
).toEqual({ ok: true });
const persisted = JSON.parse(storage.getItem(READER_KEYS.reader)!);
expect(
persisted.data["account:one"].progress["genshin:a:2026-10-01"],
).toMatchObject({ status: "doing", note: "Keep me" });
expect(
await a.dispatch({
type: "progress",
accountId: "account:one",
itemId: "genshin:a:2026-10-01",
patch: { status: "done" },
at: "2026-10-15T12:00:00.000Z",
opId: "c",
}),
).toEqual({ ok: false, reason: "conflict" });
});
test("operation replay is idempotent, including toggles", async () => {
const storage = new MemoryStorage(),
s = createReaderStore(
storage,
migrateLegacy({}, ["genshin"], at, () => "account:one"),
);
const action = {
type: "day" as const,
accountId: "account:one",
itemId: "dailies:genshin",
revisionId: "initial",
day: "2026-10-16",
checked: true,
at,
opId: "same",
};
await s.dispatch(action);
const rev = s.getSnapshot().revision;
await s.dispatch(action);
expect(s.getSnapshot().revision).toBe(rev);
});
test("failed migration preserves legacy bytes and never records success", () => {
const storage = new MemoryStorage();
storage.setItem("gacha-tracker:v1:prefs", '{"onboarded":true}');
storage.fail = READER_KEYS.reader;
const result = initializeReader(
storage,
["genshin"],
at,
() => "account:one",
);
expect(result.ok).toBe(false);
expect(storage.getItem("gacha-tracker:v1:prefs")).toBe('{"onboarded":true}');
expect(storage.getItem(READER_KEYS.reader)).toBeNull();
expect(storage.getItem(READER_KEYS.migrationRecovery)).not.toBeNull();
});
test("quota failure does not publish a successful in-memory edit", async () => {
const storage = new MemoryStorage(),
s = createReaderStore(
storage,
migrateLegacy({}, ["genshin"], at, () => "account:one"),
);
storage.fail = READER_KEYS.reader;
expect(
await s.dispatch({
type: "prefs",
patch: { interface: "relaxed" },
at,
opId: "a",
}),
).toEqual({ ok: false, reason: "quota" });
expect(s.getSnapshot().prefs.interface).toBe("structured");
});
test("new-event markers seed accounts together rather than queueing per-account writes", async () => {
const storage = new MemoryStorage();
let n = 0;
const s = createReaderStore(
storage,
migrateLegacy({}, ["genshin", "hsr"], at, () => `account:test${++n}`),
);
await s.dispatch({
type: "seenBatch",
entries: [
{ id: "account:test1", ids: ["genshin:a"] },
{ id: "account:test2", ids: ["hsr:a"] },
],
at,
opId: "batch",
});
expect(s.getSnapshot().revision).toBe(1);
expect(s.getSnapshot().eventSeen).toEqual({
"account:test1": ["genshin:a"],
"account:test2": ["hsr:a"],
});
});
test("stale tabs preserve independent account routine choices", async () => {
const storage = new MemoryStorage(),
initial = migrateLegacy({}, ["genshin"], at, () => "account:one");
storage.setItem(READER_KEYS.reader, JSON.stringify(initial));
const a = createReaderStore(storage, initial),
b = createReaderStore(storage, initial),
account = initial.accounts["account:one"]!;
await a.dispatch({
type: "account",
account: {
...account,
routines: {
...account.routines,
first: { followed: true, checklist: true, timeline: false },
},
},
at,
opId: "first",
});
await b.dispatch({
type: "account",
account: {
...account,
routines: {
...account.routines,
second: { followed: true, checklist: false, timeline: true },
},
},
at,
opId: "second",
});
expect(b.getSnapshot().accounts[account.id]!.routines.first).toHaveProperty(
"followed",
true,
);
expect(b.getSnapshot().accounts[account.id]!.routines.second).toHaveProperty(
"followed",
true,
);
});
test("resuming a retired account removes its retirement without erasing history", async () => {
const storage = new MemoryStorage(),
initial = migrateLegacy({}, ["genshin"], at, () => "account:one");
initial.accounts["account:one"]!.retiredAt = at;
storage.setItem(READER_KEYS.reader, JSON.stringify(initial));
const store = createReaderStore(storage, initial),
account = { ...initial.accounts["account:one"]! };
delete account.retiredAt;
expect(
await store.dispatch({ type: "account", account, at, opId: "resume" }),
).toEqual({ ok: true });
expect(store.getSnapshot().accounts[account.id]!.retiredAt).toBeUndefined();
});
test("stale tabs preserve independent pins on the same routine", async () => {
const storage = new MemoryStorage(),
initial = migrateLegacy({}, ["genshin"], at, () => "account:one"),
account = initial.accounts["account:one"]!;
account.routines.test = { followed: true, checklist: false, timeline: false };
storage.setItem(READER_KEYS.reader, JSON.stringify(initial));
const a = createReaderStore(storage, initial),
b = createReaderStore(storage, initial);
await a.dispatch({
type: "account",
account: {
...account,
routines: {
...account.routines,
test: { ...account.routines.test!, checklist: true },
},
},
at,
opId: "checklist",
});
await b.dispatch({
type: "account",
account: {
...account,
routines: {
...account.routines,
test: { ...account.routines.test!, timeline: true },
},
},
at,
opId: "timeline",
});
expect(b.getSnapshot().accounts[account.id]!.routines.test).toEqual({
followed: true,
checklist: true,
timeline: true,
});
});