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/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.ts` call sites invoke configured runtime handlers and runtime-object methods directly;
do not add local pass-through wrappers around them.
## 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
// is re-added when that session finalizes and syncs.
assert.equal(
queryOne<{ frequency: number }>(localPath, `SELECT frequency FROM imm_words WHERE word = '食べた'`)
?.frequency,
queryOne<{ frequency: number }>(
localPath,
`SELECT frequency FROM imm_words WHERE word = '食べた'`,
)?.frequency,
1,
);
@@ -650,8 +652,10 @@ test('adopted word frequency excludes active-session counts that merge later', (
assert.equal(second.sessionsAlreadyPresent, 1);
// 1 (ended session) + 4 (finalized session), not 5 + 4 = 9.
assert.equal(
queryOne<{ frequency: number }>(localPath, `SELECT frequency FROM imm_words WHERE word = '食べた'`)
?.frequency,
queryOne<{ frequency: number }>(
localPath,
`SELECT frequency FROM imm_words WHERE word = '食べた'`,
)?.frequency,
5,
);
} finally {
@@ -404,8 +404,7 @@ export function buildIntegrationConfigOptionRegistry(
path: 'tsukihime.apiBaseUrl',
kind: 'string',
defaultValue: defaultConfig.tsukihime.apiBaseUrl,
description:
'Base URL of the TsukiHime API (Animetosho successor). No API key required.',
description: 'Base URL of the TsukiHime API (Animetosho successor). No API key required.',
},
{
path: 'tsukihime.maxSearchResults',
+1 -3
View File
@@ -302,9 +302,7 @@ export function registerAnkiJimakuIpcHandlers(
await deps.onDownloadedSecondarySubtitle(result.path);
}
} else {
logger.error(
`[tsukihime] download-file failed: ${result.error?.error ?? 'unknown error'}`,
);
logger.error(`[tsukihime] download-file failed: ${result.error?.error ?? 'unknown error'}`);
}
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', '--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', () => {
+7 -2
View File
@@ -58,7 +58,9 @@ export function parseSyncCliTokens(tokens: readonly string[]): ParsedSyncCli {
for (let i = 0; i < rest.length; i += 1) {
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 (token.includes('=')) {
assignValue(token.slice(token.indexOf('=') + 1));
@@ -109,7 +111,10 @@ export function parseSyncCliTokens(tokens: readonly string[]): ParsedSyncCli {
Boolean(removeTemp),
].filter(Boolean).length;
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) {
return {
+1 -3
View File
@@ -34,9 +34,7 @@ export function resolveImmersionDbPath(): string {
// no config or unreadable config → default location
}
if (configured) {
return configured.startsWith('~')
? path.join(os.homedir(), configured.slice(1))
: configured;
return configured.startsWith('~') ? path.join(os.homedir(), configured.slice(1)) : configured;
}
return path.join(getDefaultConfigDir(), 'immersion.sqlite');
}
@@ -251,11 +251,7 @@ export function mergeMediaMetadata(
}
}
export function mergeExcludedWords(
local: SyncDb,
remote: SyncDb,
summary: SyncMergeSummary,
): void {
export function mergeExcludedWords(local: SyncDb, remote: SyncDb, summary: SyncMergeSummary): void {
if (
!tableExists(remote, 'imm_stats_excluded_words') ||
!tableExists(local, 'imm_stats_excluded_words')
@@ -147,7 +147,9 @@ export function refreshRollupsForNewSessions(
}
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(
'DELETE FROM imm_monthly_rollups WHERE rollup_month = ? AND video_id = ?',
);
@@ -319,7 +319,8 @@ function applyMergedSessionLifetime(
.get(videoId, sessionId),
);
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(
local
+1 -5
View File
@@ -8,11 +8,7 @@ import {
} from './merge-catalog';
import { mergeSessions } from './merge-sessions';
import { copyRemoteOnlyRollups, refreshRollupsForNewSessions } from './merge-rollups';
import {
assertMergeableSchema,
createEmptyMergeSummary,
type SyncMergeSummary,
} from './shared';
import { assertMergeableSchema, createEmptyMergeSummary, type SyncMergeSummary } from './shared';
import { openLibsqlSyncDb, type SyncDb } from './libsql-driver';
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) => {
execFile(
'/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 },
(error) => {
if (error && typeof error.code !== 'number') {
@@ -69,113 +75,107 @@ function writeExecutable(filePath: string, content: string): void {
fs.writeFileSync(filePath, content, { mode: 0o755 });
}
test(
'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);
const linuxTest = process.platform === 'linux' ? test : test.skip;
// AppRun leaves behind a straggler that keeps executing *from the mount*
// after AppRun itself exits — mimicking Chromium utility children.
fs.copyFileSync('/usr/bin/sleep', path.join(mountDir, 'straggler'));
fs.chmodSync(path.join(mountDir, 'straggler'), 0o755);
writeExecutable(
path.join(mountDir, 'AppRun'),
[
'#!/bin/sh',
`"${mountDir}/straggler" 1 &`,
`date +%s%N > "${resultsDir}/apprun-exited"`,
'exit 42',
].join('\n'),
linuxTest('keepalive script releases the mount only after straggler processes exit', 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*
// after AppRun itself exits — mimicking Chromium utility children.
fs.copyFileSync('/usr/bin/sleep', path.join(mountDir, 'straggler'));
fs.chmodSync(path.join(mountDir, 'straggler'), 0o755);
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',
);
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
// (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));
// 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);
}
assert.ok(holderReleased !== null, 'holder release timestamp must be recorded');
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 });
if (holderReleased !== null) break;
await new Promise((r) => setTimeout(r, 25));
}
},
);
assert.ok(holderReleased !== null, 'holder release timestamp must be recorded');
test(
'keepalive script falls back to direct run when --appimage-mount fails',
{ 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 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 });
}
});
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 });
}
},
);
linuxTest('keepalive script falls back to direct run when --appimage-mount fails', 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'),
);
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;
assert.ok(actionBlock);
assert.match(actionBlock, /const trackedGeometry = getCurrentTrackedOverlayGeometry\(\);/);
assert.match(
actionBlock,
/const trackedGeometry = overlayGeometryRuntime\.getCurrentTrackedOverlayGeometry\(\);/,
);
assert.match(actionBlock, /if \(trackedGeometry\) \{/);
assert.match(actionBlock, /overlayManager\.setOverlayWindowBounds\(trackedGeometry\);/);
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, /notifyNoQueued:\s*false/);
assert.match(startCacheBlock, /mode:\s*getResolvedConfig\(\)\.youtube\.mediaCache\.mode/);
assert.match(
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', () => {
@@ -587,11 +639,11 @@ test('manual visible overlay show primes current subtitle from mpv before relyin
assert.ok(toggleBlock);
assert.match(
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(
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.match(
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]) {
assert.ok(block);
assert.ok(
block.indexOf('resetLinuxVisibleOverlayStartupInputPrimer();') <
block.indexOf('startLinuxVisibleOverlayStartupInputGrace();'),
const resetIndex = block.indexOf(
'visibleOverlayInteractionRuntime.resetLinuxVisibleOverlayStartupInputPrimer();',
);
assert.ok(
block.indexOf('startLinuxVisibleOverlayStartupInputGrace();') <
block.indexOf('void primeCurrentSubtitleForVisibleOverlay();'),
const graceIndex = block.indexOf(
'visibleOverlayInteractionRuntime.startLinuxVisibleOverlayStartupInputGrace();',
);
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: () => {},
spawn,
});
children[0]!.stdout.emit(
'data',
Buffer.from('{"type":"result","ok":true,"error":null}\n'),
);
children[0]!.stdout.emit('data', Buffer.from('{"type":"result","ok":true,"error":null}\n'));
children[0]!.emit('exit', 0, null);
const result = await Promise.race([
+2 -1
View File
@@ -106,7 +106,8 @@ export function parseSyncProgressLine(line: string): SyncProgressEvent | null {
? (parsed as SyncProgressEvent)
: null;
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)
: null;
case 'remote-output':
+1 -3
View File
@@ -60,9 +60,7 @@ export function tsukihimeTrackMatchesLanguages(
export function describeTsukihimeTabLanguages(configuredLanguages: string[]): string {
const normalized = [
...new Set(
configuredLanguages
.map((candidate) => normalizeTsukihimeLangCode(candidate))
.filter(Boolean),
configuredLanguages.map((candidate) => normalizeTsukihimeLangCode(candidate)).filter(Boolean),
),
];
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;
}
export function mapTsukihimeSearchResults(
payload: unknown,
maxResults: number,
): TsukihimeEntry[] {
export function mapTsukihimeSearchResults(payload: unknown, maxResults: number): TsukihimeEntry[] {
if (!isObject(payload) || !Array.isArray(payload.results)) return [];
const entries: TsukihimeEntry[] = [];
for (const item of payload.results) {