import { test } from "node:test"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { electronExecutableCandidates, electronRuntime, findElectronExecutable, isElectronHost, isNodeFreeLauncher, launcherScript, looksLikeElectronExecutable, resolveRuntime, } from "../out/agent/runtime.js"; import { writeLauncher } from "../out/agent/setup.js"; const WIN = "win32"; const LINUX = "linux"; // ── Pure launcher generation ────────────────────────────────────────── test("electron runtime launcher runs without Node on PATH", () => { const script = launcherScript( { runtime: electronRuntime("C:\\Apps\\VSCode\\Code.exe"), serverPath: "C:\\ext\\dist\\agent\\mcpServer.js", projectDir: "D:\\Mods\\Example", }, WIN, ); assert.ok(script.startsWith("@echo off")); assert.ok(script.includes("set ELECTRON_RUN_AS_NODE=1")); assert.ok(script.includes("C:\\Apps\\VSCode\\Code.exe")); assert.ok(script.includes("C:\\ext\\dist\\agent\\mcpServer.js")); // The Node path exists but must be guarded by the runtime-existence jump. assert.ok(script.includes('if not exist "%RA3_RUNTIME%" goto :ra3_node')); assert.ok(script.includes(":ra3_node")); assert.equal(isNodeFreeLauncher(script, WIN), true); }); test("Windows launcher avoids the parse-time %errorlevel% batch pitfall", () => { const script = launcherScript( { runtime: electronRuntime("C:\\Apps\\VSCode\\Code.exe"), serverPath: "C:\\ext\\mcpServer.js", projectDir: "D:\\P", }, WIN, ); // Inside a parenthesised block %errorlevel% would expand at parse time. assert.ok(!/^if exist .*\(\s*$/m.test(script), "must not use an if (...) block"); assert.equal( (script.match(/exit \/b %errorlevel%/g) ?? []).length, 2, "both Electron and Node endings should propagate the exit code", ); }); test("node runtime launcher is generated when Electron is unavailable", () => { const script = launcherScript( { runtime: { kind: "node", executable: "node", env: {}, viaPath: true }, serverPath: "C:\\ext\\mcpServer.js", projectDir: "D:\\P", }, WIN, ); assert.ok(!script.includes("ELECTRON_RUN_AS_NODE")); assert.ok(script.includes('set "RA3_NODE=node"')); assert.equal(isNodeFreeLauncher(script, WIN), false); }); test("POSIX launcher prefers Electron and falls back to Node", () => { const script = launcherScript( { runtime: electronRuntime("/usr/share/code/code"), serverPath: "/ext/mcpServer.js", projectDir: "/mods/example", }, LINUX, ); assert.ok(script.startsWith("#!/usr/bin/env sh")); assert.ok(script.includes("ELECTRON_RUN_AS_NODE=1 exec")); assert.ok(script.includes('if [ -x "$RA3_RUNTIME" ]')); assert.equal(isNodeFreeLauncher(script, LINUX), true); }); test("launcher quotes paths safely", () => { const win = launcherScript( { runtime: electronRuntime("C:\\Program Files\\VS Code\\Code.exe"), serverPath: "C:\\my ext\\mcpServer.js", projectDir: "D:\\My Mods\\Example", }, WIN, ); assert.ok(win.includes('set "RA3_RUNTIME=C:\\Program Files\\VS Code\\Code.exe"')); assert.ok(win.includes('set "RA3_SERVER=C:\\my ext\\mcpServer.js"')); const sh = launcherScript( { runtime: electronRuntime("/opt/it's here/code"), serverPath: "/tmp/server.js", projectDir: "/tmp/proj", }, LINUX, ); // Single quotes inside a single-quoted POSIX string must be escaped. assert.ok(sh.includes(`'/opt/it'\\''s here/code'`)); }); test("looksLikeElectronExecutable recognises VS Code-family binaries", () => { assert.equal(looksLikeElectronExecutable("C:\\...\\Microsoft VS Code\\Code.exe"), true); assert.equal(looksLikeElectronExecutable("/usr/share/code/code"), true); assert.equal(looksLikeElectronExecutable("/Applications/Visual Studio Code.app/Contents/MacOS/Electron"), true); assert.equal(looksLikeElectronExecutable("C:\\Windows\\System32\\cmd.exe"), false); }); test("runtime resolution falls back to Node outside an Electron host", () => { // Tests run under plain Node, so the resolved runtime must be Node. assert.equal(isElectronHost(), false); const runtime = resolveRuntime(); assert.equal(runtime.kind, "node"); assert.equal(runtime.viaPath, true); assert.deepEqual(runtime.env, {}); }); test("electronRuntime carries the ELECTRON_RUN_AS_NODE env", () => { const runtime = electronRuntime("C:\\Code.exe"); assert.equal(runtime.kind, "electron"); assert.equal(runtime.executable, "C:\\Code.exe"); assert.deepEqual(runtime.env, { ELECTRON_RUN_AS_NODE: "1" }); assert.equal(runtime.viaPath, false); }); test("electronExecutableCandidates returns platform-appropriate probes", () => { const candidates = electronExecutableCandidates(); assert.ok(Array.isArray(candidates)); if (process.platform === "win32") { assert.ok(candidates.every((c) => c.endsWith(".exe"))); } // findElectronExecutable must never throw and returns a string or null. const found = findElectronExecutable(); assert.ok(found === null || typeof found === "string"); }); // ── writeLauncher integration ───────────────────────────────────────── test("writeLauncher writes an executable launcher and reports node-freeness", async () => { const home = mkdtempSync(join(tmpdir(), "ra3-runtime-test-")); try { const result = await writeLauncher("C:\\ext", "D:\\Mods\\Example", home, electronRuntime("C:\\Code.exe")); assert.ok(existsSync(result.path)); assert.equal(result.nodeFree, true); assert.equal(result.runtime.kind, "electron"); const text = readFileSync(result.path, "utf8"); assert.ok(text.includes("ELECTRON_RUN_AS_NODE=1")); assert.ok(text.includes("mcpServer.js")); } finally { rmSync(home, { recursive: true, force: true }); } }); // ── Live launcher execution (skipped when the sandbox forbids spawning) ── const electron = findElectronExecutable(); /** * True when this process may launch the launcher, i.e. spawn a shell. * * Some sandboxes allow spawning `node` directly but deny `cmd.exe`/`sh`, so * the probe must exercise the same capability the test needs instead of just * spawning any child process. */ function canSpawnShell() { try { if (process.platform === "win32") { execFileSync("cmd.exe", ["/d", "/c", "exit 0"], { stdio: "ignore" }); } else { execFileSync("/bin/sh", ["-c", "exit 0"], { stdio: "ignore" }); } return true; } catch { return false; } } const spawnable = canSpawnShell(); test( "generated launcher completes an MCP session on the Electron runtime", { skip: !spawnable || !electron }, (t) => { const home = mkdtempSync(join(tmpdir(), "ra3-launcher-run-")); try { const serverPath = join(process.cwd(), "dist", "agent", "mcpServer.js"); const launcher = join(home, process.platform === "win32" ? "launch.cmd" : "launch.sh"); writeFileSync( launcher, launcherScript({ runtime: electronRuntime(electron), serverPath, projectDir: "D:/Mods/Example", // Point the Node fallback at a path that cannot exist, so a // successful session proves the Electron branch was taken and the // launcher really is Node-free. nodeFallback: join(home, "no-such-node"), }), "utf8", ); const requests = [ JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }), JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }), ].join("\n"); let stdout; try { stdout = execFileSync(launcher, [], { input: requests, encoding: "utf8", timeout: 60000, shell: process.platform === "win32", }); } catch (err) { if (err?.code === "EPERM") { t.skip("sandbox forbids spawning a shell"); return; } throw err; } const lines = stdout.split(/\r?\n/).filter((l) => l.trim()); const init = JSON.parse(lines[0]); assert.equal(init.result.serverInfo.name, "ra3-mod-xml"); const tools = JSON.parse(lines[1]); assert.ok(tools.result.tools.length >= 9); } finally { rmSync(home, { recursive: true, force: true }); } }, );