Files
SubMiner/src/main/runtime/posix-launcher-bootstrap.test.ts
T

67 lines
3.7 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { posixLauncherBootstrapContent, shellQuote } from './posix-launcher-bootstrap';
test('downloaded launcher prepares once, survives unmount, and refreshes after app replacement', (t) => {
if (process.platform !== 'linux') return;
const root = fs.mkdtempSync(path.join(os.tmpdir(), "subminer bootstrap's "));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const appPath = path.join(root, 'SubMiner.AppImage');
const resources = path.join(root, 'mount', 'resources');
fs.mkdirSync(path.join(resources, 'launcher'), { recursive: true });
fs.mkdirSync(path.join(resources, 'bun', 'licenses'), { recursive: true });
fs.symlinkSync(process.execPath, path.join(resources, 'bun', 'bun'));
fs.writeFileSync(path.join(resources, 'bun', 'licenses', 'notice'), 'Bun notice');
fs.writeFileSync(path.join(resources, 'launcher', 'version'), '1.0.0\n');
fs.writeFileSync(
path.join(resources, 'launcher', 'prepare.cjs'),
`exports.prepareLauncherRuntime = require(${JSON.stringify(path.join(__dirname, 'prepare-launcher-runtime.ts'))}).prepareLauncherRuntime;`,
);
const script = (version: number) =>
`console.log(JSON.stringify({version:${version},args:process.argv.slice(2)}));`;
fs.writeFileSync(path.join(resources, 'launcher', 'subminer.js'), script(1));
const prepares = path.join(root, 'preparations');
const appContent = `#!/bin/sh\necho prepared >> ${shellQuote(prepares)}\nexport APPDIR=${shellQuote(path.dirname(resources))}\nexec ${shellQuote(process.execPath)} "$@"\n`;
fs.writeFileSync(appPath, appContent, { mode: 0o755 });
const wrapper = path.join(root, 'subminer');
fs.writeFileSync(wrapper, posixLauncherBootstrapContent(), { mode: 0o755 });
const env = { HOME: root, PATH: '', SUBMINER_BINARY_PATH: appPath };
const args = ['space here', "a'b", 'a&b', '$(touch nope)', '日本語'];
const run = (extraEnv = {}) =>
spawnSync(wrapper, args, { env: { ...env, ...extraEnv }, encoding: 'utf8' });
const first = run();
assert.equal(first.status, 0, first.stderr);
assert.deepEqual(JSON.parse(first.stdout), { version: 1, args });
const cache = path.join(root, '.local', 'share', 'SubMiner', 'launcher');
assert.equal(fs.readFileSync(path.join(cache, 'licenses', 'notice'), 'utf8'), 'Bun notice');
fs.renameSync(resources, `${resources}.unmounted`);
const warm = run({ SUBMINER_BINARY_PATH: '' }); // Finds the app recorded by preparation.
assert.equal(warm.status, 0, warm.stderr);
assert.deepEqual(JSON.parse(warm.stdout), { version: 1, args });
assert.equal(fs.readFileSync(prepares, 'utf8'), 'prepared\n');
fs.renameSync(`${resources}.unmounted`, resources);
// Same version and path, new inode: manual replacement must still refresh.
fs.writeFileSync(`${appPath}.new`, appContent, { mode: 0o755 });
fs.renameSync(`${appPath}.new`, appPath);
fs.writeFileSync(path.join(resources, 'launcher', 'subminer.js'), script(2));
const updated = run();
assert.equal(updated.status, 0, updated.stderr);
assert.deepEqual(JSON.parse(updated.stdout), { version: 2, args });
assert.equal(fs.readFileSync(prepares, 'utf8'), 'prepared\nprepared\n');
fs.unlinkSync(path.join(cache, 'bun'));
assert.equal(run().status, 0);
assert.equal(fs.readFileSync(prepares, 'utf8'), 'prepared\nprepared\nprepared\n');
fs.writeFileSync(appPath, '#!/bin/sh\nexit 42\n', { mode: 0o755 });
const failed = run();
assert.equal(failed.status, 42);
assert.equal(failed.stdout, ''); // Never silently execute stale CLI after failed preparation.
});