mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-10-22 05:54:10 -04:00
fix(stats): finalize snapshots on the verse-day window
A verse date is a player's local calendar day, spanning D-1 10:00Z (UTC+14) through D+1 12:00Z (UTC-12). The old UTC end-of-day cutoff both dropped late-night finishes in negative timezones and could persist a partial day. - Scope snapshot loads by the verse date column; use completedBefore only to pin an in-progress point-in-time build. - Gate persistence on isDayClosed (D+1 12:00Z); open days are computed live and never written. - Build the cron snapshot for latestClosedDate instead of EST yesterday, and read the same date for the community page. - Add all-time best single day to snapshot totals. - Bump PAYLOAD_SCHEMA_VERSION to 4 and extend snapshot/window tests.
This commit is contained in:
1 parent
45ffa39f2a
commit
a562ee3cc9
10 files changed
+198
-52
No files matched your search
@@ -11,9 +11,11 @@ import {
|
||||
computePayload,
|
||||
computePlayerState,
|
||||
ensureSnapshot,
|
||||
loadStatsData,
|
||||
PAYLOAD_SCHEMA_VERSION,
|
||||
type LoadedData
|
||||
} from '$lib/server/stats/snapshot';
|
||||
import { endOfVerseWindow, isDayClosed, latestClosedDate } from '$lib/server/stats/dates';
|
||||
import { SETTINGS_DEFAULTS, type CompletionRow } from '$lib/types/stats';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
@@ -72,6 +74,20 @@ describe('computePayload point-in-time cutoff', () => {
|
||||
expect(payload.kpis.completionRate).not.toBeNull();
|
||||
});
|
||||
|
||||
test('all-time best day picks the highest count and ignores future days', () => {
|
||||
const payload = computePayload('2026-09-10', settings, {
|
||||
...data,
|
||||
completions: [
|
||||
c('A', '2026-09-01'),
|
||||
c('A', '2026-09-02'),
|
||||
c('B', '2026-09-02'),
|
||||
c('C', '2026-09-02'),
|
||||
c('D', '2026-09-11') // future relative to asOfDate
|
||||
]
|
||||
});
|
||||
expect(payload.totals.bestDay).toEqual({ date: '2026-09-02', count: 3 });
|
||||
});
|
||||
|
||||
test('payload has all lab sections', () => {
|
||||
const payload = computePayload('2026-09-10', settings, data);
|
||||
expect(payload.retention.cohorts.length).toBeGreaterThan(0);
|
||||
@@ -138,4 +154,57 @@ describe('snapshot persistence', () => {
|
||||
const snap = await ensureSnapshot('2026-01-10', {}, testDb);
|
||||
expect(snap.payload.meta?.schemaVersion).toBe(PAYLOAD_SCHEMA_VERSION);
|
||||
});
|
||||
|
||||
test('groups completions by verse date, not a UTC 24h window', async () => {
|
||||
// A player in a UTC-negative timezone finishing "Jan 10" late at night
|
||||
// lands on Jan 11 04:00Z. The old UTC end-of-day cutoff dropped them.
|
||||
await testDb.insert(dailyCompletions).values({
|
||||
id: crypto.randomUUID(),
|
||||
anonymousId: 'late',
|
||||
date: '2026-01-10',
|
||||
guessCount: 4,
|
||||
completedAt: new Date('2026-01-11T04:00:00Z')
|
||||
});
|
||||
const { snapshot } = await buildSnapshot('2026-01-10', { kind: 'lazy' }, testDb);
|
||||
expect(snapshot.payload.activity.dau).toBe(1);
|
||||
});
|
||||
|
||||
test('isDayClosed gates persistence for in-progress days', async () => {
|
||||
await seed();
|
||||
const open = new Date().toISOString().slice(0, 10);
|
||||
await buildSnapshot(open, { kind: 'lazy' }, testDb);
|
||||
await buildSnapshot(open, { kind: 'lazy', replace: true }, testDb);
|
||||
const live = await ensureSnapshot(open, {}, testDb);
|
||||
expect(live.date).toBe(open);
|
||||
// None of the open-day builds may leave a persisted row behind.
|
||||
expect(await testDb.select().from(statsSnapshot)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verse day window', () => {
|
||||
test('closes at D+1 12:00Z (UTC-12 has finished the local day)', () => {
|
||||
expect(endOfVerseWindow('2026-01-10').toISOString()).toBe('2026-01-11T12:00:00.000Z');
|
||||
expect(isDayClosed('2026-01-10', new Date('2026-01-11T11:59:59Z'))).toBe(false);
|
||||
expect(isDayClosed('2026-01-10', new Date('2026-01-11T12:00:00Z'))).toBe(true);
|
||||
});
|
||||
|
||||
test('latestClosedDate reflects the window lag', () => {
|
||||
expect(latestClosedDate(new Date('2026-01-11T11:00:00Z'))).toBe('2026-01-09');
|
||||
expect(latestClosedDate(new Date('2026-01-11T13:00:00Z'))).toBe('2026-01-10');
|
||||
});
|
||||
|
||||
test('completedBefore pins a point-in-time build', async () => {
|
||||
await testDb.delete(analyticsEvents);
|
||||
await testDb.delete(dailyCompletions);
|
||||
await testDb.insert(dailyCompletions).values([
|
||||
{ id: crypto.randomUUID(), anonymousId: 'early', date: '2026-01-10', guessCount: 1, completedAt: new Date('2026-01-10T05:00:00Z') },
|
||||
{ id: crypto.randomUUID(), anonymousId: 'late', date: '2026-01-10', guessCount: 2, completedAt: new Date('2026-01-11T04:00:00Z') }
|
||||
]);
|
||||
const pinned = await loadStatsData('2026-01-10', testDb, {
|
||||
completedBefore: new Date('2026-01-10T12:00:00Z')
|
||||
});
|
||||
expect(pinned.completions.map((c) => c.anonymousId)).toEqual(['early']);
|
||||
const full = await loadStatsData('2026-01-10', testDb);
|
||||
expect(full.completions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user