mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-02 07:21:33 -07:00
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
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 } from '../immersion-tracker/sqlite';
|
|
import { ensureSchema } from '../immersion-tracker/storage';
|
|
import { mergeSnapshotIntoDb } from './merge';
|
|
|
|
const BASE_MS = 1_700_000_000_000;
|
|
const DAY_MS = 86_400_000;
|
|
|
|
/**
|
|
* Build a database holding one anime, one episode, one ended session and a
|
|
* single word occurrence.
|
|
*
|
|
* `legacyOccurrences` reproduces a peer whose schema predates the denormalised
|
|
* `seen_ms` column, so the merge has to recover the timestamp from the line.
|
|
*/
|
|
function buildDb(
|
|
dir: string,
|
|
name: string,
|
|
options: { word: string; seenMs: number; legacyOccurrences: boolean },
|
|
): string {
|
|
const dbPath = path.join(dir, name);
|
|
const db = new Database(dbPath);
|
|
ensureSchema(db);
|
|
db.exec(`
|
|
INSERT INTO imm_anime(anime_id, normalized_title_key, canonical_title, CREATED_DATE, LAST_UPDATE_DATE)
|
|
VALUES (1, 'key-${name}', 'Show ${name}', ${BASE_MS}, ${BASE_MS});
|
|
INSERT INTO imm_videos(video_id, video_key, anime_id, canonical_title, source_type, watched, duration_ms, CREATED_DATE, LAST_UPDATE_DATE)
|
|
VALUES (1, 'video-${name}', 1, 'Episode 1', 1, 1, 1440000, ${BASE_MS}, ${BASE_MS});
|
|
INSERT INTO imm_sessions(session_id, session_uuid, video_id, started_at_ms, ended_at_ms, status, CREATED_DATE, LAST_UPDATE_DATE)
|
|
VALUES (1, 'uuid-${name}', 1, '${options.seenMs}', '${options.seenMs + 1000}', 2, ${BASE_MS}, ${BASE_MS});
|
|
INSERT INTO imm_subtitle_lines(line_id, session_id, video_id, anime_id, line_index, text, CREATED_DATE, LAST_UPDATE_DATE)
|
|
VALUES (1, 1, 1, 1, 0, 'line', ${options.seenMs}, ${options.seenMs});
|
|
INSERT INTO imm_words(id, headword, word, reading, part_of_speech, pos1, first_seen, last_seen, frequency)
|
|
VALUES (1, '${options.word}', '${options.word}', '', 'noun', '名詞',
|
|
${Math.floor(options.seenMs / 1000)}, ${Math.floor(options.seenMs / 1000)}, 1);
|
|
`);
|
|
db.exec(
|
|
options.legacyOccurrences
|
|
? 'INSERT INTO imm_word_line_occurrences(line_id, word_id, occurrence_count) VALUES (1, 1, 1)'
|
|
: `INSERT INTO imm_word_line_occurrences(line_id, word_id, occurrence_count, seen_ms) VALUES (1, 1, 1, ${options.seenMs})`,
|
|
);
|
|
db.close();
|
|
return dbPath;
|
|
}
|
|
|
|
function mergedOccurrences(dbPath: string): Array<{ word: string; seenMs: number | null }> {
|
|
const db = new Database(dbPath);
|
|
try {
|
|
return db
|
|
.prepare(
|
|
`SELECT w.word AS word, o.seen_ms AS seenMs
|
|
FROM imm_word_line_occurrences o
|
|
JOIN imm_words w ON w.id = o.word_id
|
|
ORDER BY w.word`,
|
|
)
|
|
.all() as Array<{ word: string; seenMs: number | null }>;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
for (const legacyOccurrences of [false, true]) {
|
|
const label = legacyOccurrences ? 'a peer predating the seen_ms column' : 'a current peer';
|
|
|
|
test(`sync merge carries occurrence timestamps in from ${label}`, () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-merge-occurrences-test-'));
|
|
try {
|
|
const local = buildDb(dir, 'local.sqlite', {
|
|
word: '猫',
|
|
seenMs: BASE_MS,
|
|
legacyOccurrences: false,
|
|
});
|
|
const remoteSeenMs = BASE_MS + 5 * DAY_MS;
|
|
const remote = buildDb(dir, 'remote.sqlite', {
|
|
word: '犬',
|
|
seenMs: remoteSeenMs,
|
|
legacyOccurrences,
|
|
});
|
|
|
|
mergeSnapshotIntoDb(local, remote);
|
|
|
|
const rows = mergedOccurrences(local);
|
|
assert.equal(rows.length, 2, 'local and remote occurrences both survive the merge');
|
|
for (const row of rows) {
|
|
assert.ok(row.seenMs, `${row.word} must carry a timestamp so aggregates stay index-only`);
|
|
}
|
|
assert.equal(
|
|
Number(rows.find((row) => row.word === '犬')?.seenMs),
|
|
remoteSeenMs,
|
|
"the remote line's own timestamp is preserved rather than stamped with merge time",
|
|
);
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|