diff --git a/package.json b/package.json index d05f3d7..ea15731 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "make:dynamic": "npm run build:fortran:dynamic && electron-forge make", "make:flatpak": "npm run build:fortran && FORGE_FLATPAK=1 electron-forge make --targets=@electron-forge/maker-flatpak", "build:wsl2": "bash packaging/wsl2/build-wsl2.sh", - "test:wsl-runtime": "node tests/wsl-runtime.test.js", + "test:wsl-runtime": "node tests/wsl-runtime.test.js && node tests/runtime-gate.test.js", "checksums:md5": "sh -c 'for d in out/make/deb/x64 out/make/rpm/x64 out/make/flatpak/x86_64; do [ -d \"$d\" ] || continue; for f in \"$d\"/*; do [ -f \"$f\" ] || continue; case \"$f\" in *.deb|*.rpm|*.flatpak) base=$(basename \"$f\"); (cd \"$d\" && md5sum \"$base\" > \"$base.md5\");; esac; done; done'", "checksums:sha256": "sh -c 'for d in out/make/deb/x64 out/make/rpm/x64 out/make/flatpak/x86_64; do [ -d \"$d\" ] || continue; for f in \"$d\"/*; do [ -f \"$f\" ] || continue; case \"$f\" in *.deb|*.rpm|*.flatpak) base=$(basename \"$f\"); (cd \"$d\" && sha256sum \"$base\" > \"$base.sha256\");; esac; done; done'", "checksums": "npm run checksums:md5 && npm run checksums:sha256" diff --git a/packaging/wsl2/README.md b/packaging/wsl2/README.md index 2141b31..71175d1 100644 --- a/packaging/wsl2/README.md +++ b/packaging/wsl2/README.md @@ -77,13 +77,38 @@ lets the renderer detect the bundled runtime and route through WSL. ## Tests ```sh -npm run test:wsl-runtime # tests/wsl-runtime.test.js +npm run test:wsl-runtime # tests/wsl-runtime.test.js + tests/runtime-gate.test.js ``` -Covers the pure main-process module (with an injected `wsl.exe` spawner) and the -renderer-side launch translation, including inertness on Linux. +Covers the pure main-process module (with an injected `wsl.exe` spawner), the +renderer-side launch translation, including inertness on Linux, and that the +scan pipeline awaits the runtime gate before launching the engine. -## Open items +## Manual remote-session smoke test + +The engine path is a console process and never touches the display, so it cannot +misroute a window the way the file dialogs could. It is still worth checking once +on a real Windows host, and especially over SSH/RDP, since that is how the +workbench is often driven remotely. After the first-run import, verify: + +1. The import succeeded and the engine runs in the distribution: + + ```powershell + wsl -d trimspNL-runtime -- /opt/trimsp-runtime/bin/trimspNL + ``` + + Expected: the `Usage: trimspNL [ranlux|pcg32|xoshiro]` banner. + +2. Over a remote session (SSH/RDP, not just a local console), launch the app, set + a Work Folder on a Windows drive, and click Run. Expected: `_E*_.rge` + and `_Seq_Results.dat` appear in that folder, proving both the path + translation and the `wsl.exe` spawn work without a local display. + +3. The status bar first shows "Preparing the bundled engine runtime (first run + only)..." and the run then proceeds, proving the runtime gate waited for the + import rather than racing it. + +### Open items - The rootfs is built from `almalinux:8`. Adjust `BASE_IMAGE` if the engine is rebuilt against a different baseline. diff --git a/tests/runtime-gate.test.js b/tests/runtime-gate.test.js new file mode 100644 index 0000000..6e6d984 --- /dev/null +++ b/tests/runtime-gate.test.js @@ -0,0 +1,103 @@ +// Checks the shared scan pipeline is wired to the shell's runtime gate. +// +// On Windows the bundled WSL2 engine is imported on first run, so a Run must +// wait for the gate before launching the engine. This test guards two things +// without needing a Windows host: +// 1. the behaviour of ensureRuntimeGateReady() against stub gates; +// 2. that startSequence() actually awaits it, so a future refactor cannot +// silently drop the gate and race the import. +// +// Run with: node tests/runtime-gate.test.js + +const fs = require('fs'); +const path = require('path'); + +const assert = require('assert'); + +const libPath = path.join(__dirname, '..', 'TrimSPlib.js'); +const src = fs.readFileSync(libPath, 'utf8'); + +function grab(re) { + const m = src.match(re); + if (!m) { + throw new Error('pattern not found in ' + libPath + ': ' + re); + } + return m[0]; +} + +// Lift the real ensureRuntimeGateReady() so its shipped code is exercised. +const gateSource = grab(/async function ensureRuntimeGateReady\s*\([^)]*\)\s*\{[\s\S]*?\n\}/m); + +let statusCalls = []; +let stubWindow = {}; + +const factory = new Function('window', 'updateStatusBar', + gateSource + '\nreturn ensureRuntimeGateReady;'); + +function makeGateFn() { + statusCalls = []; + return factory(stubWindow, function (payload) { statusCalls.push(payload); }); +} + +let passed = 0; +let failed = 0; +function check(name, ok, detail) { + if (ok) { passed++; } else { failed++; } + console.log((ok ? 'PASS ' : 'FAIL ') + name.padEnd(46) + (detail || '')); +} + +async function main() { + // 1. No gate installed (web/standalone) -> proceed, no status write. + stubWindow = {}; + let gate = makeGateFn(); + let result = await gate('Run'); + check('no gate: proceeds', result === true && statusCalls.length === 0); + + // 2. Gate resolves true -> proceed. + stubWindow = { TrimSPRuntimeGate: { ready: () => Promise.resolve(true) } }; + gate = makeGateFn(); + result = await gate('Run'); + check('gate ready: proceeds', result === true); + + // 3. Gate resolves false -> blocked and message surfaced. + stubWindow = { + TrimSPRuntimeGate: { + ready: () => Promise.resolve(false), + message: () => 'Check that WSL2 is installed, then try again.' + } + }; + gate = makeGateFn(); + result = await gate('Run'); + check('gate not ready: blocked', result === false); + check('gate not ready: status shown', + statusCalls.length === 1 + && /runtime is not ready/.test(statusCalls[0].run) + && /WSL2 is installed/.test(statusCalls[0].run), + statusCalls.length ? '"' + statusCalls[0].run + '"' : 'no status'); + + // 4. Gate that throws -> treated as not ready, not an unhandled rejection. + stubWindow = { + TrimSPRuntimeGate: { + ready: () => Promise.reject(new Error('boom')), + message: () => 'failed' + } + }; + gate = makeGateFn(); + result = await gate('Run'); + check('gate throwing: blocked', result === false && statusCalls.length === 1); + + // 5. startSequence must await the gate before running the engine. + const startSequence = grab(/async function startSequence\s*\([^)]*\)\s*\{[\s\S]*?\n\}/m); + const guardIndex = startSequence.indexOf('ensureRuntimeGateReady'); + const launchIndex = startSequence.indexOf('runSimulationSequence'); + check('startSequence awaits gate', + guardIndex !== -1, + guardIndex !== -1 ? '' : 'no ensureRuntimeGateReady() call'); + check('gate checked before launch', + guardIndex !== -1 && launchIndex !== -1 && guardIndex < launchIndex); + + console.log('\n' + passed + ' passed, ' + failed + ' failed'); + process.exit(failed ? 1 : 0); +} + +main();