Files
SubMiner/launcher/commands/command-modules.test.ts
T
sudacode 8fc20b2499 refactor: remove dead code and enable noUnusedLocals (#292)
* refactor: remove dead code and enable noUnusedLocals

- delete launcher/jimaku.ts and the launcher Args/config jimaku* fields
  that only fed it (the SUBMINER_JIMAKU_* env vars had no effect)
- delete plugin/subminer/hover.lua; nothing has sent subminer-hover-token
  since Overlay 2.0
- drop never-read anilist.characterDictionary.refreshTtlHours and
  evictionPolicy (still hidden in the settings UI for old configs)
- drop unused root deps: eslint, @fontsource-variable/geist{,-mono}
  (stats/ declares its own)
- delete the committed draft renderer-performance plan doc
- turn on noUnusedLocals for typecheck and remove what it surfaced,
  including Windows focus-handoff values computed but never consumed

* docs(changes): add docs fragment and note retired hover messages
2026-10-12 22:10:54 -07:00

545 lines
17 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { parseArgs } from '../config.js';
import type { ProcessAdapter } from '../process-adapter.js';
import type { LauncherCommandContext } from './context.js';
import { runConfigCommand } from './config-command.js';
import { runDictionaryCommand } from './dictionary-command.js';
import { runDoctorCommand } from './doctor-command.js';
import { runMpvPostAppCommand, runMpvPreAppCommand } from './mpv-command.js';
import { runAppPassthroughCommand } from './app-command.js';
import { runStatsCommand } from './stats-command.js';
class ExitSignal extends Error {
code: number;
constructor(code: number) {
super(`exit:${code}`);
this.code = code;
}
}
function createContext(overrides: Partial<LauncherCommandContext> = {}): LauncherCommandContext {
const args = parseArgs([], 'subminer', {});
const adapter: ProcessAdapter = {
platform: () => 'linux',
onSignal: () => {},
writeStdout: () => {},
exit: (code) => {
throw new ExitSignal(code);
},
setExitCode: () => {},
};
return {
args,
scriptPath: '/tmp/subminer',
scriptName: 'subminer',
mpvSocketPath: '/tmp/subminer.sock',
pluginRuntimeConfig: {
socketPath: '/tmp/subminer.sock',
binaryPath: '',
backend: 'auto',
autoStart: true,
autoStartVisibleOverlay: true,
autoStartPauseUntilReady: true,
osdMessages: false,
texthookerEnabled: false,
},
appPath: '/tmp/subminer.app',
launcherJellyfinConfig: {},
processAdapter: adapter,
...overrides,
};
}
type StatsTestArgOverrides = {
stats?: boolean;
statsBackground?: boolean;
statsCleanup?: boolean;
statsCleanupVocab?: boolean;
statsCleanupLifetime?: boolean;
statsStop?: boolean;
logLevel?: LauncherCommandContext['args']['logLevel'];
};
function createStatsTestHarness(overrides: StatsTestArgOverrides = {}) {
const context = createContext();
const forwarded: string[][] = [];
const removedPaths: string[] = [];
const createTempDir = (_prefix: string) => {
const created = `/tmp/subminer-stats-test`;
return created;
};
const joinPath = (...parts: string[]) => parts.join('/');
const removeDir = (targetPath: string) => {
removedPaths.push(targetPath);
};
const runAppCommandAttachedStub = async (
_appPath: string,
appArgs: string[],
_logLevel: LauncherCommandContext['args']['logLevel'],
_label: string,
) => {
forwarded.push(appArgs);
return 0;
};
const waitForStatsResponseStub = async () => ({ ok: true, url: 'http://127.0.0.1:5175' });
context.args = {
...context.args,
stats: true,
...overrides,
};
return {
context,
forwarded,
removedPaths,
createTempDir,
joinPath,
removeDir,
runAppCommandAttachedStub,
waitForStatsResponseStub,
commandDeps: {
createTempDir,
joinPath,
runAppCommandAttached: runAppCommandAttachedStub,
waitForStatsResponse: waitForStatsResponseStub,
removeDir,
},
};
}
test('config command writes newline-terminated path via process adapter', () => {
const writes: string[] = [];
const context = createContext();
context.args.configPath = true;
context.processAdapter = {
...context.processAdapter,
writeStdout: (text) => writes.push(text),
};
const handled = runConfigCommand(context, {
existsSync: () => true,
readFileSync: () => '',
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
});
assert.equal(handled, true);
assert.deepEqual(writes, ['/tmp/SubMiner/config.jsonc\n']);
});
test('doctor command exits non-zero for missing hard dependencies', () => {
const context = createContext({ appPath: null });
context.args.doctor = true;
assert.throws(
() =>
runDoctorCommand(context, {
commandExists: () => false,
configExists: () => true,
resolveMainConfigPath: () => '/tmp/SubMiner/config.jsonc',
runAppCommandWithInherit: () => {
throw new Error('unexpected app handoff');
},
}),
(error: unknown) => error instanceof ExitSignal && error.code === 1,
);
});
test('app command starts default background app detached from launcher app detached from launcher', () => {
const context = createContext();
context.args.appPassthrough = true;
context.args.appArgs = [];
const calls: string[] = [];
const handled = runAppPassthroughCommand(context, {
runAppCommandWithInherit: () => {
calls.push('attached');
},
launchSyncUiDetached: () => calls.push('sync-ui'),
launchYoutubeBrowserDetached: () => calls.push('youtube'),
launchAppBackgroundDetached: (appPath, logLevel) => {
calls.push(`detached:${appPath}:${logLevel}`);
},
});
assert.equal(handled, true);
assert.deepEqual(calls, ['detached:/tmp/subminer.app:warn']);
});
test('app command keeps explicit passthrough args attached', () => {
const context = createContext();
context.args.appPassthrough = true;
context.args.appArgs = ['--settings'];
const forwarded: string[][] = [];
const detached: string[] = [];
const handled = runAppPassthroughCommand(context, {
runAppCommandWithInherit: (_appPath, appArgs) => {
forwarded.push(appArgs);
},
launchSyncUiDetached: () => detached.push('sync-ui'),
launchYoutubeBrowserDetached: () => detached.push('youtube'),
launchAppBackgroundDetached: () => {
detached.push('detached');
},
});
assert.equal(handled, true);
assert.deepEqual(forwarded, [['--settings']]);
assert.deepEqual(detached, []);
});
test('sync UI command launches the app detached from the terminal', () => {
const context = createContext();
context.args.syncUi = true;
const calls: string[] = [];
const handled = runAppPassthroughCommand(context, {
runAppCommandWithInherit: () => calls.push('piped'),
launchSyncUiDetached: (appPath, logLevel) => calls.push(`sync-ui:${appPath}:${logLevel}`),
launchYoutubeBrowserDetached: () => calls.push('youtube'),
launchAppBackgroundDetached: () => calls.push('detached'),
});
assert.equal(handled, true);
assert.deepEqual(calls, ['sync-ui:/tmp/subminer.app:warn']);
});
test('youtube command launches the YouTube browser detached from the terminal', () => {
const context = createContext();
context.args.youtubeBrowser = true;
const calls: string[] = [];
const handled = runAppPassthroughCommand(context, {
runAppCommandWithInherit: () => calls.push('piped'),
launchSyncUiDetached: () => calls.push('sync-ui'),
launchYoutubeBrowserDetached: (appPath, logLevel) =>
calls.push(`youtube:${appPath}:${logLevel}`),
launchAppBackgroundDetached: () => calls.push('detached'),
});
assert.equal(handled, true);
assert.deepEqual(calls, ['youtube:/tmp/subminer.app:warn']);
});
test('mpv pre-app command exits non-zero when socket is not ready', async () => {
const context = createContext();
context.args.mpvStatus = true;
await assert.rejects(
async () => {
await runMpvPreAppCommand(context, {
ensureRuntimePluginReady: async () => {},
waitForUnixSocketReady: async () => false,
resolveRuntimePluginPath: () => null,
launchMpvIdleDetached: async () => {},
});
},
(error: unknown) => error instanceof ExitSignal && error.code === 1,
);
});
test('mpv idle command ensures Linux runtime plugin before detached launch', async () => {
const context = createContext();
context.args.mpvIdle = true;
const calls: string[] = [];
const handled = await runMpvPostAppCommand(context, {
ensureRuntimePluginReady: async () => {
calls.push('plugin');
},
waitForUnixSocketReady: async () => {
calls.push('wait');
return true;
},
launchMpvIdleDetached: async () => {
calls.push('launch');
},
resolveRuntimePluginPath: () => {
calls.push('resolve');
return '/tmp/plugin/main.lua';
},
});
assert.equal(handled, true);
assert.deepEqual(calls, ['plugin', 'resolve', 'launch', 'wait']);
});
test('dictionary command forwards --dictionary and target path to app binary', () => {
const context = createContext();
context.args.dictionary = true;
context.args.dictionaryTarget = '/tmp/anime';
const forwarded: string[][] = [];
const handled = runDictionaryCommand(context, {
runAppCommandWithInherit: (_appPath, appArgs) => {
forwarded.push(appArgs);
},
});
assert.equal(handled, true);
assert.deepEqual(forwarded, [['--start', '--dictionary', '--dictionary-target', '/tmp/anime']]);
});
test('dictionary command forwards candidate and selection modes to app binary', () => {
const candidatesContext = createContext();
candidatesContext.args.dictionary = true;
candidatesContext.args.dictionaryCandidates = true;
candidatesContext.args.dictionaryTarget = '/tmp/anime.mkv';
const selectContext = createContext();
selectContext.args.dictionary = true;
selectContext.args.dictionarySelect = true;
selectContext.args.dictionaryAnilistId = 21355;
selectContext.args.dictionaryTarget = '/tmp/anime.mkv';
const forwarded: string[][] = [];
runDictionaryCommand(candidatesContext, {
runAppCommandWithInherit: (_appPath, appArgs) => {
forwarded.push(appArgs);
},
});
runDictionaryCommand(selectContext, {
runAppCommandWithInherit: (_appPath, appArgs) => {
forwarded.push(appArgs);
},
});
assert.deepEqual(forwarded, [
['--start', '--dictionary-candidates', '--dictionary-target', '/tmp/anime.mkv'],
[
'--start',
'--dictionary-select',
'--dictionary-anilist-id',
'21355',
'--dictionary-target',
'/tmp/anime.mkv',
],
]);
});
test('stats command launches attached app command with response path', async () => {
const harness = createStatsTestHarness({ stats: true, logLevel: 'debug' });
const handled = await runStatsCommand(harness.context, harness.commandDeps);
assert.equal(handled, true);
assert.deepEqual(harness.forwarded, [
[
'--stats',
'--stats-response-path',
'/tmp/subminer-stats-test/response.json',
'--log-level',
'debug',
],
]);
assert.equal(harness.removedPaths.length, 1);
});
const STATS_RESPONSE_ARGS = ['--stats-response-path', '/tmp/subminer-stats-test/response.json'];
const statsForwardingCases = [
{
name: 'stats background command launches the daemon start command',
overrides: { statsBackground: true },
expected: ['--stats-daemon-start', ...STATS_RESPONSE_ARGS],
},
{
name: 'stats stop command forwards the daemon stop flag',
overrides: { statsStop: true },
expected: ['--stats-daemon-stop', ...STATS_RESPONSE_ARGS],
},
{
name: 'stats cleanup command forwards cleanup vocab flags',
overrides: { statsCleanup: true, statsCleanupVocab: true },
expected: ['--stats', ...STATS_RESPONSE_ARGS, '--stats-cleanup', '--stats-cleanup-vocab'],
},
{
name: 'stats cleanup command forwards the lifetime rebuild flag',
overrides: { statsCleanup: true, statsCleanupLifetime: true },
expected: ['--stats', ...STATS_RESPONSE_ARGS, '--stats-cleanup', '--stats-cleanup-lifetime'],
},
];
for (const c of statsForwardingCases) {
test(`${c.name} to the app`, async () => {
const harness = createStatsTestHarness({ stats: true, ...c.overrides });
const handled = await runStatsCommand(harness.context, harness.commandDeps);
assert.equal(handled, true);
assert.deepEqual(harness.forwarded, [c.expected]);
assert.equal(harness.removedPaths.length, 1);
});
}
test('stats command waits for attached app exit after startup response', async () => {
const harness = createStatsTestHarness({ stats: true });
const started = new Promise<number>((resolve) => setTimeout(() => resolve(0), 20));
const statsCommand = runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
return started;
},
});
const result = await Promise.race([
statsCommand.then(() => 'resolved'),
new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 5)),
]);
assert.equal(result, 'timeout');
const final = await statsCommand;
assert.equal(final, true);
assert.deepEqual(harness.forwarded, [
['--stats', '--stats-response-path', '/tmp/subminer-stats-test/response.json'],
]);
assert.equal(harness.removedPaths.length, 1);
});
test('stats command throws when attached app exits non-zero after startup response', async () => {
const harness = createStatsTestHarness({ stats: true });
await assert.rejects(async () => {
await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
await new Promise((resolve) => setTimeout(resolve, 10));
return 3;
},
});
}, /Stats app exited with status 3\./);
assert.equal(harness.removedPaths.length, 1);
});
test('stats stop command exits on process exit without waiting for startup response', async () => {
const harness = createStatsTestHarness({ stats: true, statsStop: true });
let waitedForResponse = false;
const handled = await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
return 0;
},
waitForStatsResponse: async () => {
waitedForResponse = true;
return { ok: true };
},
});
assert.equal(handled, true);
assert.equal(waitedForResponse, false);
assert.equal(harness.removedPaths.length, 1);
});
test('stats command throws when stats response reports an error', async () => {
const harness = createStatsTestHarness({ stats: true });
await assert.rejects(async () => {
await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
return 0;
},
waitForStatsResponse: async () => ({
ok: false,
error: 'Immersion tracking is disabled in config.',
}),
});
}, /Immersion tracking is disabled in config\./);
assert.equal(harness.removedPaths.length, 1);
});
test('stats cleanup command fails if attached app exits before startup response', async () => {
const harness = createStatsTestHarness({
stats: true,
statsCleanup: true,
statsCleanupVocab: true,
});
await assert.rejects(async () => {
await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
return 2;
},
waitForStatsResponse: async () => {
await new Promise((resolve) => setTimeout(resolve, 25));
return { ok: true, url: 'http://127.0.0.1:5175' };
},
});
}, /Stats app exited before startup response \(status 2\)\./);
assert.equal(harness.removedPaths.length, 1);
});
test('stats command aborts pending response wait when app exits before startup response', async () => {
const harness = createStatsTestHarness({ stats: true });
let aborted = false;
await assert.rejects(async () => {
await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
return 2;
},
waitForStatsResponse: async (_responsePath, signal) =>
await new Promise((resolve) => {
signal?.addEventListener(
'abort',
() => {
aborted = true;
resolve({ ok: false, error: 'aborted' });
},
{ once: true },
);
}),
});
}, /Stats app exited before startup response \(status 2\)\./);
assert.equal(aborted, true);
assert.equal(harness.removedPaths.length, 1);
});
test('stats command aborts pending response wait when attached app fails to spawn', async () => {
const harness = createStatsTestHarness({ stats: true });
const spawnError = new Error('spawn failed');
let aborted = false;
await assert.rejects(
async () => {
await runStatsCommand(harness.context, {
...harness.commandDeps,
runAppCommandAttached: async (...args) => {
await harness.runAppCommandAttachedStub(...args);
throw spawnError;
},
waitForStatsResponse: async (_responsePath, signal) =>
await new Promise((resolve) => {
signal?.addEventListener(
'abort',
() => {
aborted = true;
resolve({ ok: false, error: 'aborted' });
},
{ once: true },
);
}),
});
},
(error: unknown) => error === spawnError,
);
assert.equal(aborted, true);
assert.equal(harness.removedPaths.length, 1);
});