fix(immersion): use localtime rollup keys and guard sentence search limi

- Remove UTC-based UNION branch from getRollupGroupsForSessions; keep only localtime rollup
- Add resolveSentenceSearchLimit to clamp/sanitize infinity and negative limit values
- Switch onMiningImageEvent from queued to latest-value IPC listener (drop stale frames)
- Add regression tests for rollup key locality and sentence search limit edge cases
This commit is contained in:
2026-06-05 13:08:42 -07:00
parent eb5e07cfc0
commit 10c99b526b
7 changed files with 55 additions and 14 deletions
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
test('getRollupGroupsForSessions uses only localtime rollup keys', () => {
const source = fs.readFileSync(
path.join(process.cwd(), 'src/core/services/immersion-tracker/maintenance.ts'),
'utf8',
);
const start = source.indexOf('export function getRollupGroupsForSessions');
const end = source.indexOf('export function refreshRollupsForGroupsInTransaction');
const functionSource = source.slice(start, end);
assert.match(functionSource, /'unixepoch', 'localtime'/);
assert.doesNotMatch(functionSource, /UNION/);
assert.doesNotMatch(functionSource, /86400000/);
});
@@ -39,6 +39,7 @@ import {
upsertCoverArt,
} from '../query.js';
import {
getLocalEpochDay,
getShiftedLocalDaySec,
getStartOfLocalDayTimestamp,
toDbTimestamp,
@@ -3776,6 +3777,8 @@ test('searchSubtitleSentences searches known subtitle lines and returns media co
]);
assert.deepEqual(searchSubtitleSentences(db, 'monsters', 10), []);
assert.doesNotThrow(() => searchSubtitleSentences(db, '魔物', Number.POSITIVE_INFINITY));
assert.equal(searchSubtitleSentences(db, '魔物', -1).length, 1);
} finally {
db.close();
cleanupDbPath(dbPath);
@@ -4196,8 +4199,14 @@ test('deleteSession removes zero-session media from library and trends', () => {
const startedAtMs = 9_000_000;
const endedAtMs = startedAtMs + 120_000;
const rollupDay = Math.floor(startedAtMs / 86_400_000);
const rollupMonth = 197001;
const rollupDay = getLocalEpochDay(db, startedAtMs);
const rollupMonth = (
db
.prepare(
"SELECT CAST(strftime('%Y%m', CAST(? AS REAL) / 1000, 'unixepoch', 'localtime') AS INTEGER) AS rollupMonth",
)
.get(startedAtMs) as { rollupMonth: number }
).rollupMonth;
const { sessionId } = startSessionRecord(db, videoId, startedAtMs);
db.prepare(
@@ -319,16 +319,9 @@ export function getRollupGroupsForSessions(db: DatabaseSync, sessionIds: number[
video_id
FROM imm_sessions
WHERE session_id IN (${placeholders})
UNION
SELECT DISTINCT
CAST(CAST(started_at_ms AS REAL) / 86400000 AS INTEGER) AS rollup_day,
CAST(strftime('%Y%m', CAST(started_at_ms AS REAL) / 1000, 'unixepoch') AS INTEGER) AS rollup_month,
video_id
FROM imm_sessions
WHERE session_id IN (${placeholders})
`,
)
.all(...sessionIds, ...sessionIds) as RollupGroupRow[];
.all(...sessionIds) as RollupGroupRow[];
return rows.map((row) => ({
rollupDay: row.rollup_day,
@@ -21,6 +21,15 @@ import { nowMs } from './time';
const VOCABULARY_STATS_FILTER_OVERSAMPLE_FACTOR = 4;
const VOCABULARY_STATS_FILTER_OVERSAMPLE_MIN = 100;
const SENTENCE_SEARCH_DEFAULT_LIMIT = 50;
const SENTENCE_SEARCH_MAX_LIMIT = 100;
function resolveSentenceSearchLimit(limit: number): number {
if (!Number.isFinite(limit)) return SENTENCE_SEARCH_DEFAULT_LIMIT;
const normalized = Math.floor(limit);
if (normalized <= 0) return SENTENCE_SEARCH_DEFAULT_LIMIT;
return Math.min(normalized, SENTENCE_SEARCH_MAX_LIMIT);
}
function splitSearchTerms(query: string): string[] {
return query
@@ -228,10 +237,11 @@ export function getKanjiOccurrences(
export function searchSubtitleSentences(
db: DatabaseSync,
query: string,
limit = 50,
limit = SENTENCE_SEARCH_DEFAULT_LIMIT,
): SentenceSearchResultRow[] {
const terms = splitSearchTerms(query);
if (terms.length === 0) return [];
const resolvedLimit = resolveSentenceSearchLimit(limit);
const clauses: string[] = [];
const params: string[] = [];
@@ -270,7 +280,7 @@ export function searchSubtitleSentences(
LIMIT ?
`,
)
.all(...params, limit) as unknown as SentenceSearchResultRow[];
.all(...params, resolvedLimit) as unknown as SentenceSearchResultRow[];
}
export function getSessionEvents(