import test from 'node:test'; import assert from 'node:assert/strict'; import path from 'node:path'; import { detectBun, detectLauncher, installLauncher, resolveBunInstallCommand, resolveLauncherInstallTarget, type BunSnapshot, } from './command-line-launcher'; function createBunSnapshot(status: BunSnapshot['status']): BunSnapshot { return { status, commandPath: status === 'ready' ? '/bin/bun' : null, version: status === 'ready' ? '1.3.0' : null, installMethod: null, installCommand: null, message: null, }; } test('detectBun reports ready when bun --version succeeds on PATH', async () => { const snapshot = await detectBun({ platform: 'linux', env: { PATH: '/usr/local/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/usr/local/bin/bun', accessSync: (candidate) => { if (candidate !== '/usr/local/bin/bun') throw new Error('not executable'); }, runCommand: async (command, args) => { assert.equal(command, '/usr/local/bin/bun'); assert.deepEqual(args, ['--version']); return { exitCode: 0, stdout: '1.3.5\n', stderr: '' }; }, }); assert.deepEqual(snapshot, { status: 'ready', commandPath: '/usr/local/bin/bun', version: '1.3.5', installMethod: null, installCommand: null, message: null, }); }); test('detectBun reports missing with an install command when bun is absent', async () => { const snapshot = await detectBun({ platform: 'linux', env: { PATH: '/usr/bin' }, existsSync: () => false, accessSync: () => { throw new Error('missing'); }, runCommand: async () => ({ exitCode: 127, stdout: '', stderr: 'missing' }), }); assert.equal(snapshot.status, 'missing'); assert.equal(snapshot.commandPath, null); assert.deepEqual(snapshot.installCommand, [ 'bash', '-lc', 'curl -fsSL https://bun.com/install | bash', ]); }); test('resolveBunInstallCommand prefers winget on Windows', () => { assert.deepEqual( resolveBunInstallCommand({ platform: 'win32', env: { PATH: 'C:\\Tools' }, existsSync: (candidate) => candidate === 'C:\\Tools\\winget.exe', }), [ 'C:\\Tools\\winget.exe', 'install', '--id', 'Oven-sh.Bun', '--exact', '--accept-package-agreements', '--accept-source-agreements', ], ); }); test('resolveBunInstallCommand falls back to scoop on Windows before official installer', () => { assert.deepEqual( resolveBunInstallCommand({ platform: 'win32', env: { PATH: 'C:\\Tools' }, existsSync: (candidate) => candidate === 'C:\\Tools\\scoop.cmd', }), ['C:\\Tools\\scoop.cmd', 'install', 'bun'], ); }); test('resolveBunInstallCommand uses Homebrew on macOS when available', () => { assert.deepEqual( resolveBunInstallCommand({ platform: 'darwin', env: { PATH: '/opt/homebrew/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/opt/homebrew/bin/brew', accessSync: (candidate) => { if (candidate !== '/opt/homebrew/bin/brew') throw new Error('not executable'); }, }), ['/opt/homebrew/bin/brew', 'install', 'bun'], ); }); test('detectBun reports homebrew install method from POSIX brew path', async () => { const snapshot = await detectBun({ platform: 'darwin', env: { PATH: '/opt/homebrew/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/opt/homebrew/bin/brew', accessSync: (candidate) => { if (candidate !== '/opt/homebrew/bin/brew') throw new Error('not executable'); }, runCommand: async () => ({ exitCode: 127, stdout: '', stderr: 'missing' }), }); assert.equal(snapshot.status, 'missing'); assert.equal(snapshot.installMethod, 'homebrew'); }); test('resolveLauncherInstallTarget prefers writable user bin on Linux', async () => { const target = await resolveLauncherInstallTarget({ platform: 'linux', homeDir: '/home/tester', env: { PATH: '/usr/bin:/home/tester/.local/bin:/tmp/bin' }, existsSync: (candidate) => candidate === '/usr/bin' || candidate === '/home/tester/.local/bin' || candidate === '/tmp/bin', accessSync: (candidate) => { if (candidate !== '/home/tester/.local/bin') throw new Error('not writable'); }, }); assert.equal(target.status, 'not_installed'); assert.equal(target.pathDir, '/home/tester/.local/bin'); assert.equal(target.installPath, '/home/tester/.local/bin/subminer'); }); test('resolveLauncherInstallTarget returns not_installable without writable PATH dirs', async () => { const target = await resolveLauncherInstallTarget({ platform: 'linux', homeDir: '/home/tester', env: { PATH: '/usr/bin' }, existsSync: (candidate) => candidate === '/usr/bin', accessSync: () => { throw new Error('not writable'); }, }); assert.equal(target.status, 'not_installable'); assert.equal(target.installPath, null); }); test('resolveLauncherInstallTarget skips Homebrew bin for empty macOS manual installs', async () => { const target = await resolveLauncherInstallTarget({ platform: 'darwin', homeDir: '/Users/tester', env: { PATH: '/opt/homebrew/bin:/usr/local/bin:/Users/tester/.local/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/opt/homebrew/bin' || candidate === '/usr/local/bin' || candidate === '/Users/tester/.local/bin' || candidate === '/usr/bin', accessSync: (candidate) => { if ( candidate !== '/opt/homebrew/bin' && candidate !== '/usr/local/bin' && candidate !== '/Users/tester/.local/bin' ) { throw new Error('not writable'); } }, }); assert.equal(target.status, 'not_installed'); assert.equal(target.pathDir, '/Users/tester/.local/bin'); assert.equal(target.installPath, '/Users/tester/.local/bin/subminer'); }); test('resolveLauncherInstallTarget uses usr local bin for macOS manual install when user bin is absent', async () => { const target = await resolveLauncherInstallTarget({ platform: 'darwin', homeDir: '/Users/tester', env: { PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/opt/homebrew/bin' || candidate === '/usr/local/bin' || candidate === '/usr/bin', accessSync: (candidate) => { if (candidate !== '/opt/homebrew/bin' && candidate !== '/usr/local/bin') { throw new Error('not writable'); } }, }); assert.equal(target.status, 'not_installed'); assert.equal(target.pathDir, '/usr/local/bin'); assert.equal(target.installPath, '/usr/local/bin/subminer'); }); test('installLauncher writes Windows cmd shim and appends user PATH once', async () => { const files = new Map(); const dirs = new Set(); const launcherResource = 'C:\\Apps\\SubMiner\\resources\\launcher\\subminer'; files.set(launcherResource, 'launcher'); let userPath = 'C:\\Tools;C:\\Users\\tester\\AppData\\Local\\SubMiner\\bin'; let setPathCalls = 0; const snapshot = await installLauncher({ platform: 'win32', localAppData: 'C:\\Users\\tester\\AppData\\Local', appExePath: 'C:\\Apps\\SubMiner\\SubMiner.exe', launcherResourcePath: launcherResource, env: { PATH: userPath }, existsSync: (candidate) => files.has(candidate) || dirs.has(candidate), mkdirSync: (candidate) => dirs.add(candidate), readFileSync: (candidate) => files.get(candidate) ?? '', writeFileSync: (candidate, content) => files.set(candidate, content), getUserPath: () => userPath, setUserPath: (next) => { setPathCalls += 1; userPath = next; }, broadcastEnvironmentChange: () => undefined, runCommand: async (command, args) => { if (command.endsWith('subminer.cmd') && args[0] === '--help') { return { exitCode: 0, stdout: 'help', stderr: '' }; } return { exitCode: 1, stdout: '', stderr: 'unexpected' }; }, bunSnapshot: createBunSnapshot('ready'), }); const shimPath = 'C:\\Users\\tester\\AppData\\Local\\SubMiner\\bin\\subminer.cmd'; assert.equal(setPathCalls, 0); assert.match( files.get(shimPath) ?? '', /set "SUBMINER_BINARY_PATH=C:\\Apps\\SubMiner\\SubMiner\.exe"/, ); assert.match( files.get(shimPath) ?? '', /bun "C:\\Apps\\SubMiner\\resources\\launcher\\subminer" %\*/, ); assert.equal(snapshot.status, 'ready'); }); test('detectLauncher reports shadowed when another subminer appears earlier on PATH', async () => { const snapshot = await detectLauncher({ platform: 'linux', homeDir: '/home/tester', env: { PATH: '/tmp/bin:/home/tester/.local/bin' }, existsSync: (candidate) => candidate === '/tmp/bin' || candidate === '/home/tester/.local/bin' || candidate === '/tmp/bin/subminer' || candidate === '/home/tester/.local/bin/subminer', accessSync: () => undefined, bunSnapshot: createBunSnapshot('ready'), }); assert.equal(snapshot.status, 'shadowed'); assert.equal(snapshot.shadowedBy, '/tmp/bin/subminer'); assert.equal(snapshot.installPath, '/home/tester/.local/bin/subminer'); }); test('detectLauncher accepts installed macOS launcher from user local bin before Homebrew target', async () => { const snapshot = await detectLauncher({ platform: 'darwin', homeDir: '/Users/tester', env: { PATH: '/Users/tester/.local/bin:/opt/homebrew/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/Users/tester/.local/bin' || candidate === '/opt/homebrew/bin' || candidate === '/Users/tester/.local/bin/subminer', accessSync: () => undefined, runCommand: async (command, args) => { assert.equal(command, '/Users/tester/.local/bin/subminer'); assert.deepEqual(args, ['--help']); return { exitCode: 0, stdout: 'help', stderr: '' }; }, bunSnapshot: createBunSnapshot('ready'), }); assert.equal(snapshot.status, 'ready'); assert.equal(snapshot.commandPath, '/Users/tester/.local/bin/subminer'); assert.equal(snapshot.installPath, '/Users/tester/.local/bin/subminer'); assert.equal(snapshot.pathDir, '/Users/tester/.local/bin'); assert.equal(snapshot.shadowedBy, null); }); test('detectLauncher accepts installed macOS launcher from Homebrew bin', async () => { const snapshot = await detectLauncher({ platform: 'darwin', homeDir: '/Users/tester', env: { PATH: '/opt/homebrew/bin:/usr/bin' }, existsSync: (candidate) => candidate === '/opt/homebrew/bin' || candidate === '/opt/homebrew/bin/subminer', accessSync: () => undefined, runCommand: async (command, args) => { assert.equal(command, '/opt/homebrew/bin/subminer'); assert.deepEqual(args, ['--help']); return { exitCode: 0, stdout: 'help', stderr: '' }; }, bunSnapshot: createBunSnapshot('ready'), }); assert.equal(snapshot.status, 'ready'); assert.equal(snapshot.commandPath, '/opt/homebrew/bin/subminer'); assert.equal(snapshot.installPath, '/opt/homebrew/bin/subminer'); assert.equal(snapshot.pathDir, '/opt/homebrew/bin'); assert.equal(snapshot.shadowedBy, null); }); test('detectLauncher reports installed_bun_missing when launcher exists but bun is missing', async () => { const snapshot = await detectLauncher({ platform: 'linux', homeDir: '/home/tester', env: { PATH: '/home/tester/.local/bin' }, existsSync: (candidate) => candidate === '/home/tester/.local/bin' || candidate === '/home/tester/.local/bin/subminer', accessSync: () => undefined, bunSnapshot: createBunSnapshot('missing'), }); assert.equal(snapshot.status, 'installed_bun_missing'); }); test('detectLauncher treats stale Windows shim as not installed', async () => { const shimPath = 'C:\\Users\\tester\\AppData\\Local\\SubMiner\\bin\\subminer.cmd'; const snapshot = await detectLauncher({ platform: 'win32', localAppData: 'C:\\Users\\tester\\AppData\\Local', appExePath: 'C:\\Apps\\SubMiner\\SubMiner.exe', launcherResourcePath: 'C:\\Apps\\SubMiner\\resources\\launcher\\subminer', env: { PATH: 'C:\\Users\\tester\\AppData\\Local\\SubMiner\\bin' }, existsSync: (candidate) => candidate === shimPath || candidate === 'C:\\Users\\tester\\AppData\\Local\\SubMiner\\bin', readFileSync: () => '@echo off\nset "SUBMINER_BINARY_PATH=C:\\Old\\SubMiner.exe"\nbun "C:\\Old\\launcher\\subminer" %*\n', bunSnapshot: createBunSnapshot('ready'), }); assert.equal(snapshot.status, 'not_installed'); assert.match(snapshot.message ?? '', /previous SubMiner install/); }); test('installLauncher copies packaged launcher and chmods on POSIX', async () => { const files = new Map([['/resources/launcher/subminer', 'launcher']]); const modes = new Map(); const snapshot = await installLauncher({ platform: 'linux', homeDir: '/home/tester', env: { PATH: '/home/tester/.local/bin' }, launcherResourcePath: '/resources/launcher/subminer', existsSync: (candidate) => files.has(candidate) || candidate === '/home/tester/.local/bin', accessSync: () => undefined, copyFileSync: (from, to) => files.set(to, files.get(from) ?? ''), chmodSync: (candidate, mode) => modes.set(candidate, mode), runCommand: async (command, args) => { assert.equal(command, '/home/tester/.local/bin/subminer'); assert.deepEqual(args, ['--help']); return { exitCode: 0, stdout: 'help', stderr: '' }; }, bunSnapshot: createBunSnapshot('ready'), }); assert.equal(files.get('/home/tester/.local/bin/subminer'), 'launcher'); assert.equal(modes.get('/home/tester/.local/bin/subminer'), 0o755); assert.equal(snapshot.status, 'ready'); });