Files
pradigmaz 10e2850d12 Fix PowerShell Unicode path handling (#839)
* fix(web): re-apply hero pattern + make Insight user-count dynamic

Two regressions snuck back in when the /context-saving rename agent
copied from the next branch (which never had the hero pattern fix).
Re-applying on main:

- .cm-master + .cm-wordmark: inline-flex → flex (so the hero-tag
  pill never shares a baseline-aligned row with the wordmark)
- .hero-tag: margin-bottom → margin-top + align-self:flex-start
  (sits below the wordmark on its own line)
- Insight wordmark italic: 1.18em → 1em, margin-left .12em → .14em
- HTML order: <h1> wordmark first, <p class="hero-tag"> after
  (matches master/insight/context-saving)

Plus making the Insight landing's "287,000+ developers" references
dynamic (.js-user-count + the GitHub stats.json fetch shim), so the
number matches the OSS plugin's source-of-truth count instead of
drifting like the screenshots showed (290k+ on /, 287k+ here).

* feat(og): bake live user-count from stats.json into rendered banners

OG PNGs are static, so the 287,000+ number drifted away from the
landing page (which already reads stats.json live). Solution:

- Mark every user-count occurrence in master-og.html and
  context-saving-og.html with class js-user-count
- render-og.mjs now fetches the same stats.json before opening the
  browser, then runs a one-shot DOM swap on every .js-user-count
  element before the screenshot

Result: each render bakes the freshest count in, no manual update
needed. Today's render shows 290,000+ across all three banners.

* perf(web): smooth cursor blink on context-saving hero + topnav

The hero green cursor was driving compositor work harder than it
needed to: steps(2) + a 24px box-shadow meant the GPU repainted
the full glow on every tick, which user reported as a low-FPS feel.

- animation timing: 1.06s steps(2) → 1.4s ease-in-out (smooth pulse
  between opacity 1 and 0.28, terminal-feeling without hard flicker)
- box-shadow blur: 24px → 18px and alpha .45 → .40 (lighter recompose)
- add will-change:opacity + translateZ(0) so the cursor lives on its
  own composited layer (no repaint of surrounding text)
- topnav cursor: same easing pass, renamed keyframe to nav-blink so
  the hero and nav don't share a single declaration name
- prefers-reduced-motion now resolves to a static 0.85 opacity cursor
  instead of a stuck-on solid block

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* ci: update install stats

* Fix PowerShell Unicode path handling

* ci: update install stats

* ci: update install stats

* ci: update install stats

* test: expect UTF-8 BOM in buildPowerShellScriptContent output

The duplicate suite at tests/executor.test.ts still asserted the script
started with the bare [Console] prelude. After #839 prepends a UTF-8 BOM
(U+FEFF) so Windows PowerShell 5.1 detects the script as UTF-8, that
startsWith() assertion failed on every platform (macos + ubuntu CI).

Update the assertion to expect the BOM, mirroring the new test in
tests/core/executor.test.ts and preserving the BOM behavior #839 adds.

---------

Co-authored-by: Mert Koseoglu <bm.ksglu@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 16:00:49 +03:00

2085 lines
74 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, test, expect, afterAll } from "vitest";
import { strict as assert } from "node:assert";
import { existsSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
PolyglotExecutor,
buildScriptFilename,
buildShellScriptContent,
buildPowerShellScriptContent,
buildSpawnOptions,
} from "../src/executor.js";
import {
detectRuntimes,
buildCommand,
getRuntimeSummary,
type RuntimeMap,
} from "../src/runtime.js";
const runtimes = detectRuntimes();
const executor = new PolyglotExecutor({ runtimes });
function findWindowsGitBash(): string | null {
if (process.platform !== "win32") return null;
const candidates = [
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
"C:\\Program Files\\Git\\bin\\bash.exe",
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
];
return candidates.find(p => existsSync(p)) ?? null;
}
describe("Runtime Detection", () => {
test("detects JavaScript runtime (bun or node)", async () => {
const isBun = runtimes.javascript.endsWith("bun");
const isAbsoluteNode = runtimes.javascript.startsWith("/") || runtimes.javascript.includes("\\");
assert.ok(
isBun || isAbsoluteNode,
`Expected bun path or absolute node path, got: ${runtimes.javascript}`,
);
});
test("detects JavaScript runtime (bun or absolute node path)", async () => {
// runtimes.javascript is either a bun path/command or process.execPath —
// never the bare string "node", since snap/wrapper envs need the real binary.
const isBun = runtimes.javascript.endsWith("bun");
const isAbsoluteNode = runtimes.javascript.startsWith("/") || runtimes.javascript.includes("\\");
assert.ok(
isBun || isAbsoluteNode,
`Expected bun path or absolute node path, got: ${runtimes.javascript}`,
);
});
test("buildCommand: javascript uses executable path, not bare 'node'", async () => {
const cmd = buildCommand(runtimes, "javascript", "/tmp/test.js");
assert.notEqual(cmd[0], "node", "Should not use bare 'node' — use process.execPath or full bun path");
assert.equal(cmd[cmd.length - 1], "/tmp/test.js");
});
test("buildCommand: javascript with bun-path runtime uses 'run' subcommand", async () => {
const bunRuntimes: RuntimeMap = { ...runtimes, javascript: "/home/user/.bun/bin/bun" };
const cmd = buildCommand(bunRuntimes, "javascript", "/tmp/test.js");
assert.equal(cmd[0], "/home/user/.bun/bin/bun");
assert.equal(cmd[1], "run");
assert.equal(cmd[2], "/tmp/test.js");
});
test("buildCommand: javascript with Windows bun.exe runtime uses 'run' subcommand", async () => {
const bunRuntimes: RuntimeMap = { ...runtimes, javascript: "C:\\Users\\me\\.bun\\bin\\bun.exe" };
const cmd = buildCommand(bunRuntimes, "javascript", "C:\\tmp\\test.js");
assert.equal(cmd[0], "C:\\Users\\me\\.bun\\bin\\bun.exe");
assert.equal(cmd[1], "run");
assert.equal(cmd[2], "C:\\tmp\\test.js");
});
test("buildCommand: typescript with Windows bun.exe runtime uses 'run' subcommand", async () => {
const bunRuntimes: RuntimeMap = { ...runtimes, typescript: "C:\\Users\\me\\.bun\\bin\\bun.exe" };
const cmd = buildCommand(bunRuntimes, "typescript", "C:\\tmp\\test.ts");
assert.equal(cmd[0], "C:\\Users\\me\\.bun\\bin\\bun.exe");
assert.equal(cmd[1], "run");
assert.equal(cmd[2], "C:\\tmp\\test.ts");
});
test("detects Shell runtime (non-empty string)", async () => {
assert.ok(
typeof runtimes.shell === "string" && runtimes.shell.length > 0,
`Got: ${runtimes.shell}`,
);
});
if (process.platform === "win32") {
const gitBash = findWindowsGitBash();
// #791 is specifically Git Bash + native git path conversion; fallback
// shells are covered by the runtime detection test above.
const gitBashTest = gitBash ? test : test.skip;
test("Windows: shell is Git Bash or fallback, never WSL bash", async () => {
const shell = runtimes.shell.toLowerCase();
assert.ok(
!shell.includes("system32") && !shell.includes("windowsapps"),
`Shell should not be WSL bash, got: ${runtimes.shell}`,
);
});
test("Windows: shell execute works with non-ASCII (Chinese) project path", async () => {
const { mkdirSync } = await import("node:fs");
const { rm } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const chineseDir = join(tmpdir(), "测试目录");
try { mkdirSync(chineseDir, { recursive: true }); } catch {}
const chineseExecutor = new PolyglotExecutor({ runtimes, projectRoot: chineseDir });
const r = await chineseExecutor.execute({ language: "shell", code: 'echo "chinese path ok"' });
assert.equal(r.exitCode, 0, `Failed with stderr: ${r.stderr}`);
assert.ok(r.stdout.includes("chinese path ok"), `Got: ${r.stdout}`);
try { await rm(chineseDir, { recursive: true, force: true }); } catch {}
});
gitBashTest("Windows: Git Bash sees native git repos created under mktemp dirs (#791)", async () => {
const gitBashRuntimes: RuntimeMap = { ...runtimes, shell: gitBash! };
const gitBashExecutor = new PolyglotExecutor({ runtimes: gitBashRuntimes });
const previousNoPathConv = process.env.MSYS_NO_PATHCONV;
const previousArgConvExcl = process.env.MSYS2_ARG_CONV_EXCL;
process.env.MSYS_NO_PATHCONV = "1";
process.env.MSYS2_ARG_CONV_EXCL = "*";
try {
const r = await gitBashExecutor.execute({
language: "shell",
timeout: 20_000,
code: [
"set -e",
"T=$(mktemp -d)",
"git init --bare --quiet \"$T/origin.git\"",
"test -d \"$T/origin.git\"",
"git clone --quiet \"$T/origin.git\" \"$T/clone\"",
"test -d \"$T/clone/.git\"",
"echo git bash tmp path ok",
].join("\n"),
});
assert.equal(r.exitCode, 0, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
assert.ok(r.stdout.includes("git bash tmp path ok"), `Got: ${r.stdout}`);
} finally {
if (previousNoPathConv === undefined) delete process.env.MSYS_NO_PATHCONV;
else process.env.MSYS_NO_PATHCONV = previousNoPathConv;
if (previousArgConvExcl === undefined) delete process.env.MSYS2_ARG_CONV_EXCL;
else process.env.MSYS2_ARG_CONV_EXCL = previousArgConvExcl;
}
});
}
test("detects TypeScript runtime", async () => {
assert.ok(runtimes.typescript !== null, "No TS runtime found");
});
test("detects Python runtime", async () => {
assert.ok(runtimes.python !== null, "No Python runtime found");
});
test("buildCommand: correct JS command structure", async () => {
const cmd = buildCommand(runtimes, "javascript", "/tmp/test.js");
assert.ok(cmd.length >= 2);
assert.ok(cmd[cmd.length - 1] === "/tmp/test.js");
});
test("buildCommand: throws for unavailable runtime", async () => {
const noRuntimes: RuntimeMap = {
javascript: "node",
typescript: null,
python: null,
shell: "sh",
ruby: null,
go: null,
rust: null,
php: null,
perl: null,
r: null,
elixir: null,
csharp: null,
};
assert.throws(
() => buildCommand(noRuntimes, "typescript", "/tmp/t.ts"),
/No TypeScript runtime/,
);
assert.throws(
() => buildCommand(noRuntimes, "python", "/tmp/t.py"),
/No Python runtime/,
);
assert.throws(
() => buildCommand(noRuntimes, "ruby", "/tmp/t.rb"),
/Ruby not available/,
);
assert.throws(
() => buildCommand(noRuntimes, "elixir", "/tmp/t.exs"),
/Elixir not available/,
);
assert.throws(
() => buildCommand(noRuntimes, "csharp", "/tmp/t.csx"),
/C# not available/,
);
});
test("buildShellScriptContent restores inherited PATH on Unix", () => {
const script = buildShellScriptContent("echo ok", "/parent/bin:/usr/bin", "linux");
assert.equal(script, "export PATH='/parent/bin:/usr/bin'\necho ok");
});
test("buildShellScriptContent escapes single quotes in inherited PATH", () => {
const script = buildShellScriptContent("echo ok", "/parent/it'works/bin", "darwin");
assert.equal(script, "export PATH='/parent/it'\\''works/bin'\necho ok");
});
test("buildShellScriptContent leaves Windows shell scripts unchanged", () => {
const script = buildShellScriptContent("echo ok", "C:\\parent\\bin", "win32");
assert.equal(script, "echo ok");
});
test("buildPowerShellScriptContent prefixes UTF-8 encoding setup", () => {
const script = buildPowerShellScriptContent('Write-Output "ok"');
// Windows PowerShell 5.1 only reliably detects a script as UTF-8 when the
// file opens with a BOM; without it the body is read as the ANSI code page.
assert.equal(script.charCodeAt(0), 0xfeff);
assert.ok(script.startsWith("[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()"));
assert.ok(script.includes("$OutputEncoding = [System.Text.UTF8Encoding]::new()"));
assert.ok(script.endsWith('Write-Output "ok"'));
});
});
describe("JavaScript Execution", () => {
test("JS: hello world", async () => {
const r = await executor.execute({
language: "javascript",
code: 'console.log("hello from js");',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from js"));
assert.equal(r.timedOut, false);
});
test("JS: variables, math, template literals", async () => {
const r = await executor.execute({
language: "javascript",
code: "const x = 42; const y = 58; console.log(`sum: ${x + y}`);",
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 100"));
});
test("JS: JSON parse + transform", async () => {
const r = await executor.execute({
language: "javascript",
code: `
const data = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
];
const avg = data.reduce((s, d) => s + d.age, 0) / data.length;
console.log(JSON.stringify({ count: data.length, avgAge: avg.toFixed(1) }));
`,
});
assert.equal(r.exitCode, 0);
const output = JSON.parse(r.stdout.trim());
assert.equal(output.count, 3);
assert.equal(output.avgAge, "30.0");
});
test("JS: async/await + setTimeout", async () => {
const r = await executor.execute({
language: "javascript",
code: `
async function work() {
return new Promise(resolve => setTimeout(() => resolve("async done"), 50));
}
work().then(r => console.log(r));
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("async done"));
});
test("JS: require node:os module", async () => {
const r = await executor.execute({
language: "javascript",
code: 'const os = require("os"); console.log("platform:", os.platform());',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("platform:"));
});
test("JS: Array.from + map/filter/reduce chain", async () => {
const r = await executor.execute({
language: "javascript",
code: `
const nums = Array.from({length: 100}, (_, i) => i + 1);
const evenSum = nums.filter(n => n % 2 === 0).reduce((a, b) => a + b, 0);
console.log("even sum:", evenSum);
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("even sum: 2550"));
});
});
describe.runIf(runtimes.typescript)("TypeScript Execution", () => {
test("TS: hello world with type annotation", async () => {
const r = await executor.execute({
language: "typescript",
code: 'const msg: string = "hello from ts"; console.log(msg);',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from ts"));
});
test("TS: interface + generics", async () => {
const r = await executor.execute({
language: "typescript",
code: `
interface Item<T> { id: number; value: T; }
const items: Item<string>[] = [
{ id: 1, value: "apple" },
{ id: 2, value: "banana" },
];
function first<T>(arr: T[]): T | undefined { return arr[0]; }
console.log(first(items)?.value);
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("apple"));
});
test("TS: enum + switch", async () => {
const r = await executor.execute({
language: "typescript",
code: `
enum Color { Red = "red", Blue = "blue", Green = "green" }
function describe(c: Color): string {
switch (c) {
case Color.Red: return "warm";
case Color.Blue: return "cool";
case Color.Green: return "natural";
}
}
console.log(describe(Color.Blue));
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("cool"));
});
test("TS: async + Promise.all", async () => {
const r = await executor.execute({
language: "typescript",
code: `
async function fetchNum(n: number): Promise<number> {
return new Promise(resolve => setTimeout(() => resolve(n * 2), 10));
}
Promise.all([1, 2, 3].map(fetchNum)).then(results => {
console.log("doubled:", results.join(", "));
});
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("doubled: 2, 4, 6"));
});
});
describe.runIf(runtimes.python)("Python Execution", () => {
test("Python: hello world", async () => {
const r = await executor.execute({
language: "python",
code: 'print("hello from python")',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from python"));
});
test("Python: list comprehension + math", async () => {
const r = await executor.execute({
language: "python",
code: `
nums = [i**2 for i in range(10)]
print(f"squares: {nums}")
print(f"sum: {sum(nums)}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 285"));
});
test("Python: dict + json", async () => {
const r = await executor.execute({
language: "python",
code: `
import json
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
print(json.dumps({"count": len(data["users"])}))
`,
});
assert.equal(r.exitCode, 0);
const out = JSON.parse(r.stdout.trim());
assert.equal(out.count, 2);
});
test("Python: csv with io.StringIO", async () => {
const r = await executor.execute({
language: "python",
code: `
import io, csv
data = "name,age\\nAlice,30\\nBob,25\\nCharlie,35"
reader = csv.DictReader(io.StringIO(data))
rows = list(reader)
print(f"rows: {len(rows)}, names: {[r['name'] for r in rows]}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("rows: 3"));
});
test("Python: regex extraction", async () => {
const r = await executor.execute({
language: "python",
code: `
import re
text = "Error: 404 at /api/users, Error: 500 at /api/data, OK: 200"
errors = re.findall(r'Error: (\\d+) at (\\S+)', text)
print(f"Found {len(errors)} errors: {errors}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("Found 2 errors"));
});
test("Python: collections.Counter", async () => {
const r = await executor.execute({
language: "python",
code: `
from collections import Counter
words = "the cat sat on the mat the cat".split()
c = Counter(words)
print(f"most common: {c.most_common(2)}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("most common:"));
assert.ok(r.stdout.includes("the"));
});
});
describe("Shell Execution", () => {
test("Shell: hello world", async () => {
const r = await executor.execute({
language: "shell",
code: 'echo "hello from shell"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from shell"));
});
test("Shell: pipes + sort", async () => {
const r = await executor.execute({
language: "shell",
code: 'printf "banana\\napple\\ncherry" | sort',
});
assert.equal(r.exitCode, 0);
const lines = r.stdout.trim().split("\n");
assert.equal(lines[0], "apple");
assert.equal(lines[1], "banana");
assert.equal(lines[2], "cherry");
});
test("Shell: arithmetic + variables", async () => {
const r = await executor.execute({
language: "shell",
code: 'X=10\nY=20\necho "sum: $((X + Y))"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 30"));
});
test.runIf(process.platform !== "win32")(
"shell restores inherited PATH after startup clobbers it",
async () => {
const originalPath = process.env.PATH;
const testDir = join(tmpdir(), `ctx-path-${process.pid}-${Date.now()}`);
const binDir = join(testDir, "bin");
const fakeBin = join(binDir, "ctx-path-probe");
const fakeShell = join(testDir, "sh");
try {
mkdirSync(binDir, { recursive: true });
writeFileSync(fakeBin, "#!/bin/sh\nprintf 'probe-ok\\n'\n", { mode: 0o755 });
writeFileSync(
fakeShell,
"#!/bin/sh\nPATH=/usr/bin:/bin\nexport PATH\nexec /bin/sh \"$@\"\n",
{ mode: 0o755 },
);
process.env.PATH = `${binDir}:${originalPath ?? ""}`;
const pathClobberingShell = new PolyglotExecutor({
runtimes: { ...runtimes, shell: fakeShell },
});
const r = await pathClobberingShell.execute({
language: "shell",
code: "command -v ctx-path-probe\nctx-path-probe",
timeout: 5_000,
});
assert.equal(r.exitCode, 0, r.stderr);
assert.ok(r.stdout.includes(fakeBin), `Expected ${fakeBin}, got: ${r.stdout}`);
assert.ok(r.stdout.includes("probe-ok"), `Got: ${r.stdout}`);
} finally {
if (originalPath === undefined) delete process.env.PATH;
else process.env.PATH = originalPath;
rmSync(testDir, { recursive: true, force: true });
}
},
);
test("shell TMPDIR points to sandbox temp dir, not project root", async () => {
const r = await executor.execute({
language: "shell",
code: 'echo "$TMPDIR"',
timeout: 5_000,
});
const reported = r.stdout.trim();
assert.ok(
!reported.startsWith(process.cwd()),
`TMPDIR should not be project root, got: ${reported}`,
);
assert.ok(
reported.includes(".ctx-mode-"),
`TMPDIR should be the sandbox temp dir, got: ${reported}`,
);
});
test("shell TMPDIR works cross-platform (not just echo)", async () => {
// Use a command that writes to TMPDIR to verify it's writable
const r = await executor.execute({
language: "shell",
code: 'touch "$TMPDIR/ctx-test-file" && echo "ok"',
timeout: 5_000,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.trim() === "ok");
});
test("Shell: for loop + wc", async () => {
const r = await executor.execute({
language: "shell",
code: 'for i in 1 2 3 4 5; do echo "item $i"; done | wc -l | tr -d " "',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.trim() === "5");
});
test("Shell: awk processing", async () => {
const r = await executor.execute({
language: "shell",
code: `printf "Alice 30\\nBob 25\\nCharlie 35" | awk '{sum += $2; count++} END {print "avg:", sum/count}'`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("avg: 30"));
});
});
describe.runIf(runtimes.ruby)("Ruby Execution", () => {
test("Ruby: hello world", async () => {
const r = await executor.execute({
language: "ruby",
code: 'puts "hello from ruby"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from ruby"));
});
test("Ruby: array methods", async () => {
const r = await executor.execute({
language: "ruby",
code: `
nums = (1..10).to_a
evens = nums.select { |n| n.even? }
puts "evens: #{evens.join(', ')}"
puts "sum: #{evens.sum}"
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 30"));
});
test("Ruby: hash + JSON", async () => {
const r = await executor.execute({
language: "ruby",
code: `
require 'json'
data = { users: [{ name: "Alice" }, { name: "Bob" }] }
puts JSON.generate({ count: data[:users].length })
`,
});
assert.equal(r.exitCode, 0);
const out = JSON.parse(r.stdout.trim());
assert.equal(out.count, 2);
});
});
describe.runIf(runtimes.go)("Go Execution", () => {
test("Go: hello world", async () => {
const r = await executor.execute({
language: "go",
code: 'fmt.Println("hello from go")',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from go"));
});
test("Go: loops + slices", async () => {
const r = await executor.execute({
language: "go",
code: `
nums := []int{1, 2, 3, 4, 5}
sum := 0
for _, n := range nums {
sum += n
}
fmt.Println("sum:", sum)
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 15"));
});
});
describe.runIf(runtimes.php)("PHP Execution", () => {
test("PHP: hello world", async () => {
const r = await executor.execute({
language: "php",
code: 'echo "hello from php\\n";',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from php"));
});
test("PHP: array functions", async () => {
const r = await executor.execute({
language: "php",
code: `
$nums = range(1, 10);
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
echo "evens: " . implode(", ", $evens) . "\\n";
echo "sum: " . array_sum($evens) . "\\n";
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 30"));
});
});
describe.runIf(runtimes.perl)("Perl Execution", () => {
test("Perl: hello world", async () => {
const r = await executor.execute({
language: "perl",
code: 'print "hello from perl\\n";',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from perl"));
});
test("Perl: regex + array", async () => {
const r = await executor.execute({
language: "perl",
code: `
my @words = ("apple", "banana", "avocado", "blueberry");
my @a_words = grep { /^a/i } @words;
print "a-words: @a_words\\n";
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("apple"));
assert.ok(r.stdout.includes("avocado"));
});
});
describe.runIf(runtimes.r)("R Execution", () => {
test("R: hello world", async () => {
const r = await executor.execute({
language: "r",
code: 'cat("hello from R\\n")',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from R"));
});
test("R: vector operations", async () => {
const r = await executor.execute({
language: "r",
code: `
nums <- 1:10
cat("mean:", mean(nums), "\\n")
cat("sum:", sum(nums), "\\n")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 55"));
});
});
describe.runIf(runtimes.elixir)("Elixir Execution", () => {
test("Elixir: hello world", async () => {
const r = await executor.execute({
language: "elixir",
code: 'IO.puts("hello from elixir")',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("hello from elixir"));
});
test("Elixir: list operations + Enum", async () => {
const r = await executor.execute({
language: "elixir",
code: `
nums = Enum.to_list(1..10)
evens = Enum.filter(nums, fn n -> rem(n, 2) == 0 end)
IO.puts("evens: #{Enum.join(evens, ", ")}")
IO.puts("sum: #{Enum.sum(evens)}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("sum: 30"));
});
test("Elixir: map + pattern matching", async () => {
const r = await executor.execute({
language: "elixir",
code: `
users = [%{name: "Alice", role: "admin"}, %{name: "Bob", role: "user"}]
admins = Enum.filter(users, fn %{role: role} -> role == "admin" end)
IO.puts("admins: #{length(admins)}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("admins: 1"));
});
test("Elixir: pipe operator + String functions", async () => {
const r = await executor.execute({
language: "elixir",
code: `
result =
"hello world from elixir"
|> String.split()
|> Enum.map(&String.upcase/1)
|> Enum.join(" ")
IO.puts(result)
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("HELLO WORLD FROM ELIXIR"));
});
test("Elixir: error raises non-zero exit", async () => {
const r = await executor.execute({
language: "elixir",
code: 'raise "intentional error"',
});
assert.notEqual(r.exitCode, 0);
assert.ok(r.stderr.length > 0 || r.stdout.includes("intentional error"));
});
});
describe.runIf(runtimes.csharp)("CSharp Execution", () => {
test("C#: hello world", async () => {
const r = await executor.execute({
language: "csharp",
code: 'Console.WriteLine("hello from csharp");',
});
assert.equal(r.exitCode, 0, r.stderr);
assert.ok(r.stdout.includes("hello from csharp"));
});
test("C#: LINQ over array", async () => {
const r = await executor.execute({
language: "csharp",
code: `
using System.Linq;
var nums = Enumerable.Range(1, 10);
var evens = nums.Where(n => n % 2 == 0).ToArray();
Console.WriteLine($"sum: {evens.Sum()}");
`,
});
assert.equal(r.exitCode, 0, r.stderr);
assert.ok(r.stdout.includes("sum: 30"));
});
test("C#: error returns non-zero exit", async () => {
const r = await executor.execute({
language: "csharp",
code: 'throw new System.Exception("intentional error");',
});
assert.notEqual(r.exitCode, 0);
assert.ok(r.stderr.length > 0 || r.stdout.includes("intentional error"));
});
});
describe("Error Handling", () => {
test("JS: syntax error returns non-zero", async () => {
const r = await executor.execute({
language: "javascript",
code: "const x = {",
});
assert.notEqual(r.exitCode, 0);
assert.ok(r.stderr.length > 0);
});
test("JS: runtime error returns non-zero", async () => {
const r = await executor.execute({
language: "javascript",
code: 'throw new Error("intentional");',
});
assert.notEqual(r.exitCode, 0);
assert.ok(r.stderr.includes("intentional"));
});
test.runIf(runtimes.python)("Python: syntax error", async () => {
const r = await executor.execute({
language: "python",
code: "def foo(\n pass",
});
assert.notEqual(r.exitCode, 0);
});
test.runIf(runtimes.python)("Python: runtime error (ValueError)", async () => {
const r = await executor.execute({
language: "python",
code: 'raise ValueError("test error")',
});
assert.notEqual(r.exitCode, 0);
assert.ok(r.stderr.includes("ValueError"));
});
test("Shell: non-zero exit code preserved", async () => {
const r = await executor.execute({
language: "shell",
code: "exit 42",
});
assert.equal(r.exitCode, 42);
});
test("Shell: command not found", async () => {
const r = await executor.execute({
language: "shell",
code: "nonexistent_command_xyz 2>&1",
});
assert.notEqual(r.exitCode, 0);
});
});
describe("Timeout Handling", () => {
test("JS: infinite loop times out", async () => {
const r = await executor.execute({
language: "javascript",
code: "while(true) {}",
timeout: 500,
});
assert.equal(r.timedOut, true);
});
// Issue #406 — when timeout omitted, no server-side timer fires and a
// long-running process completes naturally. Caller (or MCP host) owns
// the timeout policy.
test("JS: no timeout — short script completes without forced kill", async () => {
const r = await executor.execute({
language: "javascript",
// 250ms wait then print — caller didn't pass timeout, so we must
// wait for natural exit, not kill at any heuristic ceiling.
code: "setTimeout(() => { console.log('done'); }, 250);",
});
assert.equal(r.timedOut, false);
assert.equal(r.stdout.trim(), "done");
});
test("Shell: no timeout — sleep 1 completes without forced kill", async () => {
const r = await executor.execute({
language: "shell",
code: "sleep 1 && echo done",
});
assert.equal(r.timedOut, false);
assert.equal(r.stdout.trim(), "done");
});
test("JS: infinite loop leaves no orphaned process after kill", async () => {
// Spawn a process that writes its PID then loops forever
const r = await executor.execute({
language: "javascript",
code: `process.stdout.write(String(process.pid)); while(true) {}`,
timeout: 1000,
});
assert.equal(r.timedOut, true);
const pid = parseInt(r.stdout.trim(), 10);
assert.ok(pid > 0, `Expected valid PID in stdout, got: "${r.stdout}"`);
// Give OS a moment to reap
await new Promise(r => setTimeout(r, 200));
let alive = false;
try {
process.kill(pid, 0); // signal 0 = check if alive
alive = true;
} catch { /* ESRCH = not found = good */ }
assert.equal(alive, false, `Process ${pid} should be dead after timeout kill`);
}, 10_000);
test("JS: child processes are killed with parent (no orphans)", async () => {
// Parent spawns a child that writes its PID to stderr, then both loop
const code = `
const { fork } = require("child_process");
if (process.env.__CHILD__) {
process.stderr.write(String(process.pid));
while(true) {}
} else {
process.stdout.write(String(process.pid));
const env = { ...process.env, __CHILD__: "1" };
fork(process.argv[1], { env });
while(true) {}
}
`;
const r = await executor.execute({
language: "javascript",
code,
timeout: 1500,
});
assert.equal(r.timedOut, true);
const parentPid = parseInt(r.stdout.trim(), 10);
const childPid = parseInt(r.stderr.trim(), 10);
assert.ok(parentPid > 0, `Expected parent PID, got: "${r.stdout}"`);
assert.ok(childPid > 0, `Expected child PID, got: "${r.stderr}"`);
await new Promise(r => setTimeout(r, 200));
for (const pid of [parentPid, childPid]) {
let alive = false;
try { process.kill(pid, 0); alive = true; } catch {}
assert.equal(alive, false, `Process ${pid} should be dead after group kill`);
}
}, 10_000);
test("Shell: sleep times out", async () => {
const r = await executor.execute({
language: "shell",
code: "sleep 5",
timeout: 500,
});
assert.equal(r.timedOut, true);
}, 10_000);
test.runIf(runtimes.python)("Python: infinite sleep times out", async () => {
const r = await executor.execute({
language: "python",
code: "import time; time.sleep(5)",
timeout: 500,
});
assert.equal(r.timedOut, true);
});
});
describe("Output Truncation", () => {
test("stdout is returned in full without truncation", async () => {
const small = new PolyglotExecutor({ runtimes });
const r = await small.execute({
language: "javascript",
code: 'for (let i = 0; i < 100; i++) console.log(`line ${i}: ${"x".repeat(20)}`);',
});
assert.ok(!r.stdout.includes("truncated"), "stdout should NOT be truncated");
assert.ok(r.stdout.includes("line 0"), "Should contain first line");
assert.ok(r.stdout.includes("line 50"), "Should contain middle line (previously lost)");
assert.ok(r.stdout.includes("line 99"), "Should contain last line");
assert.ok(!r.stdout.includes("showing first"), "Should have no truncation marker");
});
test("does not truncate under limit", async () => {
const r = await executor.execute({
language: "javascript",
code: 'console.log("small output");',
});
assert.ok(!r.stdout.includes("truncated"));
});
test("stderr is also returned in full without truncation", async () => {
const small = new PolyglotExecutor({ runtimes });
const r = await small.execute({
language: "javascript",
code: `
for (let i = 0; i < 50; i++) console.error("warn " + i);
console.error("FINAL ERROR: something broke");
`,
});
assert.ok(r.stderr.includes("FINAL ERROR"), "Should contain last error line");
assert.ok(r.stderr.includes("warn 0"), "Should contain first warning");
assert.ok(r.stderr.includes("warn 25"), "Should contain middle warning (previously lost)");
assert.ok(!r.stderr.includes("truncated"), "stderr should NOT be truncated");
});
});
describe("execute_file (FILE_CONTENT)", () => {
const testDir = join(tmpdir(), "ctx-mode-test-" + Date.now());
mkdirSync(testDir, { recursive: true });
const testFile = join(testDir, "test-data.json");
writeFileSync(
testFile,
JSON.stringify({
users: [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" },
],
}),
"utf-8",
);
test("execute_file: JS reads FILE_CONTENT", async () => {
const r = await executor.executeFile({
path: testFile,
language: "javascript",
code: `
const data = JSON.parse(FILE_CONTENT);
const admins = data.users.filter(u => u.role === "admin");
console.log("admins: " + admins.map(a => a.name).join(", "));
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("admins: Alice, Charlie"));
});
test.runIf(runtimes.python)("execute_file: Python reads FILE_CONTENT", async () => {
const r = await executor.executeFile({
path: testFile,
language: "python",
code: `
import json
data = json.loads(FILE_CONTENT)
print(f"Users: {len(data['users'])}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("Users: 3"));
});
test("execute_file: Shell reads FILE_CONTENT", async () => {
const r = await executor.executeFile({
path: testFile,
language: "shell",
code: 'echo "size: ${#FILE_CONTENT} bytes"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("bytes"));
});
test.runIf(runtimes.ruby)("execute_file: Ruby reads FILE_CONTENT", async () => {
const r = await executor.executeFile({
path: testFile,
language: "ruby",
code: `
require 'json'
data = JSON.parse(FILE_CONTENT)
puts "Users: #{data['users'].length}"
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("Users: 3"));
});
test.runIf(runtimes.csharp)("execute_file: C# reads FILE_CONTENT", async () => {
const r = await executor.executeFile({
path: testFile,
language: "csharp",
code: `
using (var doc = System.Text.Json.JsonDocument.Parse(FILE_CONTENT)) {
var users = doc.RootElement.GetProperty("users").GetArrayLength();
System.Console.WriteLine($"Users: {users}");
}
`,
});
assert.equal(r.exitCode, 0, r.stderr);
assert.ok(r.stdout.includes("Users: 3"));
});
// --- execute_file: shell $ expansion in paths ---
const dollarDir = join(testDir, "path$SHOULD_NOT_EXPAND");
mkdirSync(dollarDir, { recursive: true });
const dollarFile = join(dollarDir, "data.txt");
writeFileSync(dollarFile, "dollar-sign-content", "utf-8");
test("execute_file: Shell path with $ is not expanded", async () => {
const r = await executor.executeFile({
path: dollarFile,
language: "shell",
code: 'echo "content: $FILE_CONTENT"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(
r.stdout.includes("content: dollar-sign-content"),
`Expected literal file content, got: ${r.stdout}`,
);
});
// --- execute_file: shell paths with spaces ---
const spaceDir = join(testDir, "path with spaces");
mkdirSync(spaceDir, { recursive: true });
const spaceFile = join(spaceDir, "space file.txt");
writeFileSync(spaceFile, "space-content", "utf-8");
test("execute_file: Shell path with spaces works correctly", async () => {
const r = await executor.executeFile({
path: spaceFile,
language: "shell",
code: 'echo "content: $FILE_CONTENT"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(
r.stdout.includes("content: space-content"),
`Expected literal file content, got: ${r.stdout}`,
);
});
// --- execute_file: shell paths with single quotes ---
const quoteDir = join(testDir, "it's-a-dir");
mkdirSync(quoteDir, { recursive: true });
const quoteFile = join(quoteDir, "quote.txt");
writeFileSync(quoteFile, "quote-content", "utf-8");
test("execute_file: Shell path with single quotes works correctly", async () => {
const r = await executor.executeFile({
path: quoteFile,
language: "shell",
code: 'echo "content: $FILE_CONTENT"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(
r.stdout.includes("content: quote-content"),
`Expected literal file content, got: ${r.stdout}`,
);
});
// --- execute_file: shell paths with backticks ---
const backtickDir = join(testDir, "dir`tick");
mkdirSync(backtickDir, { recursive: true });
const backtickFile = join(backtickDir, "bt.txt");
writeFileSync(backtickFile, "backtick-content", "utf-8");
test("execute_file: Shell path with backticks is not executed", async () => {
const r = await executor.executeFile({
path: backtickFile,
language: "shell",
code: 'echo "content: $FILE_CONTENT"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(
r.stdout.includes("content: backtick-content"),
`Expected literal file content, got: ${r.stdout}`,
);
});
// --- execute_file: shell paths with combined special characters ---
const comboDir = join(testDir, "$HOME has `spaces` & 'quotes'");
mkdirSync(comboDir, { recursive: true });
const comboFile = join(comboDir, "combo.txt");
writeFileSync(comboFile, "combo-content", "utf-8");
test("execute_file: Shell path with combined special chars works", async () => {
const r = await executor.executeFile({
path: comboFile,
language: "shell",
code: 'echo "content: $FILE_CONTENT"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(
r.stdout.includes("content: combo-content"),
`Expected literal file content, got: ${r.stdout}`,
);
});
test.runIf(runtimes.elixir)("execute_file: Elixir reads file_content", async () => {
const r = await executor.executeFile({
path: testFile,
language: "elixir",
code: `
IO.puts("file size: #{byte_size(file_content)}")
IO.puts("has users: #{String.contains?(file_content, "users")}")
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("has users: true"));
});
// --- UTF-8 / Non-ASCII file content ---
const utf8File = join(testDir, "utf8-data.txt");
writeFileSync(utf8File, "这是中文内容\n日本語テスト\n한국어\nEmoji: 🔒✅\nLine 5", "utf-8");
test("execute_file: Python reads UTF-8 non-ASCII content", async () => {
const r = await executor.executeFile({
path: utf8File,
language: "python",
code: `
lines = FILE_CONTENT.strip().split('\\n')
print(f"lines: {len(lines)}")
print(f"first: {lines[0]}")
print(f"has_emoji: {'🔒' in FILE_CONTENT}")
`,
});
assert.equal(r.exitCode, 0, "Python UTF-8 exit code: " + r.stderr);
assert.ok(r.stdout.includes("lines: 5"), "Should have 5 lines");
assert.ok(r.stdout.includes("first: 这是中文内容"), "Should read Chinese");
assert.ok(r.stdout.includes("has_emoji: True"), "Should find emoji");
});
test("execute_file: JS reads UTF-8 non-ASCII content", async () => {
const r = await executor.executeFile({
path: utf8File,
language: "javascript",
code: `
const lines = FILE_CONTENT.trim().split('\\n');
console.log("lines: " + lines.length);
console.log("first: " + lines[0]);
console.log("has_emoji: " + FILE_CONTENT.includes('🔒'));
`,
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("lines: 5"));
assert.ok(r.stdout.includes("first: 这是中文内容"));
assert.ok(r.stdout.includes("has_emoji: true"));
});
test.runIf(runtimes.ruby)("execute_file: Ruby reads UTF-8 non-ASCII content", async () => {
const r = await executor.executeFile({
path: utf8File,
language: "ruby",
code: `
lines = FILE_CONTENT.strip.split("\\n")
puts "lines: #{lines.length}"
puts "first: #{lines[0]}"
puts "has_emoji: #{FILE_CONTENT.include?('🔒')}"
`,
});
assert.equal(r.exitCode, 0, "Ruby UTF-8 exit code: " + r.stderr);
assert.ok(r.stdout.includes("lines: 5"));
assert.ok(r.stdout.includes("first: 这是中文内容"));
});
test("execute_file: Shell reads UTF-8 non-ASCII content", async () => {
const r = await executor.executeFile({
path: utf8File,
language: "shell",
code: 'echo "$FILE_CONTENT" | head -1',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("这是中文内容"), "Shell should read Chinese: " + r.stdout);
});
// --- execute_file: file_path alias ---
test.runIf(runtimes.python)("execute_file: Python exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "python",
code: `
import json
with open(file_path) as f:
data = json.load(f)
print(f"Users via file_path: {len(data['users'])}")
`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes("Users via file_path: 3"), `Got: ${r.stdout}`);
});
test("execute_file: JS exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "javascript",
code: `console.log("file_path alias: " + file_path);`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test("execute_file: TypeScript exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "typescript",
code: `console.log("file_path alias: " + file_path);`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test("execute_file: Shell exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "shell",
code: 'echo "file_path alias: $file_path"',
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.ruby)("execute_file: Ruby exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "ruby",
code: `
require 'json'
data = JSON.parse(File.read(file_path))
puts "Users via file_path: #{data['users'].length}"
`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes("Users via file_path: 3"), `Got: ${r.stdout}`);
});
test.runIf(runtimes.go)("execute_file: Go exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "go",
code: `fmt.Println("file_path alias: " + file_path)`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.rust)("execute_file: Rust exposes 'file_path' as alias for file_content_path", async () => {
const r = await executor.executeFile({
path: testFile,
language: "rust",
code: `println!("file_path alias: {}", file_path);`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.php)("execute_file: PHP exposes '$file_path' as alias for $FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "php",
code: `echo "file_path alias: " . $file_path . "\\n";`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.perl)("execute_file: Perl exposes '$file_path' as alias for $FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "perl",
code: `print "file_path alias: $file_path\\n";`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.r)("execute_file: R exposes 'file_path' as alias for FILE_CONTENT_PATH", async () => {
const r = await executor.executeFile({
path: testFile,
language: "r",
code: `cat(paste0("file_path alias: ", file_path, "\\n"))`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
test.runIf(runtimes.elixir)("execute_file: Elixir exposes 'file_path' as alias for file_content_path", async () => {
const r = await executor.executeFile({
path: testFile,
language: "elixir",
code: `IO.puts("file_path alias: " <> file_path)`,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(testFile), `Got: ${r.stdout}`);
});
afterAll(() => {
rmSync(testDir, { recursive: true, force: true });
});
});
describe("Environment Passthrough", () => {
test("SSH_AUTH_SOCK is passed through to subprocess when set", async () => {
const original = process.env.SSH_AUTH_SOCK;
process.env.SSH_AUTH_SOCK = "/tmp/test-ssh-agent.sock";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK"',
});
assert.equal(r.exitCode, 0);
assert.ok(
r.stdout.includes("/tmp/test-ssh-agent.sock"),
`Expected SSH_AUTH_SOCK to be passed through, got: ${r.stdout}`,
);
} finally {
if (original === undefined) delete process.env.SSH_AUTH_SOCK;
else process.env.SSH_AUTH_SOCK = original;
}
});
test("SSH_AGENT_PID is passed through to subprocess when set", async () => {
const original = process.env.SSH_AGENT_PID;
process.env.SSH_AGENT_PID = "99999";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "SSH_AGENT_PID=$SSH_AGENT_PID"',
});
assert.equal(r.exitCode, 0);
assert.ok(
r.stdout.includes("99999"),
`Expected SSH_AGENT_PID to be passed through, got: ${r.stdout}`,
);
} finally {
if (original === undefined) delete process.env.SSH_AGENT_PID;
else process.env.SSH_AGENT_PID = original;
}
});
test("SSH_AUTH_SOCK is absent from subprocess when not set in parent", async () => {
const original = process.env.SSH_AUTH_SOCK;
delete process.env.SSH_AUTH_SOCK;
try {
const r = await executor.execute({
language: "shell",
code: 'if [ -z "${SSH_AUTH_SOCK+x}" ]; then echo "unset"; else echo "set=$SSH_AUTH_SOCK"; fi',
});
assert.equal(r.exitCode, 0);
assert.ok(
r.stdout.includes("unset"),
`Expected SSH_AUTH_SOCK to be absent, got: ${r.stdout}`,
);
} finally {
if (original !== undefined) process.env.SSH_AUTH_SOCK = original;
}
});
});
describe("Environment Denylist", () => {
test("dangerous vars are stripped from subprocess (BASH_ENV, NODE_OPTIONS)", async () => {
const origBash = process.env.BASH_ENV;
const origNode = process.env.NODE_OPTIONS;
process.env.BASH_ENV = "/tmp/evil.sh";
process.env.NODE_OPTIONS = "--inspect";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "BASH_ENV=${BASH_ENV:-unset}" && echo "NODE_OPTIONS=${NODE_OPTIONS:-unset}"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("BASH_ENV=unset"), `BASH_ENV should be stripped, got: ${r.stdout}`);
assert.ok(r.stdout.includes("NODE_OPTIONS=unset"), `NODE_OPTIONS should be stripped, got: ${r.stdout}`);
} finally {
if (origBash === undefined) delete process.env.BASH_ENV;
else process.env.BASH_ENV = origBash;
if (origNode === undefined) delete process.env.NODE_OPTIONS;
else process.env.NODE_OPTIONS = origNode;
}
});
test("dangerous vars are stripped: PERL5OPT, RUBYOPT, LD_PRELOAD", async () => {
const origPerl = process.env.PERL5OPT;
const origRuby = process.env.RUBYOPT;
const origLD = process.env.LD_PRELOAD;
process.env.PERL5OPT = "-Mbase";
process.env.RUBYOPT = "-rmalicious";
process.env.LD_PRELOAD = "/tmp/evil.so";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "PERL5OPT=${PERL5OPT:-unset}" && echo "RUBYOPT=${RUBYOPT:-unset}" && echo "LD_PRELOAD=${LD_PRELOAD:-unset}"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("PERL5OPT=unset"), `PERL5OPT should be stripped, got: ${r.stdout}`);
assert.ok(r.stdout.includes("RUBYOPT=unset"), `RUBYOPT should be stripped, got: ${r.stdout}`);
assert.ok(r.stdout.includes("LD_PRELOAD=unset"), `LD_PRELOAD should be stripped, got: ${r.stdout}`);
} finally {
if (origPerl === undefined) delete process.env.PERL5OPT;
else process.env.PERL5OPT = origPerl;
if (origRuby === undefined) delete process.env.RUBYOPT;
else process.env.RUBYOPT = origRuby;
if (origLD === undefined) delete process.env.LD_PRELOAD;
else process.env.LD_PRELOAD = origLD;
}
});
test("user env vars pass through by default (no allowlist needed)", async () => {
const origSlack = process.env.SLACK_BOT_TOKEN;
const origCustom = process.env.MY_CUSTOM_API_KEY;
process.env.SLACK_BOT_TOKEN = "xoxb-test-token";
process.env.MY_CUSTOM_API_KEY = "custom-12345";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "SLACK=$SLACK_BOT_TOKEN" && echo "CUSTOM=$MY_CUSTOM_API_KEY"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("SLACK=xoxb-test-token"), `SLACK_BOT_TOKEN should pass through, got: ${r.stdout}`);
assert.ok(r.stdout.includes("CUSTOM=custom-12345"), `MY_CUSTOM_API_KEY should pass through, got: ${r.stdout}`);
} finally {
if (origSlack === undefined) delete process.env.SLACK_BOT_TOKEN;
else process.env.SLACK_BOT_TOKEN = origSlack;
if (origCustom === undefined) delete process.env.MY_CUSTOM_API_KEY;
else process.env.MY_CUSTOM_API_KEY = origCustom;
}
});
test("sandbox overrides take precedence over parent env", async () => {
const r = await executor.execute({
language: "shell",
code: 'echo "NO_COLOR=$NO_COLOR" && echo "PYTHONUNBUFFERED=$PYTHONUNBUFFERED"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("NO_COLOR=1"), `NO_COLOR should be forced to 1, got: ${r.stdout}`);
assert.ok(r.stdout.includes("PYTHONUNBUFFERED=1"), `PYTHONUNBUFFERED should be forced to 1, got: ${r.stdout}`);
});
// PR #546 follow-up: Microsoft documents native injection vectors that
// hijack the dotnet host. Profiler attach loads an arbitrary DLL into the
// host process; DiagnosticPorts allows a peer to attach a debugger /
// inject IL via the IPC port; the bundle extract dir lets a
// single-file app extraction be redirected. Every DOTNET_* knob has a
// COMPlus_* synonym (back-compat alias). Strip the explicit set and
// sweep COMPlus_* via prefix.
// Refs: learn.microsoft.com/en-us/dotnet/core/runtime-config/{debugging-profiling,dotnet-environment-variables}
test("DOTNET profiler + DiagnosticPorts + COMPlus aliases are stripped", async () => {
const KEYS = [
"CORECLR_PROFILER",
"CORECLR_PROFILER_PATH",
"CORECLR_PROFILER_PATH_32",
"CORECLR_PROFILER_PATH_64",
"CORECLR_PROFILER_PATH_ARM32",
"CORECLR_PROFILER_PATH_ARM64",
"DOTNET_PROFILER_PATH",
"DOTNET_PROFILER_PATH_32",
"DOTNET_PROFILER_PATH_64",
"DOTNET_PROFILER_PATH_ARM32",
"DOTNET_PROFILER_PATH_ARM64",
"CORECLR_ENABLE_PROFILING",
"DOTNET_DiagnosticPorts",
"DOTNET_BUNDLE_EXTRACT_BASE_DIR",
// COMPlus_* alias coverage — sweep via /^COMPlus_/i prefix.
"COMPlus_EnableDiagnostics",
"COMPlus_DbgEnableMiniDump",
"COMPlus_TieredCompilation",
];
const saved: Record<string, string | undefined> = {};
for (const k of KEYS) {
saved[k] = process.env[k];
process.env[k] = "evil-injected";
}
try {
const echoes = KEYS.map((k) => `echo "${k}=\${${k}:-unset}"`).join(" && ");
const r = await executor.execute({ language: "shell", code: echoes });
assert.equal(r.exitCode, 0);
for (const k of KEYS) {
assert.ok(
r.stdout.includes(`${k}=unset`),
`${k} should be stripped, got: ${r.stdout}`,
);
}
} finally {
for (const k of KEYS) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
}
});
test("CLAUDE_PLUGIN_ROOT and unrelated env vars survive after DOTNET strip", async () => {
// Defense-in-depth — make sure the new explicit denylist + COMPlus_*
// sweep do not accidentally clobber unrelated env. CLAUDE_PLUGIN_ROOT
// is the canary because the hooks rely on it.
const origRoot = process.env.CLAUDE_PLUGIN_ROOT;
const origUnrelated = process.env.MY_HARMLESS_VAR;
process.env.CLAUDE_PLUGIN_ROOT = "/test/plugin/root";
process.env.MY_HARMLESS_VAR = "stay-alive";
try {
const r = await executor.execute({
language: "shell",
code:
'echo "CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT" && echo "MY_HARMLESS_VAR=$MY_HARMLESS_VAR"',
});
assert.equal(r.exitCode, 0);
assert.ok(
r.stdout.includes("CLAUDE_PLUGIN_ROOT=/test/plugin/root"),
`CLAUDE_PLUGIN_ROOT must survive, got: ${r.stdout}`,
);
assert.ok(
r.stdout.includes("MY_HARMLESS_VAR=stay-alive"),
`MY_HARMLESS_VAR must survive, got: ${r.stdout}`,
);
} finally {
if (origRoot === undefined) delete process.env.CLAUDE_PLUGIN_ROOT;
else process.env.CLAUDE_PLUGIN_ROOT = origRoot;
if (origUnrelated === undefined) delete process.env.MY_HARMLESS_VAR;
else process.env.MY_HARMLESS_VAR = origUnrelated;
}
});
test("ERL_AFLAGS and ERL_FLAGS are stripped", async () => {
const origA = process.env.ERL_AFLAGS;
const origF = process.env.ERL_FLAGS;
process.env.ERL_AFLAGS = "-eval 'os:cmd(\"id\")'";
process.env.ERL_FLAGS = "-eval 'os:cmd(\"id\")'";
try {
const r = await executor.execute({
language: "shell",
code: 'echo "ERL_AFLAGS=${ERL_AFLAGS:-unset}" && echo "ERL_FLAGS=${ERL_FLAGS:-unset}"',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("ERL_AFLAGS=unset"), `ERL_AFLAGS should be stripped, got: ${r.stdout}`);
assert.ok(r.stdout.includes("ERL_FLAGS=unset"), `ERL_FLAGS should be stripped, got: ${r.stdout}`);
} finally {
if (origA === undefined) delete process.env.ERL_AFLAGS;
else process.env.ERL_AFLAGS = origA;
if (origF === undefined) delete process.env.ERL_FLAGS;
else process.env.ERL_FLAGS = origF;
}
});
});
describe("Concurrent Execution", () => {
test("5 concurrent JS executions", async () => {
const promises = Array.from({ length: 5 }, (_, i) =>
executor.execute({
language: "javascript",
code: `console.log("concurrent ${i}");`,
}),
);
const all = await Promise.all(promises);
for (const r of all) {
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("concurrent"));
}
});
test("mixed language concurrent execution", async () => {
const promises = [
executor.execute({
language: "javascript",
code: 'console.log("js");',
}),
executor.execute({ language: "shell", code: 'echo "sh"' }),
];
promises.push(
executor.execute({ language: "python", code: 'print("py")' }),
);
const all = await Promise.all(promises);
for (const r of all) {
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.trim().length > 0);
}
});
});
describe("Edge Cases", () => {
test("empty output returns empty string", async () => {
const r = await executor.execute({
language: "javascript",
code: "// no output",
});
assert.equal(r.exitCode, 0);
assert.equal(r.stdout, "");
});
test("multiline output preserved", async () => {
const r = await executor.execute({
language: "javascript",
code: "for (let i = 0; i < 10; i++) console.log(`line ${i}`);",
});
assert.equal(r.exitCode, 0);
assert.equal(r.stdout.trim().split("\n").length, 10);
});
test("stderr captured separately from stdout", async () => {
const r = await executor.execute({
language: "javascript",
code: 'console.error("warning"); console.log("ok");',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("ok"));
assert.ok(r.stderr.includes("warning"));
});
test("special characters in output", async () => {
const r = await executor.execute({
language: "javascript",
code: 'console.log("line1\\nline2\\ttab\\n\\"quoted\\"");',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("line1"));
assert.ok(r.stdout.includes("line2"));
assert.ok(r.stdout.includes('"quoted"'));
});
test("unicode output", async () => {
const r = await executor.execute({
language: "javascript",
code: 'console.log("Hello world");',
});
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes("Hello"));
});
});
describe("Temp Cleanup Resilience", () => {
test("concurrent executions all return valid results (EBUSY resilience)", async () => {
const count = 15;
const promises = Array.from({ length: count }, (_, i) =>
executor.execute({
language: "javascript",
code: `
const fs = require('fs');
const path = require('path');
for (let j = 0; j < 3; j++) {
fs.writeFileSync(path.join(process.cwd(), 'f' + j + '.tmp'), 'data');
}
console.log("ok-${i}");
`,
}),
);
const results = await Promise.all(promises);
for (let i = 0; i < results.length; i++) {
const r = results[i];
assert.equal(typeof r.exitCode, "number", `Execution ${i}: exitCode not a number`);
assert.equal(typeof r.stdout, "string", `Execution ${i}: stdout not a string`);
assert.equal(typeof r.stderr, "string", `Execution ${i}: stderr not a string`);
assert.equal(typeof r.timedOut, "boolean", `Execution ${i}: timedOut not a boolean`);
assert.equal(r.exitCode, 0, `Execution ${i} failed with stderr: ${r.stderr}`);
assert.ok(r.stdout.includes(`ok-${i}`), `Missing output for execution ${i}`);
}
});
test("node runtime accessible from executor shell", async () => {
// Use process.execPath rather than bare 'node' — snap/wrapper installs silently
// exit 0 with no output when the snap wrapper is re-invoked as a subprocess.
const r = await executor.execute({
language: "shell",
code: `"${process.execPath}" --version`,
});
assert.equal(r.exitCode, 0, `node not found in executor env, stderr: ${r.stderr}`);
assert.ok(r.stdout.trim().startsWith("v"), `Expected version string, got: ${r.stdout}`);
});
});
describe("Background Mode", () => {
test("background: true returns partial output with backgrounded flag", async () => {
const bgExecutor = new PolyglotExecutor({ runtimes });
const r = await bgExecutor.execute({
language: "javascript",
code: `console.log("started"); setInterval(() => {}, 1000);`,
timeout: 500,
background: true,
});
assert.equal(r.backgrounded, true, "Should be marked as backgrounded");
assert.equal(r.timedOut, true, "Background detach fires on timeout");
assert.equal(r.exitCode, 0, "Backgrounded processes return exitCode 0");
assert.ok(r.stdout.includes("started"), "Should capture output before detach");
bgExecutor.cleanupBackgrounded();
}, 10_000);
test("cleanupBackgrounded kills detached process", async () => {
const bgExecutor = new PolyglotExecutor({ runtimes });
const r = await bgExecutor.execute({
language: "javascript",
code: `process.stdout.write(String(process.pid)); setInterval(() => {}, 1000);`,
timeout: 500,
background: true,
});
const pid = parseInt(r.stdout.trim(), 10);
assert.ok(pid > 0, `Expected valid PID, got: "${r.stdout}"`);
let alive = false;
try { process.kill(pid, 0); alive = true; } catch { /* ESRCH */ }
assert.equal(alive, true, "Process should be alive before cleanup");
bgExecutor.cleanupBackgrounded();
await new Promise((r) => setTimeout(r, 300));
alive = false;
try { process.kill(pid, 0); alive = true; } catch { /* ESRCH */ }
assert.equal(alive, false, `Process ${pid} should be dead after cleanup`);
}, 10_000);
test("background: true keeps process alive when it writes after detach", async () => {
const bgExecutor = new PolyglotExecutor({ runtimes });
const r = await bgExecutor.execute({
language: "javascript",
code: `
process.stdout.write(String(process.pid));
// Write every 200ms after detach (timeout is 300ms)
const id = setInterval(() => {
process.stdout.write('alive\\n');
}, 200);
// Keep alive for 3 seconds total
setTimeout(() => { clearInterval(id); process.exit(0); }, 3000);
`,
timeout: 300,
background: true,
});
const pid = parseInt(r.stdout.trim(), 10);
assert.ok(pid > 0, `Expected valid PID, got: "${r.stdout}"`);
// Give the process time to write after detach
await new Promise((r) => setTimeout(r, 1000));
let alive = false;
try { process.kill(pid, 0); alive = true; } catch { /* ESRCH */ }
assert.equal(alive, true, `Process ${pid} should still be alive after detach (SIGPIPE bug?)`);
bgExecutor.cleanupBackgrounded();
}, 10_000);
});
describe("hardCapBytes Enforcement", () => {
test("kills process when combined output exceeds byte cap", async () => {
const cappedExecutor = new PolyglotExecutor({
runtimes,
hardCapBytes: 1024,
});
const r = await cappedExecutor.execute({
language: "javascript",
code: `for (let i = 0; i < 10000; i++) console.log("x".repeat(100));`,
timeout: 10_000,
});
assert.ok(r.stderr.includes("output capped at"), "Should indicate cap was hit");
assert.notEqual(r.exitCode, 0, "Process should be killed (non-zero exit)");
}, 15_000);
test("stderr contributes to byte cap", async () => {
const cappedExecutor = new PolyglotExecutor({
runtimes,
hardCapBytes: 1024,
});
const r = await cappedExecutor.execute({
language: "javascript",
code: `for (let i = 0; i < 10000; i++) console.error("e".repeat(100));`,
timeout: 10_000,
});
assert.ok(r.stderr.includes("output capped at"), "stderr should trigger cap");
}, 15_000);
});
describe("Windows Shell Support", () => {
test("shell runtime is always a non-empty string", async () => {
assert.ok(
typeof runtimes.shell === "string" && runtimes.shell.length > 0,
`shell should always be a non-empty string, got: ${runtimes.shell}`,
);
});
test("getAvailableLanguages always includes shell", async () => {
const { getAvailableLanguages } = await import("../src/runtime.js");
const langs = getAvailableLanguages(runtimes);
assert.ok(langs.includes("shell"), `shell should always be in available languages, got: ${langs}`);
});
test("buildCommand returns shell command array", async () => {
const cmd = buildCommand(runtimes, "shell", "/tmp/script.sh");
assert.ok(Array.isArray(cmd) && cmd.length > 0, `Expected non-empty array, got: ${cmd}`);
if (process.platform === "win32" && (cmd[0].toLowerCase().includes("bash") || cmd[0].toLowerCase().endsWith("\\sh.exe"))) {
// Windows + bash → `bash -c "source 'path'"` to dodge MSYS path mangling.
assert.equal(cmd.length, 3, `Expected [bash, -c, source ...], got: ${cmd}`);
assert.equal(cmd[1], "-c");
assert.ok(cmd[2].includes("/tmp/script.sh"), `Expected source clause to reference path, got: ${cmd[2]}`);
} else if (process.platform === "win32" && /powershell|pwsh/i.test(cmd[0])) {
assert.equal(cmd.length, 6, `Expected [powershell, -NoProfile, -ExecutionPolicy, Bypass, -File, path], got: ${cmd}`);
assert.deepEqual(cmd.slice(1, 5), ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"]);
assert.equal(cmd[5], "/tmp/script.sh");
} else {
assert.equal(cmd.length, 2, `Expected [shell, path], got: ${cmd}`);
assert.equal(cmd[1], "/tmp/script.sh");
}
});
// --- Issue #384: hide Windows console + drop .sh extension for shell ---
test("buildSpawnOptions: windowsHide=true on Windows", async () => {
assert.equal(buildSpawnOptions("win32").windowsHide, true);
});
test("buildSpawnOptions: windowsHide=false on macOS/Linux", async () => {
assert.equal(buildSpawnOptions("darwin").windowsHide, false);
assert.equal(buildSpawnOptions("linux").windowsHide, false);
});
test("buildScriptFilename: POSIX shell on Windows has NO extension (avoid .sh file association)", async () => {
assert.equal(buildScriptFilename("shell", "win32"), "script");
assert.equal(buildScriptFilename("shell", "win32", "C:\\Program Files\\Git\\usr\\bin\\bash.exe"), "script");
assert.equal(buildScriptFilename("shell", "win32", "sh"), "script");
});
test("buildScriptFilename: cmd on Windows uses .cmd extension", async () => {
assert.equal(buildScriptFilename("shell", "win32", "cmd.exe"), "script.cmd");
assert.equal(buildScriptFilename("shell", "win32", "C:\\Windows\\System32\\cmd.exe"), "script.cmd");
});
test("buildScriptFilename: PowerShell on Windows uses .ps1 extension", async () => {
assert.equal(buildScriptFilename("shell", "win32", "powershell"), "script.ps1");
assert.equal(buildScriptFilename("shell", "win32", "pwsh"), "script.ps1");
assert.equal(
buildScriptFilename("shell", "win32", "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
"script.ps1",
);
assert.equal(
buildScriptFilename("shell", "win32", "C:\\Program Files\\PowerShell\\7\\pwsh.exe"),
"script.ps1",
);
});
test.runIf(process.platform === "win32")("PowerShell shell runtime executes generated script", async () => {
const powershellRuntimes: RuntimeMap = { ...runtimes, shell: "powershell" };
const powershellExecutor = new PolyglotExecutor({ runtimes: powershellRuntimes });
const r = await powershellExecutor.execute({
language: "shell",
code: 'Write-Output "POWERSHELL_EXECUTOR_OK"',
timeout: 10_000,
});
assert.equal(r.exitCode, 0, `stderr: ${r.stderr}`);
assert.ok(r.stdout.includes("POWERSHELL_EXECUTOR_OK"), `stdout: ${r.stdout}`);
});
test("buildScriptFilename: shell on Unix keeps .sh extension", async () => {
assert.equal(buildScriptFilename("shell", "darwin"), "script.sh");
assert.equal(buildScriptFilename("shell", "linux"), "script.sh");
assert.equal(buildScriptFilename("shell", "linux", "pwsh"), "script.sh");
assert.equal(buildScriptFilename("shell", "darwin", "powershell"), "script.sh");
});
test("buildScriptFilename: non-shell languages keep their extension on Windows", async () => {
assert.equal(buildScriptFilename("python", "win32"), "script.py");
assert.equal(buildScriptFilename("javascript", "win32"), "script.js");
assert.equal(buildScriptFilename("typescript", "win32"), "script.ts");
assert.equal(buildScriptFilename("ruby", "win32"), "script.rb");
assert.equal(buildScriptFilename("go", "win32"), "script.go");
assert.equal(buildScriptFilename("rust", "win32"), "script.rs");
assert.equal(buildScriptFilename("csharp", "win32"), "script.csx");
});
test("buildScriptFilename: non-shell languages keep their extension on Unix", async () => {
assert.equal(buildScriptFilename("python", "linux"), "script.py");
assert.equal(buildScriptFilename("javascript", "darwin"), "script.js");
});
});
// ─────────────────────────────────────────────────────────
// Sibling MCP discovery — issue #559
// /ctx-upgrade must terminate previous-version MCP servers so they don't
// pile up across upgrades. discoverSiblingMcpPids enumerates node procs
// whose argv contains the plugin start.mjs path, excluding self + parent.
// ─────────────────────────────────────────────────────────
describe("discoverSiblingMcpPids (#559)", () => {
test("parses pgrep output, filters own pid + ppid", async () => {
const { discoverSiblingMcpPids } = await import("../src/util/sibling-mcp.js");
const ownPid = 11111;
const ownPpid = 22222;
// Mock pgrep returning four candidate PIDs, two of which are self/parent.
const fakeRunner = (_cmd: string, _args: readonly string[]) =>
`${ownPid}\n${ownPpid}\n33333\n44444\n`;
const pids = discoverSiblingMcpPids({
ownPid,
ownPpid,
platform: "linux",
runCommand: fakeRunner,
});
assert.deepEqual(pids.sort((a, b) => a - b), [33333, 44444]);
});
test("returns empty array when discovery tool is absent (never throws)", async () => {
const { discoverSiblingMcpPids } = await import("../src/util/sibling-mcp.js");
const pids = discoverSiblingMcpPids({
ownPid: 1,
ownPpid: 0,
platform: "linux",
runCommand: () => { throw new Error("ENOENT: pgrep not found"); },
});
assert.deepEqual(pids, []);
});
test("Windows branch parses CIM/WMI ProcessId column", async () => {
const { discoverSiblingMcpPids } = await import("../src/util/sibling-mcp.js");
// PowerShell `Select-Object -ExpandProperty ProcessId` produces newline
// separated integers. Some shells echo a header row — filter must skip it.
const fakePs = (_cmd: string, _args: readonly string[]) =>
"ProcessId\n----------\n55555\n66666\n";
const pids = discoverSiblingMcpPids({
ownPid: 1,
ownPpid: 2,
platform: "win32",
runCommand: fakePs,
});
assert.deepEqual(pids.sort((a, b) => a - b), [55555, 66666]);
});
test("ignores blank lines and non-numeric noise", async () => {
const { discoverSiblingMcpPids } = await import("../src/util/sibling-mcp.js");
const fakeRunner = (_cmd: string, _args: readonly string[]) =>
"\n \nnot-a-pid\n77777\n 88888 \n";
const pids = discoverSiblingMcpPids({
ownPid: 1,
ownPpid: 2,
platform: "linux",
runCommand: fakeRunner,
});
assert.deepEqual(pids.sort((a, b) => a - b), [77777, 88888]);
});
});
// ─────────────────────────────────────────────────────────
// killSiblingMcpServers — issue #559
// SIGTERM with timeout-based escalation to SIGKILL. Reports per-pid
// outcome so cli.ts can surface a human-readable summary.
// ─────────────────────────────────────────────────────────
describe("killSiblingMcpServers (#559)", () => {
test("escalates to SIGKILL after timeout when SIGTERM is ignored", async () => {
const { killSiblingMcpServers } = await import("../src/util/sibling-mcp.js");
// Fake liveness map: pid -> array of remaining alive checks before death.
// pid 1: dies after 1 alive check (responds to SIGTERM).
// pid 2: never dies via SIGTERM, only after SIGKILL is sent.
const aliveCheckCounts = new Map<number, number>([[1, 1], [2, 999]]);
const sigKilled = new Set<number>();
const sigTermSent: number[] = [];
const isAlive = (pid: number): boolean => {
// After SIGKILL, the process is dead.
if (sigKilled.has(pid)) return false;
const remaining = aliveCheckCounts.get(pid) ?? 0;
if (remaining <= 0) return false;
aliveCheckCounts.set(pid, remaining - 1);
return true;
};
const sendSignal = (pid: number, sig: NodeJS.Signals): void => {
if (sig === "SIGKILL") sigKilled.add(pid);
if (sig === "SIGTERM") sigTermSent.push(pid);
};
const report = await killSiblingMcpServers({
pids: [1, 2],
timeoutMs: 50,
pollIntervalMs: 10,
isAlive,
sendSignal,
});
assert.deepEqual(sigTermSent.sort((a, b) => a - b), [1, 2]);
assert.equal(report.terminatedBySigterm, 1);
assert.equal(report.terminatedBySigkill, 1);
assert.equal(report.totalKilled, 2);
});
test("returns zero counts and never throws on empty input", async () => {
const { killSiblingMcpServers } = await import("../src/util/sibling-mcp.js");
const report = await killSiblingMcpServers({
pids: [],
timeoutMs: 10,
pollIntervalMs: 5,
isAlive: () => false,
sendSignal: () => { throw new Error("should not be called"); },
});
assert.equal(report.totalKilled, 0);
assert.equal(report.terminatedBySigterm, 0);
assert.equal(report.terminatedBySigkill, 0);
});
test("swallows ESRCH from sendSignal (process already gone)", async () => {
const { killSiblingMcpServers } = await import("../src/util/sibling-mcp.js");
const report = await killSiblingMcpServers({
pids: [99999999],
timeoutMs: 20,
pollIntervalMs: 5,
isAlive: () => false,
sendSignal: () => {
const e: NodeJS.ErrnoException = new Error("ESRCH") as NodeJS.ErrnoException;
e.code = "ESRCH";
throw e;
},
});
// Process was already dead — counts as 0 since we never observed it alive.
assert.equal(report.totalKilled, 0);
});
});