ai agent
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user