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

133 lines
4.1 KiB
TypeScript

import { expect, test } from "bun:test";
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
import {
appendClockRevision,
resolveReaderClock,
readerDayKey,
readerClockFor,
} from "../src/shared/readerClock.ts";
import { clockFor } from "../src/shared/time.ts";
import { dayKey } from "../src/shared/daily.ts";
import { GAMES } from "../src/shared/games.ts";
import { GachaEvent } from "../src/shared/schema.ts";
const at = "2026-10-16T12:00:00.000Z",
now = Date.parse(at);
const account = () =>
Object.values(
migrateLegacy({}, ["genshin"], at, () => "account:one").accounts,
)[0]!;
test("automatic matches every game and server's existing day-key clock", () => {
for (const game of Object.keys(GAMES))
for (const region of ["asia", "america", "europe"] as const) {
const a = Object.values(
migrateLegacy(
{ "gacha-tracker:v1:prefs": JSON.stringify({ region }) },
[game],
at,
() => "account:one",
).accounts,
)[0]!;
expect(readerDayKey(now, resolveReaderClock(a, now))).toBe(
dayKey(now, region, game),
);
}
});
test("reset override/revert retains revisions, including a backwards current day", () => {
const a = account(),
t = "2026-10-17T03:10:00.000Z";
const custom = appendClockRevision(
a,
{ hour: 5, minute: 30 },
"europe",
t,
"custom",
);
expect(
readerDayKey(Date.parse(t), resolveReaderClock(a, Date.parse(t))),
).toBe("2026-10-17");
expect(
readerDayKey(Date.parse(t), resolveReaderClock(custom, Date.parse(t))),
).toBe("2026-10-16");
expect(resolveReaderClock(custom, Date.parse(t) - 1).revision).toBe(
"initial",
);
const reverted = appendClockRevision(
custom,
null,
"europe",
"2026-10-18T12:00:00.000Z",
"reverted",
);
expect(reverted.clocks).toHaveLength(3);
expect(
resolveReaderClock(reverted, Date.parse("2026-10-18T13:00:00.000Z"))
.resetHour,
).toBe(4);
});
const event = () =>
GachaEvent.parse({
id: "genshin:test:2026-10-01",
game: "genshin",
title: "Test",
type: "story",
summary: null,
startsAt: "2026-10-01T00:00:00.000Z",
startPrecision: "day",
endsAt: "2026-10-20T00:00:00.000Z",
endPrecision: "day",
sourceUrl: "https://example.com",
regionScoped: true,
regionEnds: {},
sourceId: "test",
confidence: 1,
extractionMethod: "parser",
version: 1,
firstSeenAt: at,
updatedAt: at,
status: "published",
});
test("only reset-based boundaries move; exact regional ends remain authoritative", () => {
const a = account(),
custom = appendClockRevision(
a,
{ hour: 5, minute: 30 },
"europe",
at,
"custom",
);
const e = event();
expect(
readerClockFor(e, custom, now).endsMs! - clockFor(e, "europe", now).endsMs!,
).toBe(90 * 60000);
const exact = { ...e, regionEnds: { europe: "2026-10-20T18:00:00.000Z" } };
expect(readerClockFor(exact, custom, now).endsMs).toBe(
Date.parse(exact.regionEnds.europe),
);
expect(
readerClockFor({ ...e, endsAt: null, endPrecision: "unknown" }, custom, now)
.endsMs,
).toBeNull();
});
test("a reader-entered day-precision instant is never moved by a reset override", () => {
const a = appendClockRevision(
account(),
{ hour: 5, minute: 30 },
"europe",
at,
"custom",
);
const e = { ...event(), extractionMethod: "manual" as const };
expect(readerClockFor(e, a, now).endsMs).toBe(Date.parse(e.endsAt!));
});
import { refreshAutomaticClocks } from "../src/client/state/reader/clock.ts";
test("automatic metadata corrections create a revision instead of rewriting history", () => {
const a = account();
a.clocks[0]!.automaticBase.hour = 3;
const state = migrateLegacy({}, ["genshin"], at, () => "account:one");
state.accounts[a.id] = a;
const next = refreshAutomaticClocks(state, at);
expect(next.accounts[a.id]?.clocks).toHaveLength(2);
expect(next.accounts[a.id]?.clocks[0]?.automaticBase.hour).toBe(3);
expect(resolveReaderClock(next.accounts[a.id]!, now).resetHour).toBe(4);
});