fix(launcher): address sync review feedback

This commit is contained in:
2026-07-09 00:30:30 -07:00
parent 58d54311d8
commit dfde19cc4d
11 changed files with 747 additions and 133 deletions
+25 -7
View File
@@ -5,6 +5,7 @@ import { IMMERSION_DB_FIXTURE_DDL } from './immersion-db-schema.js';
export function createImmersionDbFixture(dbPath: string): void {
const db = new Database(dbPath, { create: true });
try {
db.run('PRAGMA foreign_keys = ON');
db.run('PRAGMA journal_mode = WAL');
for (const statement of IMMERSION_DB_FIXTURE_DDL.split(';')) {
const sql = statement.trim();
@@ -14,7 +15,9 @@ export function createImmersionDbFixture(dbPath: string): void {
SCHEMA_VERSION,
String(Date.now()),
);
db.prepare(`INSERT INTO imm_rollup_state(state_key, state_value) VALUES ('last_rollup_sample_ms', 0)`).run();
db.prepare(
`INSERT INTO imm_rollup_state(state_key, state_value) VALUES ('last_rollup_sample_ms', 0)`,
).run();
db.prepare(
`INSERT INTO imm_lifetime_global(global_id, CREATED_DATE, LAST_UPDATE_DATE) VALUES (1, ?, ?)`,
).run(String(Date.now()), String(Date.now()));
@@ -53,7 +56,9 @@ export function insertFixtureSession(dbPath: string, input: FixtureSessionInput)
let animeId: number | null = null;
if (input.animeTitleKey) {
const existing = db
.query<{ anime_id: number }>('SELECT anime_id FROM imm_anime WHERE normalized_title_key = ?')
.query<{
anime_id: number;
}>('SELECT anime_id FROM imm_anime WHERE normalized_title_key = ?')
.get(input.animeTitleKey);
animeId = existing
? existing.anime_id
@@ -98,7 +103,8 @@ export function insertFixtureSession(dbPath: string, input: FixtureSessionInput)
db.prepare('UPDATE imm_videos SET watched = 1 WHERE video_id = ?').run(videoId);
}
const endedAtMs = input.endedAtMs === undefined ? input.startedAtMs + 1_500_000 : input.endedAtMs;
const endedAtMs =
input.endedAtMs === undefined ? input.startedAtMs + 1_500_000 : input.endedAtMs;
const activeWatchedMs = input.activeWatchedMs ?? 1_200_000;
const cardsMined = input.cardsMined ?? 2;
const linesSeen = input.linesSeen ?? 100;
@@ -147,7 +153,9 @@ export function insertFixtureSession(dbPath: string, input: FixtureSessionInput)
for (const word of input.words ?? []) {
const existing = db
.query<{ id: number }>('SELECT id FROM imm_words WHERE headword = ? AND word = ? AND reading = ?')
.query<{
id: number;
}>('SELECT id FROM imm_words WHERE headword = ? AND word = ? AND reading = ?')
.get(word.headword, word.word, word.reading);
const wordId = existing
? existing.id
@@ -172,13 +180,23 @@ export function insertFixtureSession(dbPath: string, input: FixtureSessionInput)
`INSERT INTO imm_subtitle_lines (session_id, video_id, anime_id, line_index, text, CREATED_DATE, LAST_UPDATE_DATE)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
)
.run(sessionId, videoId, animeId, uniqueCounter, `line ${word.word}`, input.startedAtMs, input.startedAtMs)
.lastInsertRowid,
.run(
sessionId,
videoId,
animeId,
uniqueCounter,
`line ${word.word}`,
input.startedAtMs,
input.startedAtMs,
).lastInsertRowid,
);
db.prepare(
'INSERT INTO imm_word_line_occurrences (line_id, word_id, occurrence_count) VALUES (?, ?, ?)',
).run(lineId, wordId, word.count);
db.prepare('UPDATE imm_words SET frequency = frequency + ? WHERE id = ?').run(word.count, wordId);
db.prepare('UPDATE imm_words SET frequency = frequency + ? WHERE id = ?').run(
word.count,
wordId,
);
}
if (input.applyLifetime && endedAtMs !== null) {
@@ -0,0 +1,109 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Database as BunDatabase } from 'bun:sqlite';
import { ensureSchema } from '../../src/core/services/immersion-tracker/storage.js';
import { createImmersionDbFixture } from './immersion-db-fixture.js';
type SchemaRow = { type: string; name: string; tbl_name: string; sql: string | null };
const SYNC_SCHEMA_OBJECTS = [
'imm_anime',
'imm_videos',
'imm_sessions',
'imm_session_telemetry',
'imm_session_events',
'imm_daily_rollups',
'imm_monthly_rollups',
'imm_words',
'imm_kanji',
'imm_subtitle_lines',
'imm_word_line_occurrences',
'imm_kanji_line_occurrences',
'imm_media_art',
'imm_youtube_videos',
'imm_cover_art_blobs',
'imm_lifetime_global',
'imm_lifetime_anime',
'imm_lifetime_media',
'imm_lifetime_applied_sessions',
'imm_stats_excluded_words',
'idx_anime_normalized_title',
'idx_anime_anilist_id',
'idx_videos_anime_id',
'idx_sessions_video_started',
'idx_sessions_status_started',
'idx_sessions_started_at',
'idx_sessions_ended_at',
'idx_telemetry_session_sample',
'idx_telemetry_sample_ms',
'idx_events_session_ts',
'idx_events_type_ts',
'idx_rollups_day_video',
'idx_rollups_month_video',
'idx_words_headword_word_reading',
'idx_words_frequency',
'idx_kanji_kanji',
'idx_kanji_frequency',
'idx_subtitle_lines_session_line',
'idx_subtitle_lines_video_line',
'idx_subtitle_lines_anime_line',
'idx_word_line_occurrences_word',
'idx_kanji_line_occurrences_kanji',
'idx_media_art_cover_blob_hash',
'idx_media_art_anilist_id',
'idx_media_art_cover_url',
'idx_youtube_videos_channel_id',
'idx_youtube_videos_youtube_video_id',
] as const;
function normalizeSql(sql: string | null): string {
return (sql ?? '')
.replace(/\bIF NOT EXISTS\b/gi, '')
.replace(/\s+/g, ' ')
.replace(/\s+([(),])/g, '$1')
.replace(/,\s+/g, ', ')
.trim();
}
function readSchema(dbPath: string): Map<string, string> {
const db = new BunDatabase(dbPath, { readonly: true });
try {
const rows = db
.query<SchemaRow>(
`SELECT type, name, tbl_name, sql
FROM sqlite_schema
WHERE name IN (${SYNC_SCHEMA_OBJECTS.map(() => '?').join(',')})
ORDER BY type, name`,
)
.all(...SYNC_SCHEMA_OBJECTS);
return new Map(rows.map((row) => [row.name, normalizeSql(row.sql)]));
} finally {
db.close();
}
}
test('fixture schema stays aligned with production sync-touched tables and indexes', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-fixture-schema-'));
const fixturePath = path.join(dir, 'fixture.sqlite');
const productionPath = path.join(dir, 'production.sqlite');
const productionDb = new BunDatabase(productionPath, { create: true });
try {
createImmersionDbFixture(fixturePath);
ensureSchema(productionDb as never);
productionDb.close();
const fixtureSchema = readSchema(fixturePath);
const productionSchema = readSchema(productionPath);
assert.deepEqual(fixtureSchema, productionSchema);
} finally {
try {
productionDb.close();
} catch {
// already closed
}
fs.rmSync(dir, { recursive: true, force: true });
}
});