/**
* Unit tests for Code Mode Sandbox
*
* Tests sandbox creation, code execution, timeout handling,
* and resource cleanup.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { CodeModeSandbox, SandboxPool } from "../sandbox.js";
describe("CodeModeSandbox", () => {
let sandbox: CodeModeSandbox;
beforeEach(() => {
sandbox = CodeModeSandbox.create();
});
afterEach(() => {
sandbox.dispose();
});
describe("create()", () => {
it("should create a sandbox instance", () => {
expect(sandbox).toBeDefined();
expect(sandbox.isHealthy()).toBe(true);
});
it("should accept custom options", () => {
const customSandbox = CodeModeSandbox.create({
timeoutMs: 5000,
memoryLimitMb: 64,
});
expect(customSandbox).toBeDefined();
expect(customSandbox.isHealthy()).toBe(true);
customSandbox.dispose();
});
});
describe("execute()", () => {
it("should execute simple code and return result", async () => {
const result = await sandbox.execute("return 1 + 2;", {});
expect(result.success).toBe(true);
expect(result.result).toBe(3);
expect(result.metrics.wallTimeMs).toBeGreaterThanOrEqual(0);
});
it("should execute async code", async () => {
const result = await sandbox.execute(
"return await Promise.resolve(42);",
{},
);
expect(result.success).toBe(true);
expect(result.result).toBe(42);
});
it("should provide access to pg bindings", async () => {
const mockBindings = {
core: {
listTables: async () => [{ name: "users" }, { name: "products" }],
},
};
const result = await sandbox.execute(
"const tables = await pg.core.listTables(); return tables.length;",
mockBindings,
);
expect(result.success).toBe(true);
expect(result.result).toBe(2);
});
it("should handle execution errors", async () => {
const result = await sandbox.execute(
'throw new Error("test error");',
{},
);
expect(result.success).toBe(false);
expect(result.error).toContain("test error");
});
it("should block access to require", async () => {
const result = await sandbox.execute(
'const fs = require("fs"); return fs;',
{},
);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
it("should block access to process", async () => {
const result = await sandbox.execute("return process.env;", {});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
it("should return error after disposed", async () => {
sandbox.dispose();
const result = await sandbox.execute("return 1;", {});
expect(result.success).toBe(false);
expect(result.error).toContain("disposed");
});
it("should provide execution metrics", async () => {
const result = await sandbox.execute('return "test";', {});
expect(result.metrics).toBeDefined();
expect(result.metrics.wallTimeMs).toBeGreaterThanOrEqual(0);
expect(result.metrics.cpuTimeMs).toBeGreaterThanOrEqual(0);
expect(typeof result.metrics.memoryUsedMb).toBe("number");
});
});
describe("dispose()", () => {
it("should mark sandbox as unhealthy", () => {
expect(sandbox.isHealthy()).toBe(true);
sandbox.dispose();
expect(sandbox.isHealthy()).toBe(false);
});
it("should be idempotent", () => {
sandbox.dispose();
sandbox.dispose(); // Should not throw
expect(sandbox.isHealthy()).toBe(false);
});
});
});
describe("SandboxPool", () => {
let pool: SandboxPool;
beforeEach(() => {
pool = new SandboxPool({ minInstances: 2, maxInstances: 5 });
pool.initialize();
});
afterEach(() => {
pool.dispose();
});
describe("initialize()", () => {
it("should create minimum instances", () => {
const stats = pool.getStats();
expect(stats.available).toBe(2);
expect(stats.inUse).toBe(0);
});
});
describe("acquire()", () => {
it("should return a sandbox from the pool", () => {
const sandbox = pool.acquire();
expect(sandbox).toBeDefined();
expect(sandbox.isHealthy()).toBe(true);
const stats = pool.getStats();
expect(stats.inUse).toBe(1);
});
it("should create new sandboxes if pool is empty", () => {
// Acquire more than minInstances
const s1 = pool.acquire();
const s2 = pool.acquire();
const s3 = pool.acquire();
expect(pool.getStats().inUse).toBe(3);
pool.release(s1);
pool.release(s2);
pool.release(s3);
});
it("should throw when pool is exhausted", () => {
const sandboxes: CodeModeSandbox[] = [];
for (let i = 0; i < 5; i++) {
sandboxes.push(pool.acquire());
}
expect(() => pool.acquire()).toThrowError(/exhausted/);
sandboxes.forEach((s) => pool.release(s));
});
it("should throw after disposed", () => {
pool.dispose();
expect(() => pool.acquire()).toThrowError(/disposed/);
});
});
describe("release()", () => {
it("should return sandbox to pool", () => {
const sandbox = pool.acquire();
expect(pool.getStats().inUse).toBe(1);
pool.release(sandbox);
expect(pool.getStats().inUse).toBe(0);
expect(pool.getStats().available).toBeGreaterThan(0);
});
it("should ignore sandboxes not from pool", () => {
const external = CodeModeSandbox.create();
pool.release(external); // Should not throw
external.dispose();
});
});
describe("execute()", () => {
it("should execute code and return result", async () => {
const result = await pool.execute("return 5 * 5;", {});
expect(result.success).toBe(true);
expect(result.result).toBe(25);
});
it("should return sandbox to pool after execution", async () => {
await pool.execute("return 1;", {});
const stats = pool.getStats();
expect(stats.inUse).toBe(0);
});
it("should return sandbox to pool even on error", async () => {
await pool.execute('throw new Error("fail");', {});
const stats = pool.getStats();
expect(stats.inUse).toBe(0);
});
});
describe("dispose()", () => {
it("should clear all sandboxes", () => {
pool.dispose();
const stats = pool.getStats();
expect(stats.available).toBe(0);
expect(stats.inUse).toBe(0);
});
it("should be idempotent", () => {
pool.dispose();
pool.dispose(); // Should not throw
});
});
describe("cleanup edge cases", () => {
it("should dispose sandbox on release when pool is disposed", async () => {
const sandbox = pool.acquire();
expect(sandbox.isHealthy()).toBe(true);
pool.dispose();
// Pool dispose also disposes all sandboxes including in-use ones
expect(sandbox.isHealthy()).toBe(false);
});
it("should dispose unhealthy sandbox on release", () => {
const sandbox = pool.acquire();
sandbox.dispose(); // Make it unhealthy
pool.release(sandbox); // Should not throw
});
});
describe("getStats()", () => {
it("should return correct pool statistics", () => {
const stats = pool.getStats();
expect(stats).toHaveProperty("available");
expect(stats).toHaveProperty("inUse");
expect(stats).toHaveProperty("max");
expect(stats.max).toBe(5);
});
it("should track in-use count correctly", () => {
const s1 = pool.acquire();
expect(pool.getStats().inUse).toBe(1);
const s2 = pool.acquire();
expect(pool.getStats().inUse).toBe(2);
pool.release(s1);
expect(pool.getStats().inUse).toBe(1);
pool.release(s2);
expect(pool.getStats().inUse).toBe(0);
});
});
});
describe("CodeModeSandbox Console", () => {
let sandbox: CodeModeSandbox;
beforeEach(() => {
sandbox = CodeModeSandbox.create();
});
afterEach(() => {
sandbox.dispose();
});
describe("console output capture", () => {
it("should capture console.log output", async () => {
await sandbox.execute('console.log("test message");', {});
const output = sandbox.getConsoleOutput();
expect(output).toContain("test message");
});
it("should capture console.warn output with prefix", async () => {
await sandbox.execute('console.warn("warning");', {});
const output = sandbox.getConsoleOutput();
expect(
output.some(
(line) => line.includes("[WARN]") && line.includes("warning"),
),
).toBe(true);
});
it("should capture console.error output with prefix", async () => {
await sandbox.execute('console.error("error message");', {});
const output = sandbox.getConsoleOutput();
expect(
output.some(
(line) => line.includes("[ERROR]") && line.includes("error message"),
),
).toBe(true);
});
it("should capture console.info output with prefix", async () => {
await sandbox.execute('console.info("info message");', {});
const output = sandbox.getConsoleOutput();
expect(
output.some(
(line) => line.includes("[INFO]") && line.includes("info message"),
),
).toBe(true);
});
it("should serialize objects in console output", async () => {
await sandbox.execute('console.log({ key: "value" });', {});
const output = sandbox.getConsoleOutput();
expect(
output.some(
(line) => line.includes('"key"') && line.includes('"value"'),
),
).toBe(true);
});
it("should handle multiple console arguments", async () => {
await sandbox.execute('console.log("a", "b", "c");', {});
const output = sandbox.getConsoleOutput();
expect(output.some((line) => line.includes("a b c"))).toBe(true);
});
it("should clear console output", async () => {
await sandbox.execute('console.log("first");', {});
expect(sandbox.getConsoleOutput().length).toBeGreaterThan(0);
sandbox.clearConsoleOutput();
expect(sandbox.getConsoleOutput()).toEqual([]);
});
});
});
describe("CodeModeSandbox Timeout Handling", () => {
it("should detect timeout errors", async () => {
const sandbox = CodeModeSandbox.create({ timeoutMs: 50 });
// Execute code that will timeout
const result = await sandbox.execute(
`
let i = 0;
while(true) { i++; }
return i;
`,
{},
);
expect(result.success).toBe(false);
expect(result.error).toContain("timeout");
sandbox.dispose();
});
});
describe("SandboxPool Cleanup", () => {
it("should skip unhealthy sandboxes during acquire", () => {
const pool = new SandboxPool({ minInstances: 2, maxInstances: 5 });
pool.initialize();
// Acquire and dispose to make unhealthy
const s1 = pool.acquire();
s1.dispose();
pool.release(s1);
// Next acquire should skip the unhealthy one
const s2 = pool.acquire();
expect(s2.isHealthy()).toBe(true);
pool.release(s2);
pool.dispose();
});
it("should not return excess sandboxes to pool", () => {
const pool = new SandboxPool({ minInstances: 1, maxInstances: 3 });
pool.initialize();
// Acquire all 3
const sandboxes = [pool.acquire(), pool.acquire(), pool.acquire()];
// Release all - pool should only keep up to max
for (const s of sandboxes) {
pool.release(s);
}
// Available should not exceed max
expect(pool.getStats().available).toBeLessThanOrEqual(3);
pool.dispose();
});
it("should dispose sandbox when released after pool is disposed (line 302-303)", () => {
const pool = new SandboxPool({ minInstances: 1, maxInstances: 3 });
pool.initialize();
// Acquire a sandbox
const sandbox = pool.acquire();
expect(sandbox.isHealthy()).toBe(true);
// Dispose pool while sandbox is in use
pool.dispose();
// The sandbox should now be disposed (pool.dispose disposes all in-use)
// but let's also verify the release path explicitly
expect(sandbox.isHealthy()).toBe(false);
});
it("should dispose unhealthy sandbox found during acquire loop (line 276)", () => {
const pool = new SandboxPool({ minInstances: 3, maxInstances: 5 });
pool.initialize();
// Acquire all available sandboxes and make them unhealthy
const s1 = pool.acquire();
const s2 = pool.acquire();
const s3 = pool.acquire();
// Dispose them to make unhealthy
s1.dispose();
s2.dispose();
s3.dispose();
// Release them back to pool (they go back as unhealthy)
pool.release(s1);
pool.release(s2);
pool.release(s3);
// Now acquire should find unhealthy sandboxes and dispose them,
// then create a new one
const newSandbox = pool.acquire();
expect(newSandbox.isHealthy()).toBe(true);
pool.release(newSandbox);
pool.dispose();
});
});
describe("SandboxPool Cleanup Interval", () => {
it("should trigger cleanup on interval and remove unhealthy sandboxes (lines 333-344)", async () => {
// Create pool with very short idle timeout for testing
const pool = new SandboxPool(
{ minInstances: 1, maxInstances: 5, idleTimeoutMs: 50 },
{ timeoutMs: 10000 },
);
pool.initialize();
// Pool should start with minInstances
expect(pool.getStats().available).toBe(1);
// Acquire and release to add more to pool
const s1 = pool.acquire();
const s2 = pool.acquire();
pool.release(s1);
pool.release(s2);
expect(pool.getStats().available).toBe(2);
// Wait for cleanup interval to trigger (which trims to minInstances)
await new Promise((resolve) => setTimeout(resolve, 100));
// After cleanup, should be trimmed back to minInstances
expect(pool.getStats().available).toBe(1);
pool.dispose();
});
it("should trim excess sandboxes during cleanup to minInstances (lines 347-349)", async () => {
const pool = new SandboxPool({
minInstances: 1,
maxInstances: 10,
idleTimeoutMs: 30,
});
pool.initialize();
// Create many sandboxes
const sandboxes = [];
for (let i = 0; i < 5; i++) {
sandboxes.push(pool.acquire());
}
// Release them all back
for (const s of sandboxes) {
pool.release(s);
}
// Should have 5 available now
expect(pool.getStats().available).toBe(5);
// Wait for cleanup to trim
await new Promise((resolve) => setTimeout(resolve, 80));
// Should be trimmed to minInstances (1)
expect(pool.getStats().available).toBe(1);
pool.dispose();
});
it("should remove unhealthy sandboxes during cleanup (lines 335-341)", async () => {
const pool = new SandboxPool({
minInstances: 2,
maxInstances: 5,
idleTimeoutMs: 30,
});
pool.initialize();
// Make one of the initial sandboxes unhealthy by acquiring and disposing it
const s1 = pool.acquire();
const s2 = pool.acquire();
s1.dispose(); // Make unhealthy
pool.release(s1);
pool.release(s2);
// Wait for cleanup
await new Promise((resolve) => setTimeout(resolve, 80));
// After cleanup, unhealthy should be removed, then trimmed to min
// Only healthy ones should remain
const remaining = pool.getStats().available;
expect(remaining).toBeLessThanOrEqual(2);
pool.dispose();
});
});