mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 06:12:06 -07:00
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
This commit is contained in:
@@ -81,6 +81,34 @@ function cleanupDbPath(dbPath: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withMockDate<T>(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', () => {
|
test('getSessionSummaries returns sessionId and canonicalTitle', () => {
|
||||||
const dbPath = makeDbPath();
|
const dbPath = makeDbPath();
|
||||||
const db = new Database(dbPath);
|
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', () => {
|
test('getTrendsDashboard month grouping spans every touched calendar month and keeps progress monthly', () => {
|
||||||
const dbPath = makeDbPath();
|
const dbPath = makeDbPath();
|
||||||
const db = new Database(dbPath);
|
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 {
|
const febStartedAtMs = new RealDate(2026, 1, 15, 20, 0, 0).getTime();
|
||||||
constructor(...args: any[]) {
|
const marStartedAtMs = new RealDate(2026, 2, 1, 9, 0, 0).getTime();
|
||||||
type MockDateArgs = [any, any, any, any, any, any, any];
|
const febSessionId = startSessionRecord(db, febVideoId, febStartedAtMs).sessionId;
|
||||||
if (args.length === 0) {
|
const marSessionId = startSessionRecord(db, marVideoId, marStartedAtMs).sessionId;
|
||||||
super(new RealDate(2026, 2, 1, 12, 0, 0).getTime());
|
|
||||||
} else {
|
for (const [sessionId, startedAtMs, tokensSeen, cardsMined, yomitanLookupCount] of [
|
||||||
super(...(args as MockDateArgs));
|
[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(
|
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.progress.lookups.map((point) => point.label),
|
||||||
dashboard.activity.watchTime.map((point) => point.label),
|
dashboard.activity.watchTime.map((point) => point.label),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.Date = RealDate;
|
db.close();
|
||||||
db.close();
|
cleanupDbPath(dbPath);
|
||||||
cleanupDbPath(dbPath);
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getQueryHints reads all-time totals from lifetime summary', () => {
|
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', () => {
|
test('getQueryHints computes weekly new-word cutoff from calendar midnights', () => {
|
||||||
const dbPath = makeDbPath();
|
const dbPath = makeDbPath();
|
||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
const RealDate = Date;
|
|
||||||
|
|
||||||
class MockDate extends Date {
|
withMockDate(new Date(2026, 2, 15, 12, 0, 0), (RealDate) => {
|
||||||
constructor(...args: any[]) {
|
try {
|
||||||
type MockDateArgs = [any, any, any, any, any, any, any];
|
ensureSchema(db);
|
||||||
if (args.length === 0) {
|
|
||||||
super(new RealDate(2026, 2, 15, 12, 0, 0).getTime());
|
const insertWord = db.prepare(
|
||||||
} else {
|
`
|
||||||
super(...(args as MockDateArgs));
|
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', () => {
|
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);
|
const rows = getMonthlyRollups(db, 1);
|
||||||
assert.equal(rows.length, 2);
|
assert.equal(rows.length, 2);
|
||||||
assert.equal(rows[1]?.cardsPerHour, 30);
|
const rowsByVideoId = new Map(rows.map((row) => [row.videoId, row]));
|
||||||
assert.equal(rows[1]?.tokensPerMin, 3);
|
assert.equal(rowsByVideoId.get(2)?.cardsPerHour, 30);
|
||||||
assert.equal(rows[1]?.lookupHitRate ?? null, null);
|
assert.equal(rowsByVideoId.get(2)?.tokensPerMin, 3);
|
||||||
assert.equal(rows[0]?.cardsPerHour ?? null, null);
|
assert.equal(rowsByVideoId.get(2)?.lookupHitRate ?? null, null);
|
||||||
assert.equal(rows[0]?.tokensPerMin ?? null, null);
|
assert.equal(rowsByVideoId.get(1)?.cardsPerHour ?? null, null);
|
||||||
|
assert.equal(rowsByVideoId.get(1)?.tokensPerMin ?? null, null);
|
||||||
} finally {
|
} finally {
|
||||||
db.close();
|
db.close();
|
||||||
cleanupDbPath(dbPath);
|
cleanupDbPath(dbPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user