refactor(main): eliminate local pass-through wrappers in main.ts (#168)

This commit is contained in:
2026-07-15 03:22:03 -07:00
committed by GitHub
parent 8711cf1a48
commit 2398f5c030
19 changed files with 706 additions and 814 deletions
+2
View File
@@ -31,6 +31,8 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
- `src/config/` owns config definitions, defaults, loading, and resolution. - `src/config/` owns config definitions, defaults, loading, and resolution.
- `src/types/` owns shared cross-runtime contracts via domain entrypoints; `src/types.ts` stays a compatibility barrel. - `src/types/` owns shared cross-runtime contracts via domain entrypoints; `src/types.ts` stays a compatibility barrel.
- `src/main/runtime/composers/` owns larger domain compositions. - `src/main/runtime/composers/` owns larger domain compositions.
- `src/main.ts` call sites invoke configured runtime handlers and runtime-object methods directly;
do not add local pass-through wrappers around them.
## Architecture Intent ## Architecture Intent
+8 -4
View File
@@ -632,8 +632,10 @@ test('adopted word frequency excludes active-session counts that merge later', (
// Only the ended session's count is adopted; the active session's slice // Only the ended session's count is adopted; the active session's slice
// is re-added when that session finalizes and syncs. // is re-added when that session finalizes and syncs.
assert.equal( assert.equal(
queryOne<{ frequency: number }>(localPath, `SELECT frequency FROM imm_words WHERE word = '食べた'`) queryOne<{ frequency: number }>(
?.frequency, localPath,
`SELECT frequency FROM imm_words WHERE word = '食べた'`,
)?.frequency,
1, 1,
); );
@@ -650,8 +652,10 @@ test('adopted word frequency excludes active-session counts that merge later', (
assert.equal(second.sessionsAlreadyPresent, 1); assert.equal(second.sessionsAlreadyPresent, 1);
// 1 (ended session) + 4 (finalized session), not 5 + 4 = 9. // 1 (ended session) + 4 (finalized session), not 5 + 4 = 9.
assert.equal( assert.equal(
queryOne<{ frequency: number }>(localPath, `SELECT frequency FROM imm_words WHERE word = '食べた'`) queryOne<{ frequency: number }>(
?.frequency, localPath,
`SELECT frequency FROM imm_words WHERE word = '食べた'`,
)?.frequency,
5, 5,
); );
} finally { } finally {
@@ -404,8 +404,7 @@ export function buildIntegrationConfigOptionRegistry(
path: 'tsukihime.apiBaseUrl', path: 'tsukihime.apiBaseUrl',
kind: 'string', kind: 'string',
defaultValue: defaultConfig.tsukihime.apiBaseUrl, defaultValue: defaultConfig.tsukihime.apiBaseUrl,
description: description: 'Base URL of the TsukiHime API (Animetosho successor). No API key required.',
'Base URL of the TsukiHime API (Animetosho successor). No API key required.',
}, },
{ {
path: 'tsukihime.maxSearchResults', path: 'tsukihime.maxSearchResults',
+1 -3
View File
@@ -302,9 +302,7 @@ export function registerAnkiJimakuIpcHandlers(
await deps.onDownloadedSecondarySubtitle(result.path); await deps.onDownloadedSecondarySubtitle(result.path);
} }
} else { } else {
logger.error( logger.error(`[tsukihime] download-file failed: ${result.error?.error ?? 'unknown error'}`);
`[tsukihime] download-file failed: ${result.error?.error ?? 'unknown error'}`,
);
} }
return result; return result;
@@ -48,7 +48,10 @@ test('parseSyncCliTokens handles the temp-dir protocol modes', () => {
} }
assert.equal(parseSyncCliTokens(['sync', '--make-temp', 'host']).kind, 'error'); assert.equal(parseSyncCliTokens(['sync', '--make-temp', 'host']).kind, 'error');
assert.equal(parseSyncCliTokens(['sync', '--make-temp', '--remove-temp', '/tmp/x']).kind, 'error'); assert.equal(
parseSyncCliTokens(['sync', '--make-temp', '--remove-temp', '/tmp/x']).kind,
'error',
);
}); });
test('parseSyncCliTokens owns the sync CLI validation rules', () => { test('parseSyncCliTokens owns the sync CLI validation rules', () => {
+7 -2
View File
@@ -58,7 +58,9 @@ export function parseSyncCliTokens(tokens: readonly string[]): ParsedSyncCli {
for (let i = 0; i < rest.length; i += 1) { for (let i = 0; i < rest.length; i += 1) {
const token = rest[i]!; const token = rest[i]!;
const assignValue = valueFlags.get(token.includes('=') ? token.slice(0, token.indexOf('=')) : token); const assignValue = valueFlags.get(
token.includes('=') ? token.slice(0, token.indexOf('=')) : token,
);
if (assignValue) { if (assignValue) {
if (token.includes('=')) { if (token.includes('=')) {
assignValue(token.slice(token.indexOf('=') + 1)); assignValue(token.slice(token.indexOf('=') + 1));
@@ -109,7 +111,10 @@ export function parseSyncCliTokens(tokens: readonly string[]): ParsedSyncCli {
Boolean(removeTemp), Boolean(removeTemp),
].filter(Boolean).length; ].filter(Boolean).length;
if (modes === 0) { if (modes === 0) {
return { kind: 'error', message: 'Sync requires a host, --snapshot <file>, or --merge <file>.' }; return {
kind: 'error',
message: 'Sync requires a host, --snapshot <file>, or --merge <file>.',
};
} }
if (modes > 1) { if (modes > 1) {
return { return {
+1 -3
View File
@@ -34,9 +34,7 @@ export function resolveImmersionDbPath(): string {
// no config or unreadable config → default location // no config or unreadable config → default location
} }
if (configured) { if (configured) {
return configured.startsWith('~') return configured.startsWith('~') ? path.join(os.homedir(), configured.slice(1)) : configured;
? path.join(os.homedir(), configured.slice(1))
: configured;
} }
return path.join(getDefaultConfigDir(), 'immersion.sqlite'); return path.join(getDefaultConfigDir(), 'immersion.sqlite');
} }
@@ -251,11 +251,7 @@ export function mergeMediaMetadata(
} }
} }
export function mergeExcludedWords( export function mergeExcludedWords(local: SyncDb, remote: SyncDb, summary: SyncMergeSummary): void {
local: SyncDb,
remote: SyncDb,
summary: SyncMergeSummary,
): void {
if ( if (
!tableExists(remote, 'imm_stats_excluded_words') || !tableExists(remote, 'imm_stats_excluded_words') ||
!tableExists(local, 'imm_stats_excluded_words') !tableExists(local, 'imm_stats_excluded_words')
@@ -147,7 +147,9 @@ export function refreshRollupsForNewSessions(
} }
const stampMs = nowDbTimestamp(); const stampMs = nowDbTimestamp();
const deleteDaily = local.query('DELETE FROM imm_daily_rollups WHERE rollup_day = ? AND video_id = ?'); const deleteDaily = local.query(
'DELETE FROM imm_daily_rollups WHERE rollup_day = ? AND video_id = ?',
);
const deleteMonthly = local.query( const deleteMonthly = local.query(
'DELETE FROM imm_monthly_rollups WHERE rollup_month = ? AND video_id = ?', 'DELETE FROM imm_monthly_rollups WHERE rollup_month = ? AND video_id = ?',
); );
@@ -319,7 +319,8 @@ function applyMergedSessionLifetime(
.get(videoId, sessionId), .get(videoId, sessionId),
); );
const isFirstSessionForVideoRun = !mediaLifetime && !hasOtherSessionForVideo; const isFirstSessionForVideoRun = !mediaLifetime && !hasOtherSessionForVideo;
const isFirstCompletedSessionForVideoRun = watched > 0 && Number(mediaLifetime?.completed ?? 0) <= 0; const isFirstCompletedSessionForVideoRun =
watched > 0 && Number(mediaLifetime?.completed ?? 0) <= 0;
const hasOtherSessionOnDay = Boolean( const hasOtherSessionOnDay = Boolean(
local local
+1 -5
View File
@@ -8,11 +8,7 @@ import {
} from './merge-catalog'; } from './merge-catalog';
import { mergeSessions } from './merge-sessions'; import { mergeSessions } from './merge-sessions';
import { copyRemoteOnlyRollups, refreshRollupsForNewSessions } from './merge-rollups'; import { copyRemoteOnlyRollups, refreshRollupsForNewSessions } from './merge-rollups';
import { import { assertMergeableSchema, createEmptyMergeSummary, type SyncMergeSummary } from './shared';
assertMergeableSchema,
createEmptyMergeSummary,
type SyncMergeSummary,
} from './shared';
import { openLibsqlSyncDb, type SyncDb } from './libsql-driver'; import { openLibsqlSyncDb, type SyncDb } from './libsql-driver';
export type { SyncMergeSummary } from './shared'; export type { SyncMergeSummary } from './shared';
+397 -659
View File
File diff suppressed because it is too large Load Diff
+104 -104
View File
@@ -52,7 +52,13 @@ function runKeepaliveScript(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
execFile( execFile(
'/bin/sh', '/bin/sh',
['-c', APPIMAGE_MOUNT_KEEPALIVE_SCRIPT, APPIMAGE_MOUNT_KEEPALIVE_LABEL, appImagePath, ...extraArgs], [
'-c',
APPIMAGE_MOUNT_KEEPALIVE_SCRIPT,
APPIMAGE_MOUNT_KEEPALIVE_LABEL,
appImagePath,
...extraArgs,
],
{ timeout: 30_000 }, { timeout: 30_000 },
(error) => { (error) => {
if (error && typeof error.code !== 'number') { if (error && typeof error.code !== 'number') {
@@ -69,113 +75,107 @@ function writeExecutable(filePath: string, content: string): void {
fs.writeFileSync(filePath, content, { mode: 0o755 }); fs.writeFileSync(filePath, content, { mode: 0o755 });
} }
test( const linuxTest = process.platform === 'linux' ? test : test.skip;
'keepalive script releases the mount only after straggler processes exit',
{ skip: process.platform !== 'linux' },
async () => {
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-keepalive-test-'));
const resultsDir = path.join(workDir, 'results');
fs.mkdirSync(resultsDir);
const mountDir = path.join(workDir, 'fake-mount');
fs.mkdirSync(mountDir);
// AppRun leaves behind a straggler that keeps executing *from the mount* linuxTest('keepalive script releases the mount only after straggler processes exit', async () => {
// after AppRun itself exits — mimicking Chromium utility children. const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-keepalive-test-'));
fs.copyFileSync('/usr/bin/sleep', path.join(mountDir, 'straggler')); const resultsDir = path.join(workDir, 'results');
fs.chmodSync(path.join(mountDir, 'straggler'), 0o755); fs.mkdirSync(resultsDir);
writeExecutable( const mountDir = path.join(workDir, 'fake-mount');
path.join(mountDir, 'AppRun'), fs.mkdirSync(mountDir);
[
'#!/bin/sh', // AppRun leaves behind a straggler that keeps executing *from the mount*
`"${mountDir}/straggler" 1 &`, // after AppRun itself exits — mimicking Chromium utility children.
`date +%s%N > "${resultsDir}/apprun-exited"`, fs.copyFileSync('/usr/bin/sleep', path.join(mountDir, 'straggler'));
'exit 42', fs.chmodSync(path.join(mountDir, 'straggler'), 0o755);
].join('\n'), writeExecutable(
path.join(mountDir, 'AppRun'),
[
'#!/bin/sh',
`"${mountDir}/straggler" 1 &`,
`date +%s%N > "${resultsDir}/apprun-exited"`,
'exit 42',
].join('\n'),
);
const fakeAppImage = path.join(workDir, 'Fake.AppImage');
writeExecutable(
fakeAppImage,
[
'#!/bin/sh',
'if [ "${1:-}" = "--appimage-mount" ]; then',
` echo "${mountDir}"`,
` trap ': > "${resultsDir}/holder-released"; sleep 0.1; date +%s%N > "${resultsDir}/holder-released"; exit 0' TERM INT`,
' while :; do sleep 0.05; done',
'fi',
`date +%s%N > "${resultsDir}/direct-run"`,
'exit 0',
].join('\n'),
);
try {
const { status } = await runKeepaliveScript(fakeAppImage);
assert.equal(status, 42, 'exit code of AppRun must be propagated');
assert.ok(
!fs.existsSync(path.join(resultsDir, 'direct-run')),
'must not fall back to direct AppImage run when mount succeeds',
); );
// The script does not wait for the holder to finish handling SIGTERM
const fakeAppImage = path.join(workDir, 'Fake.AppImage'); // (the real runtime unmounts on its own after the signal), so poll.
writeExecutable( const releasedMarker = path.join(resultsDir, 'holder-released');
fakeAppImage, const pollDeadline = Date.now() + 2000;
[ let holderReleased: number | null = null;
'#!/bin/sh', while (holderReleased === null && Date.now() < pollDeadline) {
'if [ "${1:-}" = "--appimage-mount" ]; then', if (fs.existsSync(releasedMarker)) {
` echo "${mountDir}"`, const timestamp = fs.readFileSync(releasedMarker, 'utf8').trim();
` trap ': > "${resultsDir}/holder-released"; sleep 0.1; date +%s%N > "${resultsDir}/holder-released"; exit 0' TERM INT`, if (/^\d+$/.test(timestamp)) holderReleased = Number(timestamp);
' while :; do sleep 0.05; done',
'fi',
`date +%s%N > "${resultsDir}/direct-run"`,
'exit 0',
].join('\n'),
);
try {
const { status } = await runKeepaliveScript(fakeAppImage);
assert.equal(status, 42, 'exit code of AppRun must be propagated');
assert.ok(
!fs.existsSync(path.join(resultsDir, 'direct-run')),
'must not fall back to direct AppImage run when mount succeeds',
);
// The script does not wait for the holder to finish handling SIGTERM
// (the real runtime unmounts on its own after the signal), so poll.
const releasedMarker = path.join(resultsDir, 'holder-released');
const pollDeadline = Date.now() + 2000;
let holderReleased: number | null = null;
while (holderReleased === null && Date.now() < pollDeadline) {
if (fs.existsSync(releasedMarker)) {
const timestamp = fs.readFileSync(releasedMarker, 'utf8').trim();
if (/^\d+$/.test(timestamp)) holderReleased = Number(timestamp);
}
if (holderReleased !== null) break;
await new Promise((r) => setTimeout(r, 25));
} }
assert.ok(holderReleased !== null, 'holder release timestamp must be recorded'); if (holderReleased !== null) break;
await new Promise((r) => setTimeout(r, 25));
const appRunExited = Number(
fs.readFileSync(path.join(resultsDir, 'apprun-exited'), 'utf8').trim(),
);
const drainNs = holderReleased - appRunExited;
assert.ok(
drainNs >= 0.8e9,
`holder must outlive the 1s straggler (drained after ${drainNs / 1e9}s)`,
);
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
} }
}, assert.ok(holderReleased !== null, 'holder release timestamp must be recorded');
);
test( const appRunExited = Number(
'keepalive script falls back to direct run when --appimage-mount fails', fs.readFileSync(path.join(resultsDir, 'apprun-exited'), 'utf8').trim(),
{ skip: process.platform !== 'linux' },
async () => {
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-keepalive-test-'));
const resultsDir = path.join(workDir, 'results');
fs.mkdirSync(resultsDir);
const fakeAppImage = path.join(workDir, 'Fake.AppImage');
writeExecutable(
fakeAppImage,
[
'#!/bin/sh',
'if [ "${1:-}" = "--appimage-mount" ]; then',
' exit 1',
'fi',
`printf '%s\\n' "$@" > "${resultsDir}/direct-run"`,
'exit 7',
].join('\n'),
); );
const drainNs = holderReleased - appRunExited;
assert.ok(
drainNs >= 0.8e9,
`holder must outlive the 1s straggler (drained after ${drainNs / 1e9}s)`,
);
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
});
try { linuxTest('keepalive script falls back to direct run when --appimage-mount fails', async () => {
const { status } = await runKeepaliveScript(fakeAppImage, ['--start', '--background']); const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-keepalive-test-'));
assert.equal(status, 7, 'direct-run exit code must be propagated'); const resultsDir = path.join(workDir, 'results');
assert.equal( fs.mkdirSync(resultsDir);
fs.readFileSync(path.join(resultsDir, 'direct-run'), 'utf8'),
'--start\n--background\n', const fakeAppImage = path.join(workDir, 'Fake.AppImage');
'launch args must be forwarded to the direct run', writeExecutable(
); fakeAppImage,
} finally { [
fs.rmSync(workDir, { recursive: true, force: true }); '#!/bin/sh',
} 'if [ "${1:-}" = "--appimage-mount" ]; then',
}, ' exit 1',
); 'fi',
`printf '%s\\n' "$@" > "${resultsDir}/direct-run"`,
'exit 7',
].join('\n'),
);
try {
const { status } = await runKeepaliveScript(fakeAppImage, ['--start', '--background']);
assert.equal(status, 7, 'direct-run exit code must be propagated');
assert.equal(
fs.readFileSync(path.join(resultsDir, 'direct-run'), 'utf8'),
'--start\n--background\n',
'launch args must be forwarded to the direct run',
);
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
});
@@ -0,0 +1,99 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
test('main process call sites bypass zero-logic pass-through wrappers', () => {
const sourcePath = path.join(process.cwd(), 'src/main.ts');
const source = fs.readFileSync(sourcePath, 'utf8');
const sourceFile = ts.createSourceFile(
sourcePath,
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const offenders: string[] = [];
for (const statement of sourceFile.statements) {
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
if (isRuntimePassthrough(statement, statement.body)) {
offenders.push(statement.name.text);
}
continue;
}
if (!ts.isVariableStatement(statement)) continue;
for (const declaration of statement.declarationList.declarations) {
if (
ts.isIdentifier(declaration.name) &&
declaration.initializer &&
(ts.isArrowFunction(declaration.initializer) ||
ts.isFunctionExpression(declaration.initializer)) &&
isRuntimePassthrough(declaration.initializer, declaration.initializer.body)
) {
offenders.push(declaration.name.text);
}
}
}
assert.deepEqual(offenders, []);
});
function isRuntimePassthrough(
callable: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression,
body: ts.ConciseBody,
): boolean {
let expression: ts.Expression | undefined;
if (!ts.isBlock(body)) {
expression = body;
} else {
if (body.statements.length !== 1) return false;
const statement = body.statements[0]!;
if (ts.isExpressionStatement(statement)) expression = statement.expression;
if (ts.isReturnStatement(statement)) expression = statement.expression;
}
if (!expression) return false;
if (ts.isAwaitExpression(expression) || ts.isVoidExpression(expression)) {
expression = expression.expression;
}
if (!ts.isCallExpression(expression)) return false;
const root = getCalleeRoot(expression.expression);
if (!root) return false;
if (callable.parameters.length !== expression.arguments.length) return false;
return callable.parameters.every((parameter, index) => {
if (!ts.isIdentifier(parameter.name)) return false;
const argument = expression.arguments[index];
if (!argument) return false;
if (parameter.dotDotDotToken) {
if (!ts.isSpreadElement(argument)) return false;
const spreadExpression = unwrapTransparentExpression(argument.expression);
return ts.isIdentifier(spreadExpression) && spreadExpression.text === parameter.name.text;
}
const argumentExpression = unwrapTransparentExpression(argument);
return ts.isIdentifier(argumentExpression) && argumentExpression.text === parameter.name.text;
});
}
function unwrapTransparentExpression(expression: ts.Expression): ts.Expression {
let current = expression;
while (
ts.isParenthesizedExpression(current) ||
ts.isAsExpression(current) ||
ts.isNonNullExpression(current)
) {
current = current.expression;
}
return current;
}
function getCalleeRoot(expression: ts.LeftHandSideExpression): string {
let current: ts.Expression = expression;
while (ts.isPropertyAccessExpression(current) || ts.isCallExpression(current)) {
current = current.expression;
}
return ts.isIdentifier(current) ? current.text : '';
}
+70 -12
View File
@@ -446,7 +446,10 @@ test('Linux visible overlay recreation avoids display fallback before tracked ge
)?.groups?.body; )?.groups?.body;
assert.ok(actionBlock); assert.ok(actionBlock);
assert.match(actionBlock, /const trackedGeometry = getCurrentTrackedOverlayGeometry\(\);/); assert.match(
actionBlock,
/const trackedGeometry = overlayGeometryRuntime\.getCurrentTrackedOverlayGeometry\(\);/,
);
assert.match(actionBlock, /if \(trackedGeometry\) \{/); assert.match(actionBlock, /if \(trackedGeometry\) \{/);
assert.match(actionBlock, /overlayManager\.setOverlayWindowBounds\(trackedGeometry\);/); assert.match(actionBlock, /overlayManager\.setOverlayWindowBounds\(trackedGeometry\);/);
assert.doesNotMatch(actionBlock, /setOverlayWindowBounds\(getCurrentOverlayGeometry\(\)\)/); assert.doesNotMatch(actionBlock, /setOverlayWindowBounds\(getCurrentOverlayGeometry\(\)\)/);
@@ -556,11 +559,60 @@ test('YouTube media cache lifecycle routes through configured status notificatio
); );
assert.match(cacheBlock, /variant:\s*'success'/); assert.match(cacheBlock, /variant:\s*'success'/);
assert.match(cacheBlock, /notifyNoQueued:\s*false/); assert.match(cacheBlock, /notifyNoQueued:\s*false/);
assert.match(startCacheBlock, /mode:\s*getResolvedConfig\(\)\.youtube\.mediaCache\.mode/);
assert.match( assert.match(
startCacheBlock, startCacheBlock,
/maxHeight:\s*getResolvedConfig\(\)\.youtube\.mediaCache\.maxHeight/, /const mediaCacheConfig = configService\.getConfig\(\)\.youtube\.mediaCache;/,
); );
assert.match(startCacheBlock, /mode:\s*mediaCacheConfig\.mode/);
assert.match(startCacheBlock, /maxHeight:\s*mediaCacheConfig\.maxHeight/);
});
test('subtitle broadcasts share one frequency options snapshot per emitted payload', () => {
const source = readMainSource();
const emitBlock = source.match(
/function emitSubtitlePayload\(payload: SubtitleData\): void \{(?<body>[\s\S]*?)\n\}/,
)?.groups?.body;
const frequencyOptionsSnapshot = emitBlock?.match(
/const frequencyDictionary = configService\.getConfig\(\)\.subtitleStyle\.frequencyDictionary;(?<body>[\s\S]*?)\n \};/,
)?.[0];
assert.ok(emitBlock);
assert.ok(frequencyOptionsSnapshot);
assert.match(frequencyOptionsSnapshot, /const frequencyOptions = \{/);
assert.match(frequencyOptionsSnapshot, /enabled:\s*frequencyDictionary\.enabled/);
assert.match(frequencyOptionsSnapshot, /topX:\s*frequencyDictionary\.topX/);
assert.match(frequencyOptionsSnapshot, /mode:\s*frequencyDictionary\.mode/);
assert.equal((frequencyOptionsSnapshot.match(/configService\.getConfig\(\)/g) ?? []).length, 1);
assert.match(emitBlock, /subtitleWsService\.broadcast\(timedPayload, frequencyOptions\);/);
assert.match(
emitBlock,
/annotationSubtitleWsService\.broadcast\(timedPayload, frequencyOptions\);/,
);
});
test('websocket frequency options callbacks each read one configuration snapshot', () => {
const source = readMainSource();
const subtitleBlock = source.match(
/startSubtitleWebsocket:\s*\(port: number\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},\n startAnnotationWebsocket:/,
)?.groups?.body;
const annotationBlock = source.match(
/startAnnotationWebsocket:\s*\(port: number\)\s*=>\s*\{(?<body>[\s\S]*?)\n \},\n startTexthooker:/,
)?.groups?.body;
const subtitleFrequencyOptionsCallback = subtitleBlock?.match(
/(?<body>\(\) => \{\s+const frequencyDictionary = configService\.getConfig\(\)\.subtitleStyle\.frequencyDictionary;[\s\S]*?\s+return \{[\s\S]*?\s+\};\s+\})/,
)?.groups?.body;
const annotationFrequencyOptionsCallback = annotationBlock?.match(
/(?<body>\(\) => \{\s+const frequencyDictionary = configService\.getConfig\(\)\.subtitleStyle\.frequencyDictionary;[\s\S]*?\s+return \{[\s\S]*?\s+\};\s+\})/,
)?.groups?.body;
assert.ok(subtitleFrequencyOptionsCallback);
assert.ok(annotationFrequencyOptionsCallback);
for (const callback of [subtitleFrequencyOptionsCallback, annotationFrequencyOptionsCallback]) {
assert.match(callback, /return \{[\s\S]*enabled:\s*frequencyDictionary\.enabled/);
assert.match(callback, /topX:\s*frequencyDictionary\.topX/);
assert.match(callback, /mode:\s*frequencyDictionary\.mode/);
assert.equal((callback.match(/configService\.getConfig\(\)/g) ?? []).length, 1);
}
}); });
test('mpv connection flushes queued configured OSD notifications', () => { test('mpv connection flushes queued configured OSD notifications', () => {
@@ -587,11 +639,11 @@ test('manual visible overlay show primes current subtitle from mpv before relyin
assert.ok(toggleBlock); assert.ok(toggleBlock);
assert.match( assert.match(
setBlock, setBlock,
/if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+startLinuxVisibleOverlayStartupInputGrace\(\);\s+restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);\s+void primeCurrentSubtitleForVisibleOverlay\(\);/, /if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+visibleOverlayInteractionRuntime\.resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+visibleOverlayInteractionRuntime\.startLinuxVisibleOverlayStartupInputGrace\(\);\s+visibleOverlayInteractionRuntime\.restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);\s+void autoplaySubtitlePrimingRuntime\.primeCurrentSubtitleForVisibleOverlay\(\);/,
); );
assert.match( assert.match(
toggleBlock, toggleBlock,
/else \{\s+maybeStartOverlayLoadingOsd\(\);\s+resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+startLinuxVisibleOverlayStartupInputGrace\(\);\s+restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);\s+void primeCurrentSubtitleForVisibleOverlay\(\);/, /else \{\s+maybeStartOverlayLoadingOsd\(\);\s+visibleOverlayInteractionRuntime\.resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+visibleOverlayInteractionRuntime\.startLinuxVisibleOverlayStartupInputGrace\(\);\s+visibleOverlayInteractionRuntime\.restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);\s+void autoplaySubtitlePrimingRuntime\.primeCurrentSubtitleForVisibleOverlay\(\);/,
); );
}); });
@@ -612,7 +664,7 @@ test('Linux visible overlay show/reset does not leave an empty X11 window shape'
assert.doesNotMatch(runtimeSource, /setShape\?\.\(\[\]\)|setShape\(\[\]\)/); assert.doesNotMatch(runtimeSource, /setShape\?\.\(\[\]\)|setShape\(\[\]\)/);
assert.match( assert.match(
setBlock, setBlock,
/if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+startLinuxVisibleOverlayStartupInputGrace\(\);\s+restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);/, /if \(visible\) \{\s+maybeStartOverlayLoadingOsd\(\);\s+visibleOverlayInteractionRuntime\.resetLinuxVisibleOverlayStartupInputPrimer\(\);\s+visibleOverlayInteractionRuntime\.startLinuxVisibleOverlayStartupInputGrace\(\);\s+visibleOverlayInteractionRuntime\.restoreVisibleOverlayWindowShapeForShow\(\);\s+void ensureOverlayMpvSubtitlesHidden\(\);/,
); );
}); });
@@ -663,14 +715,20 @@ test('Linux visible overlay show starts input grace before first measurement', (
for (const block of [setVisibleBlock, toggleBlock, setOverlayBlock]) { for (const block of [setVisibleBlock, toggleBlock, setOverlayBlock]) {
assert.ok(block); assert.ok(block);
assert.ok( const resetIndex = block.indexOf(
block.indexOf('resetLinuxVisibleOverlayStartupInputPrimer();') < 'visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();',
block.indexOf('startLinuxVisibleOverlayStartupInputGrace();'),
); );
assert.ok( const graceIndex = block.indexOf(
block.indexOf('startLinuxVisibleOverlayStartupInputGrace();') < 'visibleOverlayInteractionRuntime.startLinuxVisibleOverlayStartupInputGrace();',
block.indexOf('void primeCurrentSubtitleForVisibleOverlay();'),
); );
const primeIndex = block.indexOf(
'void autoplaySubtitlePrimingRuntime.primeCurrentSubtitleForVisibleOverlay();',
);
assert.ok(resetIndex >= 0);
assert.ok(graceIndex >= 0);
assert.ok(primeIndex >= 0);
assert.ok(resetIndex < graceIndex);
assert.ok(graceIndex < primeIndex);
} }
}); });
@@ -147,10 +147,7 @@ test('runSyncLauncher settles on exit when the terminal event is already parsed'
onEvent: () => {}, onEvent: () => {},
spawn, spawn,
}); });
children[0]!.stdout.emit( children[0]!.stdout.emit('data', Buffer.from('{"type":"result","ok":true,"error":null}\n'));
'data',
Buffer.from('{"type":"result","ok":true,"error":null}\n'),
);
children[0]!.emit('exit', 0, null); children[0]!.emit('exit', 0, null);
const result = await Promise.race([ const result = await Promise.race([
+2 -1
View File
@@ -106,7 +106,8 @@ export function parseSyncProgressLine(line: string): SyncProgressEvent | null {
? (parsed as SyncProgressEvent) ? (parsed as SyncProgressEvent)
: null; : null;
case 'merge-summary': case 'merge-summary':
return (event.target === 'local' || event.target === 'remote') && isMergeSummary(event.summary) return (event.target === 'local' || event.target === 'remote') &&
isMergeSummary(event.summary)
? (parsed as SyncProgressEvent) ? (parsed as SyncProgressEvent)
: null; : null;
case 'remote-output': case 'remote-output':
+1 -3
View File
@@ -60,9 +60,7 @@ export function tsukihimeTrackMatchesLanguages(
export function describeTsukihimeTabLanguages(configuredLanguages: string[]): string { export function describeTsukihimeTabLanguages(configuredLanguages: string[]): string {
const normalized = [ const normalized = [
...new Set( ...new Set(
configuredLanguages configuredLanguages.map((candidate) => normalizeTsukihimeLangCode(candidate)).filter(Boolean),
.map((candidate) => normalizeTsukihimeLangCode(candidate))
.filter(Boolean),
), ),
]; ];
if (normalized.length === 0) return 'English'; if (normalized.length === 0) return 'English';
+1 -4
View File
@@ -76,10 +76,7 @@ function asFiniteNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null; return typeof value === 'number' && Number.isFinite(value) ? value : null;
} }
export function mapTsukihimeSearchResults( export function mapTsukihimeSearchResults(payload: unknown, maxResults: number): TsukihimeEntry[] {
payload: unknown,
maxResults: number,
): TsukihimeEntry[] {
if (!isObject(payload) || !Array.isArray(payload.results)) return []; if (!isObject(payload) || !Array.isArray(payload.results)) return [];
const entries: TsukihimeEntry[] = []; const entries: TsukihimeEntry[] = [];
for (const item of payload.results) { for (const item of payload.results) {