import test from 'node:test'; import assert from 'node:assert/strict'; import { buildWindowsMpvLaunchArgs, launchWindowsMpv, resolveWindowsMpvPath, type WindowsMpvLaunchDeps, } from './windows-mpv-launch'; function createDeps(overrides: Partial = {}): WindowsMpvLaunchDeps { return { getEnv: () => undefined, runWhere: () => ({ status: 1, stdout: '' }), fileExists: () => false, spawnDetached: async () => undefined, showError: () => undefined, ...overrides, }; } test('resolveWindowsMpvPath prefers SUBMINER_MPV_PATH', () => { const resolved = resolveWindowsMpvPath( createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', }), ); assert.equal(resolved, 'C:\\mpv\\mpv.exe'); }); test('resolveWindowsMpvPath prefers configured executable path before PATH', () => { const resolved = resolveWindowsMpvPath( createDeps({ getEnv: () => undefined, runWhere: () => ({ status: 0, stdout: 'C:\\tools\\mpv.exe\r\n' }), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', }), ' C:\\mpv\\mpv.exe ', ); assert.equal(resolved, 'C:\\mpv\\mpv.exe'); }); test('resolveWindowsMpvPath falls back to where.exe output', () => { const resolved = resolveWindowsMpvPath( createDeps({ runWhere: () => ({ status: 0, stdout: 'C:\\tools\\mpv.exe\r\nC:\\other\\mpv.exe\r\n' }), fileExists: (candidate) => candidate === 'C:\\tools\\mpv.exe', }), ); assert.equal(resolved, 'C:\\tools\\mpv.exe'); }); test('buildWindowsMpvLaunchArgs uses explicit SubMiner defaults and targets', () => { assert.deepEqual( buildWindowsMpvLaunchArgs( ['C:\\a.mkv', 'C:\\b.mkv'], [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', ), [ '--player-operation-mode=pseudo-gui', '--force-window=immediate', '--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '--input-ipc-server=\\\\.\\pipe\\subminer-socket', '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--sub-auto=fuzzy', '--sub-file-paths=subs;subtitles', '--sid=auto', '--secondary-sid=auto', '--sub-visibility=no', '--secondary-sub-visibility=no', '--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket', 'C:\\a.mkv', 'C:\\b.mkv', ], ); }); test('buildWindowsMpvLaunchArgs inserts maximized launch mode before explicit extra args when configured', () => { assert.deepEqual( buildWindowsMpvLaunchArgs( ['C:\\video.mkv'], ['--window-maximized=no'], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', 'maximized', ), [ '--player-operation-mode=pseudo-gui', '--force-window=immediate', '--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '--input-ipc-server=\\\\.\\pipe\\subminer-socket', '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--sub-auto=fuzzy', '--sub-file-paths=subs;subtitles', '--sid=auto', '--secondary-sid=auto', '--sub-visibility=no', '--secondary-sub-visibility=no', '--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket', '--window-maximized=yes', '--window-maximized=no', 'C:\\video.mkv', ], ); }); test('buildWindowsMpvLaunchArgs keeps shortcut-only launches in idle mode', () => { assert.deepEqual( buildWindowsMpvLaunchArgs( [], [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', ), [ '--player-operation-mode=pseudo-gui', '--force-window=immediate', '--idle=yes', '--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '--input-ipc-server=\\\\.\\pipe\\subminer-socket', '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--sub-auto=fuzzy', '--sub-file-paths=subs;subtitles', '--sid=auto', '--secondary-sid=auto', '--sub-visibility=no', '--secondary-sub-visibility=no', '--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket', ], ); }); test('buildWindowsMpvLaunchArgs mirrors a custom input-ipc-server into script opts', () => { assert.deepEqual( buildWindowsMpvLaunchArgs( ['C:\\video.mkv'], ['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket'], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', ), [ '--player-operation-mode=pseudo-gui', '--force-window=immediate', '--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '--input-ipc-server=\\\\.\\pipe\\custom-subminer-socket', '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--sub-auto=fuzzy', '--sub-file-paths=subs;subtitles', '--sid=auto', '--secondary-sid=auto', '--sub-visibility=no', '--secondary-sub-visibility=no', '--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket', '--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket', 'C:\\video.mkv', ], ); }); test('buildWindowsMpvLaunchArgs includes socket script opts when plugin entrypoint is present without binary path', () => { assert.deepEqual( buildWindowsMpvLaunchArgs( ['C:\\video.mkv'], ['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket'], undefined, 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', ), [ '--player-operation-mode=pseudo-gui', '--force-window=immediate', '--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '--input-ipc-server=\\\\.\\pipe\\custom-subminer-socket', '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', '--sub-auto=fuzzy', '--sub-file-paths=subs;subtitles', '--sid=auto', '--secondary-sid=auto', '--sub-visibility=no', '--secondary-sub-visibility=no', '--script-opts=subminer-socket_path=\\\\.\\pipe\\custom-subminer-socket', '--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket', 'C:\\video.mkv', ], ); }); test('buildWindowsMpvLaunchArgs uses runtime plugin config script opts', () => { const args = buildWindowsMpvLaunchArgs( ['C:\\video.mkv'], ['--input-ipc-server', '\\\\.\\pipe\\custom-subminer-socket'], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', 'normal', { socketPath: '\\\\.\\pipe\\ignored-config-socket', binaryPath: 'C:\\Custom\\SubMiner.exe', backend: 'windows', autoStart: true, autoStartVisibleOverlay: false, autoStartPauseUntilReady: false, osdMessages: false, texthookerEnabled: false, }, ); const scriptOpts = args.find((arg) => arg.startsWith('--script-opts=')); assert.match(scriptOpts ?? '', /subminer-binary_path=C:\\Custom\\SubMiner\.exe/); assert.match(scriptOpts ?? '', /subminer-socket_path=\\\\\.\\pipe\\custom-subminer-socket/); assert.match(scriptOpts ?? '', /subminer-backend=windows/); assert.match(scriptOpts ?? '', /subminer-auto_start=yes/); assert.match(scriptOpts ?? '', /subminer-auto_start_visible_overlay=no/); assert.match(scriptOpts ?? '', /subminer-auto_start_pause_until_ready=no/); assert.match(scriptOpts ?? '', /subminer-texthooker_enabled=no/); }); test('buildWindowsMpvLaunchArgs keeps Windows ipc default unless explicitly overridden', () => { const args = buildWindowsMpvLaunchArgs( ['C:\\video.mkv'], [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', 'normal', { socketPath: 'C:\\Users\\tester\\AppData\\Local\\Temp\\subminer-smoke-sock\\subminer.sock', binaryPath: '', backend: 'windows', autoStart: true, autoStartVisibleOverlay: true, autoStartPauseUntilReady: true, osdMessages: false, texthookerEnabled: false, }, ); assert.ok(args.includes('--input-ipc-server=\\\\.\\pipe\\subminer-socket')); const scriptOpts = args.find((arg) => arg.startsWith('--script-opts=')); assert.match(scriptOpts ?? '', /subminer-socket_path=\\\\\.\\pipe\\subminer-socket/); }); test('launchWindowsMpv attaches a launched video to a running app and disables plugin auto-start', async () => { const spawnedArgs: string[][] = []; const controlArgv: string[][] = []; const waitedSockets: Array<{ socketPath: string; timeoutMs: number }> = []; const logs: string[] = []; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', isAppControlServerAvailable: async () => true, waitForSocketReady: async (socketPath, timeoutMs) => { waitedSockets.push({ socketPath, timeoutMs }); return true; }, sendAppControlCommand: async (argv) => { controlArgv.push(argv); return { ok: true }; }, logInfo: (message) => logs.push(message), spawnDetached: async (_command, args) => { spawnedArgs.push(args); }, }), ['--input-ipc-server', '\\\\.\\pipe\\warm-subminer'], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '', 'normal', undefined, { socketPath: '\\\\.\\pipe\\ignored-config-socket', binaryPath: '', backend: 'windows', logLevel: 'debug', logRotation: 7, autoStart: true, autoStartVisibleOverlay: true, autoStartPauseUntilReady: true, osdMessages: false, texthookerEnabled: true, }, ); assert.equal(result.ok, true); const scriptOpts = spawnedArgs[0]?.find((arg) => arg.startsWith('--script-opts=')); assert.match(scriptOpts ?? '', /subminer-auto_start=no/); assert.match(scriptOpts ?? '', /subminer-socket_path=\\\\\.\\pipe\\warm-subminer/); assert.deepEqual(waitedSockets, [{ socketPath: '\\\\.\\pipe\\warm-subminer', timeoutMs: 10000 }]); assert.deepEqual(controlArgv, [ [ '--start', '--managed-playback', '--log-level', 'debug', '--backend', 'windows', '--socket', '\\\\.\\pipe\\warm-subminer', '--show-visible-overlay', '--texthooker', ], ]); assert.ok(logs.some((line) => line.includes('attachRunningApp=yes'))); assert.ok(logs.some((line) => line.includes('Attached launched mpv session'))); }); test('launchWindowsMpv leaves plugin auto-start enabled when no running app control socket exists', async () => { const spawnedArgs: string[][] = []; let controlCalls = 0; let waitCalls = 0; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', isAppControlServerAvailable: async () => false, waitForSocketReady: async () => { waitCalls += 1; return true; }, sendAppControlCommand: async () => { controlCalls += 1; return { ok: true }; }, spawnDetached: async (_command, args) => { spawnedArgs.push(args); }, }), [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '', 'normal', undefined, { socketPath: '\\\\.\\pipe\\subminer-socket', binaryPath: '', backend: 'windows', autoStart: true, autoStartVisibleOverlay: true, autoStartPauseUntilReady: true, osdMessages: false, texthookerEnabled: false, }, ); assert.equal(result.ok, true); const scriptOpts = spawnedArgs[0]?.find((arg) => arg.startsWith('--script-opts=')); assert.match(scriptOpts ?? '', /subminer-auto_start=yes/); assert.equal(waitCalls, 0); assert.equal(controlCalls, 0); }); test('launchWindowsMpv reports missing mpv path', async () => { const errors: string[] = []; const result = await launchWindowsMpv( [], createDeps({ showError: (_title, content) => errors.push(content), }), ); assert.equal(result.ok, false); assert.equal(result.mpvPath, ''); assert.match(errors[0] ?? '', /mpv\.executablePath/i); }); test('launchWindowsMpv spawns detached mpv with targets', async () => { const calls: string[] = []; const logs: string[] = []; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', logInfo: (message) => logs.push(message), spawnDetached: async (command, args) => { calls.push(command); calls.push(args.join('|')); }, }), [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', ); assert.equal(result.ok, true); assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe'); assert.deepEqual(calls, [ 'C:\\mpv\\mpv.exe', '--player-operation-mode=pseudo-gui|--force-window=immediate|--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua|--input-ipc-server=\\\\.\\pipe\\subminer-socket|--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us|--sub-auto=fuzzy|--sub-file-paths=subs;subtitles|--sid=auto|--secondary-sid=auto|--sub-visibility=no|--secondary-sub-visibility=no|--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner.exe,subminer-socket_path=\\\\.\\pipe\\subminer-socket|C:\\video.mkv', ]); assert.match(logs[0] ?? '', /mpvPath=C:\\mpv\\mpv\.exe/); assert.match(logs[0] ?? '', /inputIpcServer=\\\\\.\\pipe\\subminer-socket/); assert.match( logs[0] ?? '', /bundledPlugin=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main\.lua/, ); assert.match(logs[0] ?? '', /installedPlugin=none/); }); test('launchWindowsMpv forwards runtime logging config to mpv and plugin', async () => { const calls: string[] = []; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', spawnDetached: async (command, args, env) => { calls.push(command); calls.push(args.join('|')); calls.push(env?.SUBMINER_LOG_LEVEL ?? ''); calls.push(env?.SUBMINER_LOG_ROTATION ?? ''); }, }), ['--log-file=C:\\Users\\tester\\AppData\\Roaming\\SubMiner\\logs\\mpv-2026-05-26.log'], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '', 'normal', undefined, { socketPath: '\\\\.\\pipe\\subminer-socket', binaryPath: '', backend: 'windows', logLevel: 'debug', logRotation: 0, autoStart: true, autoStartVisibleOverlay: false, autoStartPauseUntilReady: true, osdMessages: false, texthookerEnabled: false, }, ); assert.equal(result.ok, true); assert.match(calls[1] ?? '', /--msg-level=all=warn,subminer=debug/); assert.doesNotMatch(calls[1] ?? '', /subminer-log_level=debug/); assert.match( calls[1] ?? '', /--log-file=C:\\Users\\tester\\AppData\\Roaming\\SubMiner\\logs\\mpv-2026-05-26\.log/, ); assert.equal(calls[2], 'debug'); assert.equal(calls[3], '0'); }); test('launchWindowsMpv skips bundled script when installed plugin is detected', async () => { const calls: string[] = []; const notifications: string[] = []; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', spawnDetached: async (command, args) => { calls.push(command); calls.push(args.join('|')); }, }), [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '', 'normal', { detectInstalledMpvPlugin: () => ({ installed: true, path: 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua', version: null, source: 'default-config', message: null, }), notifyInstalledPluginDetected: (detection) => { notifications.push(detection.path ?? ''); }, }, ); assert.equal(result.ok, true); assert.equal(calls[0], 'C:\\mpv\\mpv.exe'); assert.doesNotMatch(calls[1] ?? '', /--script=C:\\Program Files\\SubMiner/); assert.match(calls[1] ?? '', /--script-opts=subminer-binary_path=C:\\SubMiner\\SubMiner\.exe/); assert.deepEqual(notifications, [ 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua', ]); }); test('launchWindowsMpv prompts before launch and injects bundled script after legacy plugin removal', async () => { const calls: string[] = []; const prompts: string[] = []; let detectCalls = 0; const result = await launchWindowsMpv( ['C:\\video.mkv'], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', spawnDetached: async (command, args) => { calls.push(command); calls.push(args.join('|')); }, }), [], 'C:\\SubMiner\\SubMiner.exe', 'C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main.lua', '', 'normal', { detectInstalledMpvPlugin: () => { detectCalls += 1; return detectCalls === 1 ? { installed: true, path: 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua', version: '0.12.0', source: 'default-config', message: null, } : { installed: false, path: null, version: null, source: null, message: null, }; }, resolveInstalledPluginBeforeLaunch: async (detection) => { prompts.push(detection.path ?? ''); return 'removed' as const; }, }, ); assert.equal(result.ok, true); assert.equal(detectCalls, 2); assert.deepEqual(prompts, [ 'C:\\Users\\tester\\AppData\\Roaming\\mpv\\scripts\\subminer\\main.lua', ]); assert.equal(calls[0], 'C:\\mpv\\mpv.exe'); assert.match( calls[1] ?? '', /--script=C:\\Program Files\\SubMiner\\resources\\plugin\\subminer\\main\.lua/, ); }); test('launchWindowsMpv reports spawn failures with path context', async () => { const errors: string[] = []; const result = await launchWindowsMpv( [], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', spawnDetached: async () => { throw new Error('spawn failed'); }, showError: (_title, content) => errors.push(content), }), ); assert.equal(result.ok, false); assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe'); assert.match(errors[0] ?? '', /Failed to launch mpv/i); assert.match(errors[0] ?? '', /C:\\mpv\\mpv\.exe/i); }); test('launchWindowsMpv reports async spawn failures with path context', async () => { const errors: string[] = []; const result = await launchWindowsMpv( [], createDeps({ getEnv: (name) => (name === 'SUBMINER_MPV_PATH' ? 'C:\\mpv\\mpv.exe' : undefined), fileExists: (candidate) => candidate === 'C:\\mpv\\mpv.exe', spawnDetached: () => Promise.reject(new Error('async spawn failed')), showError: (_title, content) => errors.push(content), }), ); assert.equal(result.ok, false); assert.equal(result.mpvPath, 'C:\\mpv\\mpv.exe'); assert.match(errors[0] ?? '', /Failed to launch mpv/i); assert.match(errors[0] ?? '', /async spawn failed/i); });