Compare commits

..

2 Commits

Author SHA1 Message Date
sudacode 2398f5c030 refactor(main): eliminate local pass-through wrappers in main.ts (#168) 2026-07-15 03:22:03 -07:00
sudacode 8711cf1a48 remove redundant isVerbNonIndependent and consolidate test lanes
- drop always-true isVerbNonIndependent export; collapse shouldMerge branch
- remove node-compat exclusion list from bun-src-full lane
- simplify test:fast (drop test:runtime:compat), test:env, test:smoke:dist scripts
- rename test:launcher:env:src -> test:env in docs
2026-07-14 19:57:50 -07:00
25 changed files with 722 additions and 845 deletions
+1 -1
View File
@@ -135,7 +135,7 @@ Focused commands:
bun run test:config # Source-level config schema/validation tests bun run test:config # Source-level config schema/validation tests
bun run test:launcher # Launcher regression tests (config discovery + command routing) bun run test:launcher # Launcher regression tests (config discovery + command routing)
bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring bun run test:launcher:smoke:src # Launcher e2e smoke: launcher -> mpv IPC -> overlay start/stop wiring
bun run test:launcher:env:src # Launcher smoke + Lua plugin gate bun run test:env # Launcher smoke + Lua plugin gate
bun run test:src # Bun-managed maintained src/** discovery lane bun run test:src # Bun-managed maintained src/** discovery lane
bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane bun run test:launcher:unit:src # Bun-managed maintained launcher unit lane
bun run test:scripts # Bun-managed scripts/** test lane bun run test:scripts # Bun-managed scripts/** test lane
+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
+1 -1
View File
@@ -17,7 +17,7 @@ Read when: selecting the right verification lane for a change
one file cannot cascade into the rest of the lane. `--jobs N` parallelizes; one file cannot cascade into the rest of the lane. `--jobs N` parallelizes;
`--single-process` restores the shared-process mode for debugging. `--single-process` restores the shared-process mode for debugging.
- `bun run test:fast` is the full source gate: discovered `src/**`, launcher - `bun run test:fast` is the full source gate: discovered `src/**`, launcher
unit, `scripts/**`, and the compiled runtime-compat slice. unit, and `scripts/**`.
- `.github/workflows/quality-gate.yml` is the reusable `workflow_call` gate for - `.github/workflows/quality-gate.yml` is the reusable `workflow_call` gate for
pull requests, stable tags, and prerelease tags. Keep common quality steps pull requests, stable tags, and prerelease tags. Keep common quality steps
there instead of copying them into caller workflows. there instead of copying them into caller workflows.
+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 {
+6 -12
View File
@@ -47,13 +47,9 @@
"docs:preview": "bun run --cwd docs-site docs:preview", "docs:preview": "bun run --cwd docs-site docs:preview",
"docs:test": "bun run --cwd docs-site test", "docs:test": "bun run --cwd docs-site test",
"test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts", "test:docs:kb": "bun test scripts/docs-knowledge-base.test.ts",
"test:config:src": "bun scripts/run-test-lane.mjs config",
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
"test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-process-start-retries.lua && lua scripts/test-plugin-restart-feedback.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-process-start-retries.lua && lua scripts/test-plugin-restart-feedback.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua",
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
"test:launcher:src": "bun scripts/run-test-lane.mjs launcher && bun run test:plugin:src", "test:smoke:dist": "bun scripts/run-test-lane.mjs bun-src-full",
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
"test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts", "test:immersion:sqlite:src": "bun test src/core/services/immersion-tracker-service.test.ts src/core/services/immersion-tracker/storage-session.test.ts",
"test:immersion:sqlite:dist": "bun test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js", "test:immersion:sqlite:dist": "bun test dist/core/services/immersion-tracker-service.test.js dist/core/services/immersion-tracker/storage-session.test.js",
@@ -64,15 +60,13 @@
"test:launcher:unit:src": "bun scripts/run-test-lane.mjs bun-launcher-unit", "test:launcher:unit:src": "bun scripts/run-test-lane.mjs bun-launcher-unit",
"test:scripts": "bun scripts/run-test-lane.mjs scripts", "test:scripts": "bun scripts/run-test-lane.mjs scripts",
"test:stats": "bun scripts/run-test-lane.mjs stats", "test:stats": "bun scripts/run-test-lane.mjs stats",
"test:launcher:env:src": "bun run test:launcher:smoke:src && bun run test:plugin:src", "test:env": "bun run test:launcher:smoke:src && bun run test:plugin:src && bun run test:immersion:sqlite:src",
"test:env": "bun run test:launcher:env:src && bun run test:immersion:sqlite:src", "test:runtime:compat": "bun run tsc && bun scripts/run-test-lane.mjs bun-src-full",
"test:runtime:compat": "bun run tsc && bun test dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/overlay-manager.test.js dist/main/config-validation.test.js dist/main/runtime/registry.test.js dist/main/runtime/startup-config.test.js",
"test:node:compat": "bun run test:runtime:compat",
"test": "bun run test:fast", "test": "bun run test:fast",
"test:config": "bun run test:config:src", "test:config": "bun scripts/run-test-lane.mjs config",
"test:launcher": "bun run test:launcher:src", "test:launcher": "bun scripts/run-test-lane.mjs launcher && bun run test:plugin:src",
"test:subtitle": "bun run test:subtitle:src", "test:subtitle": "bun run test:subtitle:src",
"test:fast": "bun run test:src && bun run test:launcher:unit:src && bun run test:scripts && bun run test:runtime:compat", "test:fast": "bun run test:src && bun run test:launcher:unit:src && bun run test:scripts",
"generate:config-example": "bun run src/generate-config-example.ts", "generate:config-example": "bun run src/generate-config-example.ts",
"verify:config-example": "bun run src/verify-config-example.ts", "verify:config-example": "bun run src/verify-config-example.ts",
"start": "bun run build && electron . --start", "start": "bun run build && electron . --start",
-9
View File
@@ -16,15 +16,6 @@ export const testLanes: Record<string, TestLane> = {
'bun-src-full': { 'bun-src-full': {
roots: ['src'], roots: ['src'],
include: ['.test.ts', '.type-test.ts'], include: ['.test.ts', '.type-test.ts'],
// Node-compat suites; their dist builds run via test:runtime:compat.
exclude: [
'src/core/services/anki-jimaku-ipc.test.ts',
'src/core/services/ipc.test.ts',
'src/core/services/overlay-manager.test.ts',
'src/main/config-validation.test.ts',
'src/main/runtime/registry.test.ts',
'src/main/runtime/startup-config.test.ts',
],
}, },
config: { config: {
roots: ['src/config'], roots: ['src/config'],
@@ -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':
+7
View File
@@ -0,0 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as tokenMerger from './token-merger';
test('does not expose the redundant verb non-independent predicate', () => {
assert.equal('isVerbNonIndependent' in tokenMerger, false);
});
+1 -8
View File
@@ -94,10 +94,6 @@ export function isVerb(tok: Token): boolean {
return [PartOfSpeech.verb, PartOfSpeech.bound_auxiliary].includes(tok.partOfSpeech); return [PartOfSpeech.verb, PartOfSpeech.bound_auxiliary].includes(tok.partOfSpeech);
} }
export function isVerbNonIndependent(): boolean {
return true;
}
export function canReceiveAuxiliary(tok: Token): boolean { export function canReceiveAuxiliary(tok: Token): boolean {
return [PartOfSpeech.verb, PartOfSpeech.bound_auxiliary, PartOfSpeech.i_adjective].includes( return [PartOfSpeech.verb, PartOfSpeech.bound_auxiliary, PartOfSpeech.i_adjective].includes(
tok.partOfSpeech, tok.partOfSpeech,
@@ -127,10 +123,7 @@ export function shouldMerge(lastStandaloneToken: Token, token: Token): boolean {
if (isAuxVerb(token)) { if (isAuxVerb(token)) {
return true; return true;
} }
if (isContinuativeForm(lastStandaloneToken) && isVerbSuffix(token)) { if (isVerbSuffix(token)) {
return true;
}
if (isVerbSuffix(token) && isVerbNonIndependent()) {
return true; return true;
} }
} }
+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) {