From efcacded6617608580a2fc799bb79ce89399a090 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 28 Mar 2026 00:26:19 -0700 Subject: [PATCH] Factor out mock date helper in tracker tests - reuse a shared `withMockDate` helper for date-sensitive query tests - make monthly rollup assertions key off `videoId` instead of row order --- .../immersion-tracker/__tests__/query.test.ts | 382 +++++++++--------- 1 file changed, 189 insertions(+), 193 deletions(-) diff --git a/src/core/services/immersion-tracker/__tests__/query.test.ts b/src/core/services/immersion-tracker/__tests__/query.test.ts index 763f531..0af1efe 100644 --- a/src/core/services/immersion-tracker/__tests__/query.test.ts +++ b/src/core/services/immersion-tracker/__tests__/query.test.ts @@ -81,6 +81,34 @@ function cleanupDbPath(dbPath: string): void { } } +function withMockDate(fixedDate: Date, run: (realDate: typeof Date) => T): T { + const realDate = Date; + const fixedDateMs = fixedDate.getTime(); + + type MockDateArgs = [any, any, any, any, any, any, any]; + + class MockDate extends Date { + constructor(...args: MockDateArgs) { + if (args.length === 0) { + super(fixedDateMs); + } else { + super(...args); + } + } + + static override now(): number { + return fixedDateMs; + } + } + + globalThis.Date = MockDate as DateConstructor; + try { + return run(realDate); + } finally { + globalThis.Date = realDate; + } +} + test('getSessionSummaries returns sessionId and canonicalTitle', () => { const dbPath = makeDbPath(); const db = new Database(dbPath); @@ -790,127 +818,110 @@ test('getTrendsDashboard keeps local-midnight session buckets separate', () => { test('getTrendsDashboard month grouping spans every touched calendar month and keeps progress monthly', () => { const dbPath = makeDbPath(); const db = new Database(dbPath); - const RealDate = Date; + withMockDate(new Date(2026, 2, 1, 12, 0, 0), (RealDate) => { + try { + ensureSchema(db); + const stmts = createTrackerPreparedStatements(db); + const febVideoId = getOrCreateVideoRecord(db, 'local:/tmp/feb-trends.mkv', { + canonicalTitle: 'Monthly Trends', + sourcePath: '/tmp/feb-trends.mkv', + sourceUrl: null, + sourceType: SOURCE_TYPE_LOCAL, + }); + const marVideoId = getOrCreateVideoRecord(db, 'local:/tmp/mar-trends.mkv', { + canonicalTitle: 'Monthly Trends', + sourcePath: '/tmp/mar-trends.mkv', + sourceUrl: null, + sourceType: SOURCE_TYPE_LOCAL, + }); + const animeId = getOrCreateAnimeRecord(db, { + parsedTitle: 'Monthly Trends', + canonicalTitle: 'Monthly Trends', + anilistId: null, + titleRomaji: null, + titleEnglish: null, + titleNative: null, + metadataJson: null, + }); + linkVideoToAnimeRecord(db, febVideoId, { + animeId, + parsedBasename: 'feb-trends.mkv', + parsedTitle: 'Monthly Trends', + parsedSeason: 1, + parsedEpisode: 1, + parserSource: 'test', + parserConfidence: 1, + parseMetadataJson: null, + }); + linkVideoToAnimeRecord(db, marVideoId, { + animeId, + parsedBasename: 'mar-trends.mkv', + parsedTitle: 'Monthly Trends', + parsedSeason: 1, + parsedEpisode: 2, + parserSource: 'test', + parserConfidence: 1, + parseMetadataJson: null, + }); - class MockDate extends Date { - constructor(...args: any[]) { - type MockDateArgs = [any, any, any, any, any, any, any]; - if (args.length === 0) { - super(new RealDate(2026, 2, 1, 12, 0, 0).getTime()); - } else { - super(...(args as MockDateArgs)); + const febStartedAtMs = new RealDate(2026, 1, 15, 20, 0, 0).getTime(); + const marStartedAtMs = new RealDate(2026, 2, 1, 9, 0, 0).getTime(); + const febSessionId = startSessionRecord(db, febVideoId, febStartedAtMs).sessionId; + const marSessionId = startSessionRecord(db, marVideoId, marStartedAtMs).sessionId; + + for (const [sessionId, startedAtMs, tokensSeen, cardsMined, yomitanLookupCount] of [ + [febSessionId, febStartedAtMs, 100, 2, 3], + [marSessionId, marStartedAtMs, 120, 4, 5], + ] as const) { + stmts.telemetryInsertStmt.run( + sessionId, + startedAtMs + 60_000, + 30 * 60_000, + 30 * 60_000, + 4, + tokensSeen, + cardsMined, + yomitanLookupCount, + yomitanLookupCount, + yomitanLookupCount, + 0, + 0, + 0, + 0, + startedAtMs + 60_000, + startedAtMs + 60_000, + ); + db.prepare( + ` + UPDATE imm_sessions + SET + ended_at_ms = ?, + status = 2, + total_watched_ms = ?, + active_watched_ms = ?, + lines_seen = ?, + tokens_seen = ?, + cards_mined = ?, + lookup_count = ?, + lookup_hits = ?, + yomitan_lookup_count = ?, + LAST_UPDATE_DATE = ? + WHERE session_id = ? + `, + ).run( + startedAtMs + 60_000, + 30 * 60_000, + 30 * 60_000, + 4, + tokensSeen, + cardsMined, + yomitanLookupCount, + yomitanLookupCount, + yomitanLookupCount, + startedAtMs + 60_000, + sessionId, + ); } - } - - static override now(): number { - return new RealDate(2026, 2, 1, 12, 0, 0).getTime(); - } - } - - try { - globalThis.Date = MockDate as DateConstructor; - ensureSchema(db); - const stmts = createTrackerPreparedStatements(db); - const febVideoId = getOrCreateVideoRecord(db, 'local:/tmp/feb-trends.mkv', { - canonicalTitle: 'Monthly Trends', - sourcePath: '/tmp/feb-trends.mkv', - sourceUrl: null, - sourceType: SOURCE_TYPE_LOCAL, - }); - const marVideoId = getOrCreateVideoRecord(db, 'local:/tmp/mar-trends.mkv', { - canonicalTitle: 'Monthly Trends', - sourcePath: '/tmp/mar-trends.mkv', - sourceUrl: null, - sourceType: SOURCE_TYPE_LOCAL, - }); - const animeId = getOrCreateAnimeRecord(db, { - parsedTitle: 'Monthly Trends', - canonicalTitle: 'Monthly Trends', - anilistId: null, - titleRomaji: null, - titleEnglish: null, - titleNative: null, - metadataJson: null, - }); - linkVideoToAnimeRecord(db, febVideoId, { - animeId, - parsedBasename: 'feb-trends.mkv', - parsedTitle: 'Monthly Trends', - parsedSeason: 1, - parsedEpisode: 1, - parserSource: 'test', - parserConfidence: 1, - parseMetadataJson: null, - }); - linkVideoToAnimeRecord(db, marVideoId, { - animeId, - parsedBasename: 'mar-trends.mkv', - parsedTitle: 'Monthly Trends', - parsedSeason: 1, - parsedEpisode: 2, - parserSource: 'test', - parserConfidence: 1, - parseMetadataJson: null, - }); - - const febStartedAtMs = new RealDate(2026, 1, 15, 20, 0, 0).getTime(); - const marStartedAtMs = new RealDate(2026, 2, 1, 9, 0, 0).getTime(); - const febSessionId = startSessionRecord(db, febVideoId, febStartedAtMs).sessionId; - const marSessionId = startSessionRecord(db, marVideoId, marStartedAtMs).sessionId; - - for (const [sessionId, startedAtMs, tokensSeen, cardsMined, yomitanLookupCount] of [ - [febSessionId, febStartedAtMs, 100, 2, 3], - [marSessionId, marStartedAtMs, 120, 4, 5], - ] as const) { - stmts.telemetryInsertStmt.run( - sessionId, - startedAtMs + 60_000, - 30 * 60_000, - 30 * 60_000, - 4, - tokensSeen, - cardsMined, - yomitanLookupCount, - yomitanLookupCount, - yomitanLookupCount, - 0, - 0, - 0, - 0, - startedAtMs + 60_000, - startedAtMs + 60_000, - ); - db.prepare( - ` - UPDATE imm_sessions - SET - ended_at_ms = ?, - status = 2, - total_watched_ms = ?, - active_watched_ms = ?, - lines_seen = ?, - tokens_seen = ?, - cards_mined = ?, - lookup_count = ?, - lookup_hits = ?, - yomitan_lookup_count = ?, - LAST_UPDATE_DATE = ? - WHERE session_id = ? - `, - ).run( - startedAtMs + 60_000, - 30 * 60_000, - 30 * 60_000, - 4, - tokensSeen, - cardsMined, - yomitanLookupCount, - yomitanLookupCount, - yomitanLookupCount, - startedAtMs + 60_000, - sessionId, - ); - } const insertDailyRollup = db.prepare( ` @@ -987,11 +998,11 @@ test('getTrendsDashboard month grouping spans every touched calendar month and k dashboard.progress.lookups.map((point) => point.label), dashboard.activity.watchTime.map((point) => point.label), ); - } finally { - globalThis.Date = RealDate; - db.close(); - cleanupDbPath(dbPath); - } + } finally { + db.close(); + cleanupDbPath(dbPath); + } + }); }); test('getQueryHints reads all-time totals from lifetime summary', () => { @@ -1067,72 +1078,56 @@ test('getQueryHints reads all-time totals from lifetime summary', () => { test('getQueryHints computes weekly new-word cutoff from calendar midnights', () => { const dbPath = makeDbPath(); const db = new Database(dbPath); - const RealDate = Date; - class MockDate extends Date { - constructor(...args: any[]) { - type MockDateArgs = [any, any, any, any, any, any, any]; - if (args.length === 0) { - super(new RealDate(2026, 2, 15, 12, 0, 0).getTime()); - } else { - super(...(args as MockDateArgs)); - } + withMockDate(new Date(2026, 2, 15, 12, 0, 0), (RealDate) => { + try { + ensureSchema(db); + + const insertWord = db.prepare( + ` + INSERT INTO imm_words ( + headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + ); + const justBeforeWeekBoundary = Math.floor( + new RealDate(2026, 2, 7, 23, 30, 0).getTime() / 1000, + ); + const justAfterWeekBoundary = Math.floor( + new RealDate(2026, 2, 8, 0, 30, 0).getTime() / 1000, + ); + insertWord.run( + '境界前', + '境界前', + 'きょうかいまえ', + 'noun', + '名詞', + '', + '', + justBeforeWeekBoundary, + justBeforeWeekBoundary, + 1, + ); + insertWord.run( + '境界後', + '境界後', + 'きょうかいご', + 'noun', + '名詞', + '', + '', + justAfterWeekBoundary, + justAfterWeekBoundary, + 1, + ); + + const hints = getQueryHints(db); + assert.equal(hints.newWordsThisWeek, 1); + } finally { + db.close(); + cleanupDbPath(dbPath); } - - static override now(): number { - return new RealDate(2026, 2, 15, 12, 0, 0).getTime(); - } - } - - try { - globalThis.Date = MockDate as DateConstructor; - ensureSchema(db); - - const insertWord = db.prepare( - ` - INSERT INTO imm_words ( - headword, word, reading, part_of_speech, pos1, pos2, pos3, first_seen, last_seen, frequency - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - ); - const justBeforeWeekBoundary = Math.floor( - new RealDate(2026, 2, 7, 23, 30, 0).getTime() / 1000, - ); - const justAfterWeekBoundary = Math.floor( - new RealDate(2026, 2, 8, 0, 30, 0).getTime() / 1000, - ); - insertWord.run( - '境界前', - '境界前', - 'きょうかいまえ', - 'noun', - '名詞', - '', - '', - justBeforeWeekBoundary, - justBeforeWeekBoundary, - 1, - ); - insertWord.run( - '境界後', - '境界後', - 'きょうかいご', - 'noun', - '名詞', - '', - '', - justAfterWeekBoundary, - justAfterWeekBoundary, - 1, - ); - - const hints = getQueryHints(db); - assert.equal(hints.newWordsThisWeek, 1); - } finally { - globalThis.Date = RealDate; - db.close(); - cleanupDbPath(dbPath); - } + }); }); test('getQueryHints counts new words by distinct headword first-seen time', () => { @@ -1522,11 +1517,12 @@ test('getMonthlyRollups derives rate metrics from stored monthly totals', () => const rows = getMonthlyRollups(db, 1); assert.equal(rows.length, 2); - assert.equal(rows[1]?.cardsPerHour, 30); - assert.equal(rows[1]?.tokensPerMin, 3); - assert.equal(rows[1]?.lookupHitRate ?? null, null); - assert.equal(rows[0]?.cardsPerHour ?? null, null); - assert.equal(rows[0]?.tokensPerMin ?? null, null); + const rowsByVideoId = new Map(rows.map((row) => [row.videoId, row])); + assert.equal(rowsByVideoId.get(2)?.cardsPerHour, 30); + assert.equal(rowsByVideoId.get(2)?.tokensPerMin, 3); + assert.equal(rowsByVideoId.get(2)?.lookupHitRate ?? null, null); + assert.equal(rowsByVideoId.get(1)?.cardsPerHour ?? null, null); + assert.equal(rowsByVideoId.get(1)?.tokensPerMin ?? null, null); } finally { db.close(); cleanupDbPath(dbPath);