/** * Nightly comprehensive node binary integration tests. * * Covers all node CLI behaviors through the kernel: stdout, stderr, * exit codes, error types, delayed output, stdin pipes, VFS access, * cross-runtime child_process, --version, and no-args behavior. * * Each scenario is tested via kernel.exec() (non-PTY path) and key * stdout/error scenarios are also verified through TerminalHarness * (interactive PTY path). * * Gracefully skipped when WASM binaries are not built. */ import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';import { tmpdir } from 'node:os';import path from 'node:path';import { describe, it, expect, afterEach, vi } from 'vitest';import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, TerminalHarness, createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, COMMANDS_DIR,} from '@rivet-dev/agentos-vm-test-harness';import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; // A cold debug-sidecar boot can exceed Vitest's five-second default on the// self-hosted CI runner; runtime operation deadlines still detect real hangs.vi.setConfig({ testTimeout: 15_000 }); const skipReason = skipUnlessWasmBuilt(); /** brush-shell interactive prompt. */const PROMPT = 'sh-0.4$ '; /** * Find a line in the screen output that exactly matches the expected text. * Excludes lines containing the command echo (prompt line). */function findOutputLine(screen: string, expected: string): string | undefined { return screen.split('\n').find( (l) => l.trim() === expected && !l.includes(PROMPT), );} function decodeChunks(chunks: Uint8Array[]): string { const decoder = new TextDecoder(); return chunks.map((chunk) => decoder.decode(chunk)).join("");} // ---------------------------------------------------------------------------// kernel.exec() -- stdout// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec stdout', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node -e console.log produces stdout with exit 0', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "console.log(\'hello\')"'); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('hello'); }); it('node -e setTimeout delayed output appears', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec( 'node -e "setTimeout(()=>console.log(\'delayed\'),100)"', ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('delayed'); }, 10_000);}); describeIf(!skipReason, 'node binary: spawn callback routing', () => { let ctxA: IntegrationKernelResult; let ctxB: IntegrationKernelResult; afterEach(async () => { await ctxA?.dispose(); await ctxB?.dispose(); }); it('concurrent kernels keep stdout callbacks isolated per VM marker', async () => { ctxA = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); ctxB = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const stdoutA: Uint8Array[] = []; const stdoutB: Uint8Array[] = []; const stderrA: Uint8Array[] = []; const stderrB: Uint8Array[] = []; const procA = ctxA.kernel.spawn('node', ['-e', "console.log('VM_A_MARKER')"], { onStdout: (chunk) => stdoutA.push(chunk), onStderr: (chunk) => stderrA.push(chunk), }); const procB = ctxB.kernel.spawn('node', ['-e', "console.log('VM_B_MARKER')"], { onStdout: (chunk) => stdoutB.push(chunk), onStderr: (chunk) => stderrB.push(chunk), }); const [exitA, exitB] = await Promise.all([procA.wait(), procB.wait()]); expect(exitA).toBe(0); expect(exitB).toBe(0); const stdoutAText = decodeChunks(stdoutA); const stdoutBText = decodeChunks(stdoutB); expect(stdoutAText).toContain('VM_A_MARKER'); expect(stdoutAText).not.toContain('VM_B_MARKER'); expect(stdoutBText).toContain('VM_B_MARKER'); expect(stdoutBText).not.toContain('VM_A_MARKER'); expect(decodeChunks(stderrA)).toBe(''); expect(decodeChunks(stderrB)).toBe(''); }, 15_000);}); // ---------------------------------------------------------------------------// kernel.exec() -- exit codes// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec exit codes', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node -e process.exit(42) returns exit code 42', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "process.exit(42)"'); expect(result.exitCode).toBe(42); }); it('node -e process.exit(0) returns exit code 0', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "process.exit(0)"'); expect(result.exitCode).toBe(0); });}); // ---------------------------------------------------------------------------// kernel.exec() -- stderr and error types// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec stderr', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node -e console.error routes to stderr', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "console.error(\'err\')"'); expect(result.stderr).toContain('err'); expect(result.exitCode).toBe(0); }); it('node -e syntax error returns SyntaxError on stderr', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "({" '); expect(result.exitCode).not.toBe(0); expect(result.stderr).toMatch(/SyntaxError|Unexpected/); }); it('node -e ReferenceError on undefined variable', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "unknownVar"'); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain('ReferenceError'); }); it('node -e throw new Error returns message on stderr', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node -e "throw new Error(\'boom\')"'); expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain('boom'); });}); // ---------------------------------------------------------------------------// kernel.exec() -- stdin// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec stdin', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node -e reads from stdin pipe when data provided', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const code = [ 'let d = "";', 'process.stdin.setEncoding("utf8");', 'process.stdin.on("data", c => d += c);', 'process.stdin.on("end", () => console.log(d.trim()));', ].join(' '); const result = await ctx.kernel.exec(`echo "piped-input" | node -e '${code}'`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('piped-input'); }, 15_000);}); // ---------------------------------------------------------------------------// kernel.exec() -- VFS access// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec VFS access', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node -e fs.readdirSync("/") returns VFS root listing', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec( 'node -e "console.log(require(\'fs\').readdirSync(\'/\').join(\',\'))"', ); expect(result.exitCode).toBe(0); // VFS root should contain at least /bin and /tmp expect(result.stdout).toContain('bin'); expect(result.stdout).toContain('tmp'); });}); // ---------------------------------------------------------------------------// kernel.exec() -- cross-runtime child_process// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec child_process', () => { let ctx: IntegrationKernelResult; let tempDir: string | undefined; afterEach(async () => { await ctx?.dispose(); if (tempDir) { await rm(tempDir, { recursive: true, force: true }); tempDir = undefined; } }); it('node -e execSync("echo sub") captures child stdout', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const code = 'console.log(require("child_process").execSync("echo sub").toString().trim())'; const result = await ctx.kernel.exec(`node -e '${code}'`); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('sub'); }, 15_000); it('node -e spawnSync resolves shebang-backed node_modules/.bin commands through JavaScript runtime', async () => { tempDir = await mkdtemp(path.join(tmpdir(), 'kernel-node-bin-')); await writeFile( path.join(tempDir, 'package.json'), JSON.stringify({ name: 'node-bin-repro', private: true }), ); await mkdir(path.join(tempDir, 'node_modules', 'hello-pkg', 'bin'), { recursive: true, }); await writeFile( path.join(tempDir, 'node_modules', 'hello-pkg', 'bin', 'hello.js'), [ '#!/usr/bin/env node', "console.log(`hello ${process.argv.slice(2).join(' ')}`);", ].join('\n'), ); await mkdir(path.join(tempDir, 'node_modules', '.bin'), { recursive: true }); await symlink( '../hello-pkg/bin/hello.js', path.join(tempDir, 'node_modules', '.bin', 'hello-js'), ); const vfs = new NodeFileSystem({ root: tempDir }); const kernel = createKernel({ filesystem: vfs, cwd: '/' }); await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); await kernel.mount(createNodeRuntime()); ctx = { kernel, vfs, dispose: () => kernel.dispose() }; const guestScriptPath = '/tmp/spawn-bin-check.js'; const code = [ "const { spawnSync } = require('child_process');", "const env = { ...process.env, PATH: `/node_modules/.bin:${process.env.PATH}` };", "const result = spawnSync('hello-js', ['from-child'], { encoding: 'utf8', env });", "console.log(JSON.stringify({ status: result.status, stdout: result.stdout, stderr: result.stderr }));", ].join(' '); await ctx.kernel.writeFile(guestScriptPath, code); const result = await ctx.kernel.exec(`node ${guestScriptPath}`); expect(result.exitCode).toBe(0); const payload = JSON.parse(result.stdout.trim()) as { status: number | null; stdout: string; stderr: string; }; expect(payload.status).toBe(0); expect(payload.stdout.trim()).toBe('hello from-child'); expect(payload.stderr).toBe(''); }, 20_000);}); // ---------------------------------------------------------------------------// kernel.exec() -- node --version// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec --version', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node --version outputs semver pattern', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const result = await ctx.kernel.exec('node --version'); expect(result.exitCode).toBe(0); // Node version format: vNN.NN.NN expect(result.stdout.trim()).toMatch(/^v\d+\.\d+\.\d+/); });}); // ---------------------------------------------------------------------------// kernel.exec() -- node with no args + closed stdin// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: exec no args', () => { let ctx: IntegrationKernelResult; afterEach(async () => { await ctx?.dispose(); }); it('node with no args and closed stdin exits cleanly', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); // Pipe empty input so stdin is immediately closed const result = await ctx.kernel.exec('echo -n "" | node', { timeout: 10_000 }); // Should exit without hanging. Any exit code is acceptable. // (real Node exits 0 in this case) expect(typeof result.exitCode).toBe('number'); }, 15_000);}); // ---------------------------------------------------------------------------// TerminalHarness (PTY path) -- stdout verification// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: terminal stdout', () => { let harness: TerminalHarness; let ctx: IntegrationKernelResult; afterEach(async () => { await harness?.dispose(); await ctx?.dispose(); }); it('node -e console.log output visible on terminal', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); harness = new TerminalHarness(ctx.kernel); await harness.waitFor(PROMPT); await harness.type('node -e "console.log(\'MARKER\')"\n'); await harness.waitFor(PROMPT, 2, 10_000); const screen = harness.screenshotTrimmed(); expect(findOutputLine(screen, 'MARKER')).toBeDefined(); }, 15_000); it('node -e delayed output visible on terminal', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); harness = new TerminalHarness(ctx.kernel); await harness.waitFor(PROMPT); await harness.type('node -e "setTimeout(()=>console.log(\'LATE\'),100)"\n'); await harness.waitFor(PROMPT, 2, 10_000); const screen = harness.screenshotTrimmed(); expect(findOutputLine(screen, 'LATE')).toBeDefined(); }, 15_000);}); // ---------------------------------------------------------------------------// TerminalHarness (PTY path) -- stderr verification// --------------------------------------------------------------------------- describeIf(!skipReason, 'node binary: terminal stderr', () => { let harness: TerminalHarness; let ctx: IntegrationKernelResult; afterEach(async () => { await harness?.dispose(); await ctx?.dispose(); }); it('node -e ReferenceError visible on terminal', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); harness = new TerminalHarness(ctx.kernel); await harness.waitFor(PROMPT); await harness.type('node -e "unknownVar"\n'); await harness.waitFor(PROMPT, 2, 10_000); const screen = harness.screenshotTrimmed(); expect(screen).toContain('ReferenceError'); }, 15_000); it('node -e throw Error visible on terminal', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); const stderrChunks: Uint8Array[] = []; const proc = ctx.kernel.spawn('node', ['-e', "throw new Error('boom')"], { onStderr: (chunk) => stderrChunks.push(chunk), }); const exitCode = await proc.wait(); expect(exitCode).not.toBe(0); expect(decodeChunks(stderrChunks)).toContain('boom'); }, 15_000); it('node -e SyntaxError visible on terminal', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); harness = new TerminalHarness(ctx.kernel); await harness.waitFor(PROMPT); await harness.type('node -e "({"\n'); await harness.waitFor(PROMPT, 2, 10_000); const screen = harness.screenshotTrimmed(); expect(screen).toMatch(/SyntaxError|Unexpected/); }, 15_000);});
/**
* Nightly comprehensive node binary integration tests.
*
* Covers all node CLI behaviors through the kernel: stdout, stderr,
* exit codes, error types, delayed output, stdin pipes, VFS access,
* cross-runtime child_process, --version, and no-args behavior.
*
* Each scenario is tested via kernel.exec() (non-PTY path) and key
* stdout/error scenarios are also verified through TerminalHarness
* (interactive PTY path).
*
* Gracefully skipped when WASM binaries are not built.
*/
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, it, expect, afterEach, vi } from 'vitest';
import {
describeIf,
createIntegrationKernel,
skipUnlessWasmBuilt,
TerminalHarness,
createKernel,
createNodeRuntime,
createWasmVmRuntime,
NodeFileSystem,
COMMANDS_DIR,
} from '@rivet-dev/agentos-vm-test-harness';
import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness';
// A cold debug-sidecar boot can exceed Vitest's five-second default on the
// self-hosted CI runner; runtime operation deadlines still detect real hangs.
vi.setConfig({ testTimeout: 15_000 });
const skipReason = skipUnlessWasmBuilt();
/** brush-shell interactive prompt. */
const PROMPT = 'sh-0.4$ ';
/**
* Find a line in the screen output that exactly matches the expected text.
* Excludes lines containing the command echo (prompt line).
*/
function findOutputLine(screen: string, expected: string): string | undefined {
return screen.split('\n').find(
(l) => l.trim() === expected && !l.includes(PROMPT),
);
}
function decodeChunks(chunks: Uint8Array[]): string {
const decoder = new TextDecoder();
return chunks.map((chunk) => decoder.decode(chunk)).join("");
}
// ---------------------------------------------------------------------------
// kernel.exec() -- stdout
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec stdout', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node -e console.log produces stdout with exit 0', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "console.log(\'hello\')"');
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('hello');
});
it('node -e setTimeout delayed output appears', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec(
'node -e "setTimeout(()=>console.log(\'delayed\'),100)"',
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('delayed');
}, 10_000);
});
describeIf(!skipReason, 'node binary: spawn callback routing', () => {
let ctxA: IntegrationKernelResult;
let ctxB: IntegrationKernelResult;
afterEach(async () => {
await ctxA?.dispose();
await ctxB?.dispose();
});
it('concurrent kernels keep stdout callbacks isolated per VM marker', async () => {
ctxA = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
ctxB = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const stdoutA: Uint8Array[] = [];
const stdoutB: Uint8Array[] = [];
const stderrA: Uint8Array[] = [];
const stderrB: Uint8Array[] = [];
const procA = ctxA.kernel.spawn('node', ['-e', "console.log('VM_A_MARKER')"], {
onStdout: (chunk) => stdoutA.push(chunk),
onStderr: (chunk) => stderrA.push(chunk),
});
const procB = ctxB.kernel.spawn('node', ['-e', "console.log('VM_B_MARKER')"], {
onStdout: (chunk) => stdoutB.push(chunk),
onStderr: (chunk) => stderrB.push(chunk),
});
const [exitA, exitB] = await Promise.all([procA.wait(), procB.wait()]);
expect(exitA).toBe(0);
expect(exitB).toBe(0);
const stdoutAText = decodeChunks(stdoutA);
const stdoutBText = decodeChunks(stdoutB);
expect(stdoutAText).toContain('VM_A_MARKER');
expect(stdoutAText).not.toContain('VM_B_MARKER');
expect(stdoutBText).toContain('VM_B_MARKER');
expect(stdoutBText).not.toContain('VM_A_MARKER');
expect(decodeChunks(stderrA)).toBe('');
expect(decodeChunks(stderrB)).toBe('');
}, 15_000);
});
// ---------------------------------------------------------------------------
// kernel.exec() -- exit codes
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec exit codes', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node -e process.exit(42) returns exit code 42', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "process.exit(42)"');
expect(result.exitCode).toBe(42);
});
it('node -e process.exit(0) returns exit code 0', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "process.exit(0)"');
expect(result.exitCode).toBe(0);
});
});
// ---------------------------------------------------------------------------
// kernel.exec() -- stderr and error types
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec stderr', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node -e console.error routes to stderr', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "console.error(\'err\')"');
expect(result.stderr).toContain('err');
expect(result.exitCode).toBe(0);
});
it('node -e syntax error returns SyntaxError on stderr', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "({" ');
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/SyntaxError|Unexpected/);
});
it('node -e ReferenceError on undefined variable', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "unknownVar"');
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('ReferenceError');
});
it('node -e throw new Error returns message on stderr', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node -e "throw new Error(\'boom\')"');
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('boom');
});
});
// ---------------------------------------------------------------------------
// kernel.exec() -- stdin
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec stdin', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node -e reads from stdin pipe when data provided', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const code = [
'let d = "";',
'process.stdin.setEncoding("utf8");',
'process.stdin.on("data", c => d += c);',
'process.stdin.on("end", () => console.log(d.trim()));',
].join(' ');
const result = await ctx.kernel.exec(`echo "piped-input" | node -e '${code}'`);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('piped-input');
}, 15_000);
});
// ---------------------------------------------------------------------------
// kernel.exec() -- VFS access
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec VFS access', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node -e fs.readdirSync("/") returns VFS root listing', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec(
'node -e "console.log(require(\'fs\').readdirSync(\'/\').join(\',\'))"',
);
expect(result.exitCode).toBe(0);
// VFS root should contain at least /bin and /tmp
expect(result.stdout).toContain('bin');
expect(result.stdout).toContain('tmp');
});
});
// ---------------------------------------------------------------------------
// kernel.exec() -- cross-runtime child_process
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec child_process', () => {
let ctx: IntegrationKernelResult;
let tempDir: string | undefined;
afterEach(async () => {
await ctx?.dispose();
if (tempDir) {
await rm(tempDir, { recursive: true, force: true });
tempDir = undefined;
}
});
it('node -e execSync("echo sub") captures child stdout', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const code =
'console.log(require("child_process").execSync("echo sub").toString().trim())';
const result = await ctx.kernel.exec(`node -e '${code}'`);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('sub');
}, 15_000);
it('node -e spawnSync resolves shebang-backed node_modules/.bin commands through JavaScript runtime', async () => {
tempDir = await mkdtemp(path.join(tmpdir(), 'kernel-node-bin-'));
await writeFile(
path.join(tempDir, 'package.json'),
JSON.stringify({ name: 'node-bin-repro', private: true }),
);
await mkdir(path.join(tempDir, 'node_modules', 'hello-pkg', 'bin'), {
recursive: true,
});
await writeFile(
path.join(tempDir, 'node_modules', 'hello-pkg', 'bin', 'hello.js'),
[
'#!/usr/bin/env node',
"console.log(`hello ${process.argv.slice(2).join(' ')}`);",
].join('\n'),
);
await mkdir(path.join(tempDir, 'node_modules', '.bin'), { recursive: true });
await symlink(
'../hello-pkg/bin/hello.js',
path.join(tempDir, 'node_modules', '.bin', 'hello-js'),
);
const vfs = new NodeFileSystem({ root: tempDir });
const kernel = createKernel({ filesystem: vfs, cwd: '/' });
await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] }));
await kernel.mount(createNodeRuntime());
ctx = { kernel, vfs, dispose: () => kernel.dispose() };
const guestScriptPath = '/tmp/spawn-bin-check.js';
const code = [
"const { spawnSync } = require('child_process');",
"const env = { ...process.env, PATH: `/node_modules/.bin:${process.env.PATH}` };",
"const result = spawnSync('hello-js', ['from-child'], { encoding: 'utf8', env });",
"console.log(JSON.stringify({ status: result.status, stdout: result.stdout, stderr: result.stderr }));",
].join(' ');
await ctx.kernel.writeFile(guestScriptPath, code);
const result = await ctx.kernel.exec(`node ${guestScriptPath}`);
expect(result.exitCode).toBe(0);
const payload = JSON.parse(result.stdout.trim()) as {
status: number | null;
stdout: string;
stderr: string;
};
expect(payload.status).toBe(0);
expect(payload.stdout.trim()).toBe('hello from-child');
expect(payload.stderr).toBe('');
}, 20_000);
});
// ---------------------------------------------------------------------------
// kernel.exec() -- node --version
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec --version', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node --version outputs semver pattern', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const result = await ctx.kernel.exec('node --version');
expect(result.exitCode).toBe(0);
// Node version format: vNN.NN.NN
expect(result.stdout.trim()).toMatch(/^v\d+\.\d+\.\d+/);
});
});
// ---------------------------------------------------------------------------
// kernel.exec() -- node with no args + closed stdin
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: exec no args', () => {
let ctx: IntegrationKernelResult;
afterEach(async () => {
await ctx?.dispose();
});
it('node with no args and closed stdin exits cleanly', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
// Pipe empty input so stdin is immediately closed
const result = await ctx.kernel.exec('echo -n "" | node', { timeout: 10_000 });
// Should exit without hanging. Any exit code is acceptable.
// (real Node exits 0 in this case)
expect(typeof result.exitCode).toBe('number');
}, 15_000);
});
// ---------------------------------------------------------------------------
// TerminalHarness (PTY path) -- stdout verification
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: terminal stdout', () => {
let harness: TerminalHarness;
let ctx: IntegrationKernelResult;
afterEach(async () => {
await harness?.dispose();
await ctx?.dispose();
});
it('node -e console.log output visible on terminal', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
harness = new TerminalHarness(ctx.kernel);
await harness.waitFor(PROMPT);
await harness.type('node -e "console.log(\'MARKER\')"\n');
await harness.waitFor(PROMPT, 2, 10_000);
const screen = harness.screenshotTrimmed();
expect(findOutputLine(screen, 'MARKER')).toBeDefined();
}, 15_000);
it('node -e delayed output visible on terminal', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
harness = new TerminalHarness(ctx.kernel);
await harness.waitFor(PROMPT);
await harness.type('node -e "setTimeout(()=>console.log(\'LATE\'),100)"\n');
await harness.waitFor(PROMPT, 2, 10_000);
const screen = harness.screenshotTrimmed();
expect(findOutputLine(screen, 'LATE')).toBeDefined();
}, 15_000);
});
// ---------------------------------------------------------------------------
// TerminalHarness (PTY path) -- stderr verification
// ---------------------------------------------------------------------------
describeIf(!skipReason, 'node binary: terminal stderr', () => {
let harness: TerminalHarness;
let ctx: IntegrationKernelResult;
afterEach(async () => {
await harness?.dispose();
await ctx?.dispose();
});
it('node -e ReferenceError visible on terminal', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
harness = new TerminalHarness(ctx.kernel);
await harness.waitFor(PROMPT);
await harness.type('node -e "unknownVar"\n');
await harness.waitFor(PROMPT, 2, 10_000);
const screen = harness.screenshotTrimmed();
expect(screen).toContain('ReferenceError');
}, 15_000);
it('node -e throw Error visible on terminal', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
const stderrChunks: Uint8Array[] = [];
const proc = ctx.kernel.spawn('node', ['-e', "throw new Error('boom')"], {
onStderr: (chunk) => stderrChunks.push(chunk),
});
const exitCode = await proc.wait();
expect(exitCode).not.toBe(0);
expect(decodeChunks(stderrChunks)).toContain('boom');
}, 15_000);
it('node -e SyntaxError visible on terminal', async () => {
ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] });
harness = new TerminalHarness(ctx.kernel);
await harness.waitFor(PROMPT);
await harness.type('node -e "({"\n');
await harness.waitFor(PROMPT, 2, 10_000);
const screen = harness.screenshotTrimmed();
expect(screen).toMatch(/SyntaxError|Unexpected/);
}, 15_000);
});