Files
gacha-event-tracker/test/reader-migration.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

121 lines
3.6 KiB
TypeScript

import { expect, test } from "bun:test";
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
import { ReaderStateSchema } from "../src/client/state/reader/schema.ts";
const at = "2026-10-16T12:00:00.000Z";
const raw = {
"gacha-tracker:v1:prefs": JSON.stringify({
region: "europe",
view: "timeline",
theme: "light",
hiddenGames: ["hsr"],
gameOrder: ["genshin", "hsr"],
showRoutines: true,
routinesHidden: ["routine:genshin:weekly-bosses"],
}),
"gacha-tracker:v1:progress": JSON.stringify({
"genshin:test:2026-10-01": { status: "done", at, goal: { future: true } },
"foreign-id": { note: "keep", at },
}),
"gacha-tracker:v1:daily": JSON.stringify({
"dailies:genshin": { days: ["2026-10-15"], at },
}),
"gacha-tracker:v1:tasks": JSON.stringify({
"mytask:abcdef": {
id: "mytask:abcdef",
game: "genshin",
title: "Mine",
period: { unit: "week", startsOn: 1 },
measure: { kind: "days", required: 1 },
at,
updatedAt: at,
future: 42,
},
}),
"gacha-tracker:v1:amounts": JSON.stringify({
"mytask:abcdef": { "2026-10-12": { value: 7, at } },
}),
};
test("migration preserves view, region, game choices and exact history keys", () => {
let n = 0;
const state = migrateLegacy(
raw,
["genshin", "hsr"],
at,
() => `account:test${++n}`,
);
expect(ReaderStateSchema.safeParse(state).success).toBe(true);
expect(state.prefs.view).toBe("timeline");
expect(state.prefs.hiddenGames).toEqual(["hsr"]);
const account = Object.values(state.accounts).find(
(a) => a.game === "genshin",
)!;
expect(account.region).toBe("europe");
const data = state.data[account.id]!;
expect(data.progress["genshin:test:2026-10-01"]).toEqual({
status: "done",
at,
goal: { future: true },
});
expect(data.daily[account.clocks[0]!.id]?.["dailies:genshin"]).toEqual({
days: ["2026-10-15"],
at,
});
expect(data.amounts[account.clocks[0]!.id]?.["mytask:abcdef"]).toEqual({
"2026-10-12": { value: 7, at },
});
expect(account.routines["routine:genshin:weekly-bosses"]?.followed).toBe(
false,
);
expect(state.tasks["mytask:abcdef"]).toHaveProperty("future", 42);
expect(raw["gacha-tracker:v1:progress"]).toContain("foreign-id");
});
test("completion-only installs and games absent from feed retain their state", () => {
const state = migrateLegacy(
{
"gacha-tracker:v1:completions": JSON.stringify({
"zzz:old:2026-01-01": { at },
}),
},
[],
at,
() => "account:old",
);
const account = Object.values(state.accounts)[0]!;
expect(account.game).toBe("zzz");
expect(state.data[account.id]?.progress["zzz:old:2026-01-01"]).toEqual({
status: "done",
at,
});
});
test("malformed blobs cannot hide valid state in other legacy keys", () => {
const state = migrateLegacy(
{ ...raw, "gacha-tracker:v1:tasks": "broken" },
["genshin"],
at,
() => "account:one",
);
expect(state.tasks).toEqual({});
expect(state.data["account:one"]?.progress).toHaveProperty(
"genshin:test:2026-10-01",
);
});
test("new catalogue games remain switched off for a returning reader", () => {
let n = 0;
const s = migrateLegacy(
{
"gacha-tracker:v1:prefs": JSON.stringify({
knownGames: ["genshin"],
hiddenGames: [],
onboarded: true,
}),
},
["genshin", "hsr"],
at,
() => `account:new${++n}`,
);
expect(s.prefs.hiddenGames).toEqual(["hsr"]);
expect(
Object.values(s.accounts).find((a) => a.game === "hsr")?.intent.events,
).toBe(false);
});