'use strict'; /** * javascript_execute — sandbox execution, isolation guarantees, and the * timeout/serialization behaviour of the worker bridge. * * These tests spawn real worker threads and run real vm2 sandboxes: the * security properties (a runaway loop, blocked host modules) are exactly * what a mocked-out vm2 would fail to verify. * * Note: no test passes allow_network=true, because that path requires an * interactive TTY approval prompt. Network plumbing is exercised separately * in a scratch worker, not here. */ import test from 'node:test'; import assert from 'node:assert/strict'; import { Worker } from 'worker_threads'; import { fileURLToPath } from 'url'; import { javascriptExecuteTool } from '../src/tools/javascript.js'; const WORKER_PATH = fileURLToPath(new URL('../src/tools/javascript-worker.js', import.meta.url)); /** Run a snippet through the real tool, normalizing errors to a message. */ const exec = (args) => javascriptExecuteTool.invoke(args).then( (result) => ({ ok: true, result }), (err) => ({ ok: false, error: err.message }), ); /** * Drive the worker directly, bypassing the approval layer, so worker-level * behaviour (timeouts, serialization) can be asserted without a TTY. */ function runInWorker(code, { network = false, timeout = 5000, cap = 20_000 } = {}) { return new Promise((resolve) => { const worker = new Worker(WORKER_PATH, { workerData: { code, root: process.cwd(), vmTimeout: timeout, bufferLimit: 32 * 1024 * 1024, network, }, }); const bail = setTimeout(() => { worker.terminate(); resolve({ timedOut: true }); }, cap); worker.on('message', (msg) => { clearTimeout(bail); worker.terminate(); resolve(msg); }); worker.on('error', (err) => { clearTimeout(bail); worker.terminate(); resolve({ error: err.message }); }); }); } // ─── Result serialization ─────────────────────────────────────────────────── test('a snippet returns its value via module.exports', async () => { const { ok, result } = await exec({ code: 'module.exports = 6 * 7;' }); assert.ok(ok, 'expected the call to succeed'); assert.equal(result.result, 42); }); test('nested objects and arrays survive the worker boundary', async () => { // vm2 returns Proxy objects; structured-clone rejects them outright, so // the worker rebuilds plain host containers before posting. const { ok, result } = await exec({ code: 'module.exports = { a: 1, b: [1, 2, { c: true }], d: "x" };', }); assert.ok(ok); assert.deepEqual(result.result, { a: 1, b: [1, 2, { c: true }], d: 'x' }); }); test('a returned array is not reported as unserializable', async () => { const { ok, result } = await exec({ code: 'module.exports = [[1, 2], [3, { d: 4 }]];' }); assert.ok(ok); assert.deepEqual(result.result, [[1, 2], [3, { d: 4 }]]); }); test('a returned function degrades to a type marker instead of throwing', async () => { const { ok, result } = await exec({ code: 'module.exports = function f() {};' }); assert.ok(ok); assert.deepEqual(result.result, { type: 'function' }); }); test('values nested deeper than the depth limit are summarized, not dropped silently', async () => { const { ok, result } = await exec({ code: 'module.exports = { a: { b: { c: { d: { e: 1 } } } } };' }); assert.ok(ok); assert.equal(result.result.a.b.c.d.type, 'object'); assert.match(result.result.a.b.c.d.note, /deeply nested/); }); // ─── Console capture ──────────────────────────────────────────────────────── test('console output is captured and returned rather than printed to the terminal', async () => { const { ok, result } = await exec({ code: 'console.log("plain"); console.error("boom"); module.exports = 1;', }); assert.ok(ok); assert.equal(result.stdout, 'plain\nerror: boom'); assert.equal(result.consoleLines, 2); }); test('console output beyond max_output is capped and flagged', async () => { const { ok, result } = await exec({ code: 'for (let i = 0; i < 100; i++) console.log("x".repeat(20));', max_output: 60, }); assert.ok(ok); assert.equal(result.truncated, true); assert.ok(result.stdout.length <= 60, 'stdout must respect the cap'); assert.match(result.truncationNote, /60 chars/); }); // ─── Errors ───────────────────────────────────────────────────────────────── test('a thrown error surfaces as a ToolError carrying name, message and stack', async () => { const { ok, error } = await exec({ code: 'null.x;' }); assert.equal(ok, false); assert.match(error, /Cannot read properties of null/); assert.match(error, /TypeError/); }); test('a syntax error is reported rather than crashing the worker', async () => { const { ok, error } = await exec({ code: 'const = ;' }); assert.equal(ok, false); assert.ok(error.length > 0); }); test('blank code is rejected before any worker is spawned', async () => { const { ok, error } = await exec({ code: ' ' }); assert.equal(ok, false); assert.match(error, /non-empty string/); }); test('a non-positive timeout is rejected', async () => { const { ok, error } = await exec({ code: '1;', timeout: 0 }); assert.equal(ok, false); assert.match(error, /positive integer/); }); // ─── Timeout: the reason this runs in a worker at all ─────────────────────── test('an infinite loop is killed at the timeout instead of hanging', async () => { // The core guarantee. NodeVM does not honor vm2's own `timeout`, so // without the worker being terminated this call never returns. const started = Date.now(); const { ok, result } = await exec({ code: 'while (true) {}', timeout: 1000 }); const elapsed = Date.now() - started; assert.ok(ok, 'a timeout is a normal outcome, not a thrown error'); assert.equal(result.timedOut, true); assert.ok(elapsed < 8000, `expected a prompt kill, took ${elapsed} ms`); assert.match(result.note, /timeout/); }); test('the worker-level hard kill also stops a blocking loop', async () => { const report = await runInWorker('while (true) {}', { cap: 3000 }); assert.equal(report.timedOut, true); }); // ─── Isolation ────────────────────────────────────────────────────────────── test('built-in host modules are not reachable', async () => { for (const mod of ['fs', 'child_process', 'os', 'node:fs']) { const { ok, error } = await exec({ code: `require(${JSON.stringify(mod)});` }); assert.equal(ok, false, `${mod} must not be requireable`); assert.match(error, /Cannot find module/); } }); test('node_modules packages are not reachable', async () => { const { ok, error } = await exec({ code: 'require("chalk");' }); assert.equal(ok, false); assert.match(error, /Cannot find module/); }); test('vm2 itself is not reachable (no nesting escape)', async () => { const { ok, error } = await exec({ code: 'require("vm2");' }); assert.equal(ok, false); assert.match(error, /Cannot find module/); }); test('fetch is absent unless network access was granted', async () => { const offline = await runInWorker('module.exports = typeof fetch;'); assert.equal(offline.result, 'undefined'); }); // ─── Async ────────────────────────────────────────────────────────────────── test('a promise assigned to module.exports is awaited', async () => { const { ok, result } = await exec({ code: 'async function f() { return 41 + 1; }\nmodule.exports = f();', }); assert.ok(ok); assert.equal(result.result, 42); }); test('a rejected promise is reported as an error', async () => { const { ok, error } = await exec({ code: 'module.exports = (async () => { throw new Error("async boom"); })();', }); assert.equal(ok, false); assert.match(error, /async boom/); }); test('a promise that never settles is bounded by the timeout', async () => { const report = await runInWorker('module.exports = new Promise(() => {});', { timeout: 800, cap: 6000, }); assert.match(String(report.error ?? ''), /did not settle/); });