/**
* Tests for IsolatedVMSandbox
*
* Tests the isolated-vm based sandbox which provides true V8 Isolate separation.
*
* Note: In Jest, we use a mock for isolated-vm because the native module
* doesn't work well with Jest's ESM module resolution. The actual functionality
* is tested via manual integration tests.
*
* The mock always reports isAvailable() as false, so the detailed execution
* tests are skipped. This ensures the CodeExecutor properly falls back to
* other sandbox implementations.
*/
import { IsolatedVMSandbox, createIsolatedVMSandbox } from '../../src/code-mode/sandbox/isolated-vm-sandbox.js';
// In Jest with mock, isAvailable will return false
// This is expected - we're testing that the class handles unavailability gracefully
const isAvailable = IsolatedVMSandbox.isAvailable();
// Conditionally run tests
const describeIfAvailable = isAvailable ? describe : describe.skip;
describe('IsolatedVMSandbox', () => {
beforeAll(() => {
if (!isAvailable) {
console.log('isolated-vm not available (using mock), skipping execution tests');
}
});
describe('class structure', () => {
it('should have isAvailable static method', () => {
expect(typeof IsolatedVMSandbox.isAvailable).toBe('function');
});
it('should report availability status as boolean', () => {
expect(typeof IsolatedVMSandbox.isAvailable()).toBe('boolean');
});
it('should have createIsolatedVMSandbox factory function', () => {
expect(typeof createIsolatedVMSandbox).toBe('function');
});
it('should create sandbox instance with config', () => {
const sandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 64,
});
expect(sandbox).toBeInstanceOf(IsolatedVMSandbox);
});
});
describeIfAvailable('code execution', () => {
let sandbox: InstanceType<typeof IsolatedVMSandbox>;
beforeEach(() => {
sandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 64,
});
});
it('should execute simple code and return result', async () => {
const result = await sandbox.execute(
'return 1 + 1;',
[],
async () => null
);
expect(result.error).toBeUndefined();
expect(result.result).toBe(2);
});
it('should capture console.log output', async () => {
const result = await sandbox.execute(
`
console.log("Hello");
console.log("World");
return "done";
`,
[],
async () => null
);
expect(result.error).toBeUndefined();
expect(result.logs).toContain('Hello');
expect(result.logs).toContain('World');
});
it('should handle async code', async () => {
const result = await sandbox.execute(
`
const promise = new Promise(resolve => {
setTimeout(() => resolve(42), 10);
});
return await promise;
`,
[],
async () => null
);
expect(result.error).toBeUndefined();
expect(result.result).toBe(42);
});
it('should call tools via callback', async () => {
const mockToolExecutor = jest.fn().mockResolvedValue({ value: 'tool result' });
const tools = [
{ name: 'test:echo', description: 'Echo tool' },
];
const result = await sandbox.execute(
`
const response = await test.echo({ message: "hello" });
return response;
`,
tools,
mockToolExecutor
);
expect(result.error).toBeUndefined();
expect(mockToolExecutor).toHaveBeenCalledWith('test:echo', { message: 'hello' });
expect(result.result).toEqual({ value: 'tool result' });
});
it('should handle tool errors gracefully', async () => {
const mockToolExecutor = jest.fn().mockRejectedValue(new Error('Tool failed'));
const tools = [
{ name: 'test:fail', description: 'Failing tool' },
];
const result = await sandbox.execute(
`
try {
await test.fail({});
return "should not reach";
} catch (e) {
return "caught: " + e.message;
}
`,
tools,
mockToolExecutor
);
expect(result.error).toBeUndefined();
expect(result.result).toContain('caught:');
expect(result.result).toContain('Tool failed');
});
it('should enforce timeout', async () => {
const shortTimeoutSandbox = createIsolatedVMSandbox({
timeout: 100,
memoryLimit: 64,
});
const result = await shortTimeoutSandbox.execute(
`
// Infinite loop
while (true) {}
`,
[],
async () => null
);
expect(result.error).toBeDefined();
expect(result.error).toContain('timeout');
});
it('should enforce memory limits', async () => {
const lowMemorySandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 8, // Very low memory limit
});
const result = await lowMemorySandbox.execute(
`
// Try to allocate a lot of memory
const arrays = [];
for (let i = 0; i < 1000000; i++) {
arrays.push(new Array(10000).fill(i));
}
return arrays.length;
`,
[],
async () => null
);
// Should either error or be terminated
expect(result.error).toBeDefined();
});
});
describeIfAvailable('security isolation', () => {
let sandbox: InstanceType<typeof IsolatedVMSandbox>;
beforeEach(() => {
sandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 64,
});
});
it('should not have access to require', async () => {
const result = await sandbox.execute(
`
try {
const fs = require('fs');
return "should not reach";
} catch (e) {
return "blocked: " + e.message;
}
`,
[],
async () => null
);
expect(result.error).toBeDefined();
});
it('should not have access to process', async () => {
const result = await sandbox.execute(
`
try {
return process.env.PATH;
} catch (e) {
return "blocked";
}
`,
[],
async () => null
);
// Either error or "blocked" is acceptable
if (!result.error) {
expect(result.result).toBe('blocked');
}
});
it('should not have access to global Node.js APIs', async () => {
const result = await sandbox.execute(
`
const hasBuffer = typeof Buffer !== 'undefined';
const hasSetImmediate = typeof setImmediate !== 'undefined';
const hasProcess = typeof process !== 'undefined';
return { hasBuffer, hasSetImmediate, hasProcess };
`,
[],
async () => null
);
expect(result.error).toBeUndefined();
// In isolated-vm, these should not be defined
const r = result.result as { hasBuffer: boolean; hasSetImmediate: boolean; hasProcess: boolean };
expect(r.hasProcess).toBe(false);
});
it('should isolate between executions', async () => {
// First execution sets a global
await sandbox.execute(
`
global.secret = "sensitive data";
return true;
`,
[],
async () => null
);
// Create new sandbox - should not see previous global
const newSandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 64,
});
const result = await newSandbox.execute(
`
return typeof global.secret === 'undefined' ? "isolated" : global.secret;
`,
[],
async () => null
);
expect(result.error).toBeUndefined();
expect(result.result).toBe('isolated');
});
});
describeIfAvailable('multiple tools', () => {
it('should handle multiple tool namespaces', async () => {
const sandbox = createIsolatedVMSandbox({
timeout: 5000,
memoryLimit: 64,
});
const mockToolExecutor = jest.fn()
.mockImplementation((name: string, _params: unknown) => {
if (name === 'github:list_repos') return Promise.resolve(['repo1', 'repo2']);
if (name === 'slack:send_message') return Promise.resolve({ ok: true });
return Promise.resolve(null);
});
const tools = [
{ name: 'github:list_repos', description: 'List repos' },
{ name: 'slack:send_message', description: 'Send message' },
];
const result = await sandbox.execute(
`
const repos = await github.list_repos({});
const sent = await slack.send_message({ text: "Found " + repos.length + " repos" });
return { repos, sent };
`,
tools,
mockToolExecutor
);
expect(result.error).toBeUndefined();
expect(mockToolExecutor).toHaveBeenCalledTimes(2);
const r = result.result as { repos: string[]; sent: { ok: boolean } };
expect(r.repos).toEqual(['repo1', 'repo2']);
expect(r.sent).toEqual({ ok: true });
});
});
});