mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-10 17:16:20 -07:00
308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import test from 'node:test';
|
|
import {
|
|
detectBun,
|
|
installBun,
|
|
installLauncher,
|
|
refreshManagedCommandLineLauncher,
|
|
} from './command-line-launcher';
|
|
import {
|
|
cleanupOldWindowsManagedRuntimes,
|
|
MANAGED_LAUNCHER_MARKER,
|
|
managedLauncherContent,
|
|
managedLauncherPaths,
|
|
stageManagedLauncher,
|
|
windowsManagedRuntimePaths,
|
|
} from './managed-launcher';
|
|
|
|
function workspace(t: test.TestContext) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "subminer bundled bun's "));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
return root;
|
|
}
|
|
|
|
test('a missing packaged Bun never falls back to system Bun or runs an installer', async () => {
|
|
let calls = 0;
|
|
const options = {
|
|
bundledBunPath: '/missing/private/bun',
|
|
env: { PATH: path.dirname(process.execPath) },
|
|
existsSync: (candidate: string) => candidate === process.execPath,
|
|
runCommand: async () => {
|
|
calls += 1;
|
|
return { exitCode: 0, stdout: '1.3.5', stderr: '' };
|
|
},
|
|
};
|
|
for (const snapshot of [await detectBun(options), await installBun(options)]) {
|
|
assert.equal(snapshot.status, 'missing');
|
|
assert.equal(snapshot.installCommand, null);
|
|
assert.match(snapshot.message ?? '', /Reinstall SubMiner/);
|
|
}
|
|
assert.equal(calls, 0);
|
|
});
|
|
|
|
test('packaged POSIX launcher works without Bun on PATH and survives AppImage unmount', async (t) => {
|
|
if (process.platform !== 'linux') return;
|
|
const root = workspace(t);
|
|
const resources = path.join(root, 'mounted resources');
|
|
const bin = path.join(root, 'home', '.local', 'bin');
|
|
fs.mkdirSync(resources, { recursive: true });
|
|
fs.mkdirSync(bin, { recursive: true });
|
|
const launcherResourcePath = path.join(resources, 'subminer');
|
|
const bundledBunPath = path.join(resources, 'bun');
|
|
fs.symlinkSync(process.execPath, bundledBunPath);
|
|
fs.writeFileSync(
|
|
launcherResourcePath,
|
|
'console.log(JSON.stringify({args:process.argv.slice(2),app:process.env.SUBMINER_BINARY_PATH,managed:process.env.SUBMINER_MANAGED_LAUNCHER}));',
|
|
);
|
|
const appPath = path.join(root, 'SubMiner.AppImage');
|
|
fs.writeFileSync(appPath, '#!/bin/sh\nexit 73\n', { mode: 0o755 });
|
|
const options = {
|
|
platform: process.platform,
|
|
homeDir: path.join(root, 'home'),
|
|
env: {
|
|
HOME: path.join(root, 'home'),
|
|
PATH: bin,
|
|
XDG_DATA_HOME: path.join(root, 'data'),
|
|
APPIMAGE: appPath,
|
|
},
|
|
appExePath: path.join(resources, 'SubMiner'),
|
|
appVersion: '1.0.0',
|
|
bundledBunPath,
|
|
launcherResourcePath,
|
|
};
|
|
const installed = await installLauncher(options);
|
|
assert.equal(installed.status, 'ready', installed.message ?? 'install failed');
|
|
assert.match(fs.readFileSync(path.join(bin, 'subminer'), 'utf8'), /SubMiner managed launcher/);
|
|
fs.renameSync(resources, path.join(root, 'unmounted'));
|
|
const args = ['file with spaces.mkv', "single'quote", '$HOME', '$(touch nope)', '日本語'];
|
|
const result = spawnSync(path.join(bin, 'subminer'), args, {
|
|
env: options.env,
|
|
encoding: 'utf8',
|
|
timeout: 15000,
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(JSON.parse(result.stdout), { args, app: appPath, managed: '1' });
|
|
});
|
|
|
|
test('setup can install into a new user bin despite an empty GUI PATH', async (t) => {
|
|
if (process.platform === 'win32') return;
|
|
const root = workspace(t);
|
|
const appPath = path.join(root, 'SubMiner.app', 'Contents', 'MacOS', 'SubMiner');
|
|
const resources = path.join(root, 'SubMiner.app', 'Contents', 'Resources');
|
|
fs.mkdirSync(path.dirname(appPath), { recursive: true });
|
|
fs.mkdirSync(path.join(resources, 'bun'), { recursive: true });
|
|
fs.mkdirSync(path.join(resources, 'launcher'), { recursive: true });
|
|
fs.writeFileSync(appPath, '#!/bin/sh\nexit 73\n', { mode: 0o755 });
|
|
fs.symlinkSync(process.execPath, path.join(resources, 'bun', 'bun'));
|
|
const script = path.join(resources, 'launcher', 'subminer.js');
|
|
fs.writeFileSync(script, 'console.log("help");');
|
|
const snapshot = await installLauncher({
|
|
platform: 'darwin',
|
|
homeDir: root,
|
|
env: { PATH: '' },
|
|
appExePath: appPath,
|
|
bundledBunPath: process.execPath,
|
|
launcherResourcePath: script,
|
|
});
|
|
assert.equal(snapshot.status, 'not_on_path', snapshot.message ?? 'install failed');
|
|
assert.equal(snapshot.installPath, path.join(root, '.local', 'bin', 'subminer'));
|
|
assert.match(snapshot.message ?? '', /export PATH=/);
|
|
const result = spawnSync(snapshot.installPath!, ['--help'], {
|
|
env: { PATH: '' },
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
|
|
test('app upgrades refresh payloads, migrate legacy Bun launchers, and preserve custom scripts', async (t) => {
|
|
if (process.platform !== 'linux') return;
|
|
const root = workspace(t);
|
|
const bin = path.join(root, 'bin');
|
|
fs.mkdirSync(bin, { recursive: true });
|
|
const script = path.join(root, 'resource');
|
|
fs.writeFileSync(script, 'console.log("old");');
|
|
const appPath = path.join(root, 'SubMiner.AppImage');
|
|
fs.writeFileSync(appPath, '#!/bin/sh\nexit 73\n', { mode: 0o755 });
|
|
const options = {
|
|
platform: process.platform,
|
|
homeDir: root,
|
|
env: { HOME: root, PATH: bin },
|
|
appVersion: '1',
|
|
appExePath: appPath,
|
|
bundledBunPath: process.execPath,
|
|
launcherResourcePath: script,
|
|
};
|
|
assert.equal((await installLauncher(options)).status, 'ready');
|
|
fs.writeFileSync(script, 'console.log("new");');
|
|
await refreshManagedCommandLineLauncher({
|
|
...options,
|
|
env: { HOME: root, PATH: '' },
|
|
appVersion: '2',
|
|
});
|
|
const payload = managedLauncherPaths(options);
|
|
assert.equal(fs.readFileSync(payload.scriptPath, 'utf8'), 'console.log("new");');
|
|
fs.writeFileSync(path.join(bin, 'subminer'), '#!/usr/bin/env bun\n// SubMiner launcher\n');
|
|
await refreshManagedCommandLineLauncher({ ...options, appVersion: '3' });
|
|
assert.match(fs.readFileSync(path.join(bin, 'subminer'), 'utf8'), /SubMiner managed launcher/);
|
|
fs.writeFileSync(path.join(bin, 'subminer'), '#!/bin/sh\necho standalone\n');
|
|
await refreshManagedCommandLineLauncher({ ...options, appVersion: '4' });
|
|
assert.equal(fs.readFileSync(payload.versionPath, 'utf8'), '3');
|
|
});
|
|
|
|
test('Windows wrapper discovers the configured app and its versioned private runtime', () => {
|
|
const content = managedLauncherContent({
|
|
platform: 'win32',
|
|
appPath: 'C:\\Apps 100% !\\SubMiner.exe',
|
|
});
|
|
assert.ok(content.includes(MANAGED_LAUNCHER_MARKER));
|
|
assert.ok(content.includes('setlocal DisableDelayedExpansion'));
|
|
assert.ok(content.includes('set "SUBMINER_BINARY_PATH=C:\\Apps 100%% !\\SubMiner.exe"'));
|
|
assert.ok(content.includes('%SUBMINER_RESOURCES_PATH%\\launcher\\version'));
|
|
assert.ok(
|
|
content.includes(
|
|
'set "SUBMINER_BUN_PATH=%LOCALAPPDATA%\\SubMiner\\launcher-runtime\\%SUBMINER_APP_VERSION%\\bun.exe"',
|
|
),
|
|
);
|
|
assert.ok(
|
|
content.includes('"%SUBMINER_BUN_PATH%" "%SUBMINER_RESOURCES_PATH%\\launcher\\subminer.js" %*'),
|
|
);
|
|
assert.ok(content.includes('exit /b %errorlevel%'));
|
|
});
|
|
|
|
test('Windows managed runtime path is absolute, versioned, and injectable', () => {
|
|
const paths = windowsManagedRuntimePaths({
|
|
platform: 'win32',
|
|
localAppData: 'D:\\Profiles\\テスト User\\AppData\\Local',
|
|
appVersion: '1.2.3-beta.4',
|
|
});
|
|
assert.equal(
|
|
paths.bunPath,
|
|
'D:\\Profiles\\テスト User\\AppData\\Local\\SubMiner\\launcher-runtime\\1.2.3-beta.4\\bun.exe',
|
|
);
|
|
assert.ok(path.win32.isAbsolute(paths.bunPath));
|
|
assert.throws(
|
|
() =>
|
|
windowsManagedRuntimePaths({
|
|
platform: 'win32',
|
|
localAppData: 'relative',
|
|
appVersion: '1.2.3',
|
|
}),
|
|
/must be an absolute Windows path/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
windowsManagedRuntimePaths({
|
|
platform: 'win32',
|
|
localAppData: 'C:\\Users\\tester\\AppData\\Local',
|
|
appVersion: '1:2',
|
|
}),
|
|
/not a valid directory name/,
|
|
);
|
|
});
|
|
|
|
test('Windows managed launcher forwards arguments without a system Bun', async (t) => {
|
|
if (process.platform !== 'win32') return;
|
|
const root = workspace(t);
|
|
const appDirectory = path.join(root, 'Installed App');
|
|
const appPath = path.join(appDirectory, 'SubMiner.exe');
|
|
const launcherDirectory = path.join(appDirectory, 'resources', 'launcher');
|
|
const script = path.join(launcherDirectory, 'subminer.js');
|
|
fs.mkdirSync(launcherDirectory, { recursive: true });
|
|
fs.copyFileSync(process.execPath, appPath);
|
|
fs.writeFileSync(script, 'console.log(JSON.stringify(process.argv.slice(2)));');
|
|
fs.writeFileSync(path.join(launcherDirectory, 'version'), '1.0.0');
|
|
const options = {
|
|
platform: process.platform,
|
|
env: { ...process.env, PATH: '', LOCALAPPDATA: root },
|
|
bundledBunPath: process.execPath,
|
|
launcherResourcePath: script,
|
|
appExePath: appPath,
|
|
appVersion: '1.0.0',
|
|
localAppData: root,
|
|
getUserPath: () => '',
|
|
setUserPath: () => {},
|
|
broadcastEnvironmentChange: () => {},
|
|
};
|
|
const snapshot = await installLauncher(options);
|
|
assert.equal(snapshot.status, 'ready', snapshot.message ?? 'install failed');
|
|
const { getRunCommand } = await import('./command-line-launcher-deps');
|
|
const args = ['spaces here', 'a&b', 'bang!z', '日本語'];
|
|
const result = await getRunCommand({})(snapshot.installPath!, args, { env: options.env });
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
assert.deepEqual(JSON.parse(result.stdout), args);
|
|
});
|
|
|
|
test('Windows stages a new runtime version while the prior Bun executable is running', async (t) => {
|
|
if (process.platform !== 'win32') return;
|
|
const root = workspace(t);
|
|
const bundledDirectory = path.join(root, 'packaged', 'bun');
|
|
const bundledBunPath = path.join(bundledDirectory, 'bun.exe');
|
|
const launcherResourcePath = path.join(root, 'packaged', 'launcher', 'subminer');
|
|
fs.mkdirSync(path.join(bundledDirectory, 'licenses'), { recursive: true });
|
|
fs.mkdirSync(path.dirname(launcherResourcePath), { recursive: true });
|
|
fs.copyFileSync(process.execPath, bundledBunPath);
|
|
fs.writeFileSync(path.join(bundledDirectory, 'licenses', 'Bun-LICENSE.md'), 'license');
|
|
fs.writeFileSync(launcherResourcePath, 'console.log("launcher");');
|
|
|
|
const first = stageManagedLauncher({
|
|
platform: 'win32',
|
|
localAppData: root,
|
|
appVersion: '1.0.0',
|
|
bundledBunPath,
|
|
launcherResourcePath,
|
|
});
|
|
const running = spawn(first.bunPath, ['-e', 'setInterval(() => {}, 1000)']);
|
|
await new Promise<void>((resolve, reject) => {
|
|
running.once('spawn', resolve);
|
|
running.once('error', reject);
|
|
});
|
|
|
|
const expectedSecond = windowsManagedRuntimePaths({
|
|
platform: 'win32',
|
|
localAppData: root,
|
|
appVersion: '2.0.0',
|
|
});
|
|
try {
|
|
const second = stageManagedLauncher({
|
|
platform: 'win32',
|
|
localAppData: root,
|
|
appVersion: '2.0.0',
|
|
bundledBunPath,
|
|
launcherResourcePath,
|
|
});
|
|
assert.notEqual(second.bunPath, first.bunPath);
|
|
assert.ok(fs.existsSync(second.bunPath));
|
|
cleanupOldWindowsManagedRuntimes({
|
|
platform: 'win32',
|
|
localAppData: root,
|
|
appVersion: '2.0.0',
|
|
});
|
|
assert.ok(fs.existsSync(first.bunPath));
|
|
assert.ok(fs.existsSync(path.join(path.dirname(first.bunPath), 'licenses', 'Bun-LICENSE.md')));
|
|
assert.equal(
|
|
fs.readFileSync(
|
|
path.join(path.dirname(second.bunPath), 'licenses', 'Bun-LICENSE.md'),
|
|
'utf8',
|
|
),
|
|
'license',
|
|
);
|
|
} finally {
|
|
if (running.exitCode === null) {
|
|
const exited = new Promise<void>((resolve) => running.once('exit', () => resolve()));
|
|
running.kill();
|
|
await exited;
|
|
}
|
|
}
|
|
cleanupOldWindowsManagedRuntimes({
|
|
platform: 'win32',
|
|
localAppData: root,
|
|
appVersion: '2.0.0',
|
|
});
|
|
assert.equal(fs.existsSync(path.dirname(first.bunPath)), false);
|
|
assert.ok(fs.existsSync(expectedSecond.bunPath));
|
|
});
|