import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import type { ComputerRef, ProcessEvent, SandboxProvider } from "@rakazo/adapter-kit"; import { describe, expect, it } from "vitest"; import { BoxSandboxEmulator } from "./box-emulator.js"; import { DaytonaSandboxEmulator } from "./daytona-emulator.js"; import { DesktopSandboxProvider } from "./desktop-sandbox.js"; import { ManagedSandboxEmulator } from "./e2b-emulator.js"; import { FakeSandboxProvider } from "./fake-sandbox.js"; import { provisionPrepared } from "./sandbox-test-support.js"; const ctx = { operationId: "1", traceId: "1", spaceId: "w", userId: "u", signal: new AbortController().signal, }; async function drain(provider: SandboxProvider, computer: ComputerRef) { let stdout = ""; for await (const event of provider.execute(computer, { argv: ["echo", "graphical-ok"] }, ctx)) { if (event.type === "stdout") stdout += event.data; if (event.type === "exit") expect(event.code).toBe(0); } return stdout; } describe("sandbox conformance", () => { it("runs the same graphical command across fake, managed-provider emulators, and desktop", async () => { const fake = new FakeSandboxProvider(); const managed = new ManagedSandboxEmulator(); const daytona = new DaytonaSandboxEmulator(); const box = new BoxSandboxEmulator(); const desktop = new DesktopSandboxProvider(); const a = await provisionPrepared(fake, { botId: "bot-a", homePath: "/tmp/a" }, ctx); const b = await provisionPrepared(managed, { botId: "bot-b", homePath: "/tmp/b" }, ctx); const c = await provisionPrepared(daytona, { botId: "bot-c", homePath: "/tmp/c" }, ctx); const d = await provisionPrepared(box, { botId: "bot-d", homePath: "/tmp/d" }, ctx); const e = await provisionPrepared(desktop, { botId: "bot-e", homePath: "/tmp/e" }, ctx); const outA = await drain(fake, a); const outB = await drain(managed, b); const outC = await drain(daytona, c); const outD = await drain(box, d); const outE = await drain(desktop, e); expect(outA).toContain("graphical-ok"); expect(outB).toContain("graphical-ok"); expect(outC).toContain("graphical-ok"); expect(outD).toContain("graphical-ok"); expect(outE).toContain("graphical-ok"); expect(new Set([a.id, b.id, c.id, d.id, e.id]).size).toBe(5); await fake.destroy(a, ctx); await managed.destroy(b, ctx); await daytona.destroy(c, ctx); await box.destroy(d, ctx); await desktop.destroy(e, ctx); }); it("offers the same observation, action, and workspace contract across providers", async () => { const providers: SandboxProvider[] = [ new FakeSandboxProvider(), new ManagedSandboxEmulator(), new DaytonaSandboxEmulator(), new BoxSandboxEmulator(), new DesktopSandboxProvider(), ]; for (const [index, provider] of providers.entries()) { const computer = await provisionPrepared( provider, { botId: `portable-${index}`, homePath: `/tmp/portable-${index}` }, ctx, ); await provider.prepare(computer, ctx); await provider.writeFile( computer, { path: "notes/result.txt", content: new TextEncoder().encode("portable") }, ctx, ); expect(await provider.listFiles(computer, "notes", ctx)).toEqual([ { path: "notes/result.txt", kind: "file", size: 8 }, ]); expect( new TextDecoder().decode(await provider.readFile(computer, "notes/result.txt", ctx)), ).toBe("portable"); const binary = Uint8Array.from([0, 255, 1, 128]); await provider.writeFile( computer, { path: "bin/tool", content: binary, executable: true }, ctx, ); expect(await provider.readFile(computer, "bin/tool", ctx)).toEqual(binary); expect(await provider.listFiles(computer, "bin", ctx)).toEqual([ { path: "bin/tool", kind: "file", size: 4, executable: true }, ]); const acted = await provider.act( computer, { actions: [{ kind: "clipboard", text: "visible" }], observe: true }, ctx, ); expect(acted.completed).toBe(1); expect(acted.observation?.image.byteLength).toBeGreaterThan(0); const exported = []; for await (const file of provider.exportWorkspace(computer, ctx)) exported.push(file); expect(exported.map((file) => file.path)).toContain("notes/result.txt"); expect(exported.find((file) => file.path === "bin/tool")).toMatchObject({ content: binary, executable: true, }); await provider.destroy(computer, ctx); } }); it("desktop executor allows paths outside the computer home", async () => { const outside = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-outside-")); const desktop = new DesktopSandboxProvider(); const computer = await desktop.provision({ botId: "grant", homePath: "/tmp/grant" }, ctx); let stdout = ""; let code = 1; for await (const event of desktop.execute( computer, { argv: ["echo", "ok"], cwd: outside }, ctx, )) { if (event.type === "stdout") stdout += event.data; if (event.type === "exit") code = event.code; } expect(code).toBe(0); expect(stdout).toContain("ok"); await desktop.destroy(computer, ctx); rmSync(outside, { recursive: true, force: true }); }); it("desktop executor times out and kills descendants that inherited its pipes", async () => { const root = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-timeout-")); const desktop = new DesktopSandboxProvider({ root }); const computer = await desktop.provision({ botId: "timeout", homePath: "/unused" }, ctx); const marker = path.join(computer.providerRef, "descendant-survived"); const events: ProcessEvent[] = []; const startedAt = Date.now(); for await (const event of desktop.execute( computer, { argv: ["bash", "-lc", "(sleep 0.2; printf survived > descendant-survived) & wait"], timeoutMs: 40, }, ctx, )) { events.push(event); } expect(Date.now() - startedAt).toBeLessThan(1_000); expect(events).toContainEqual({ type: "exit", code: 124 }); expect(events).toContainEqual({ type: "stderr", data: "command timed out after 40 ms\n", }); await new Promise((resolve) => setTimeout(resolve, 300)); expect(() => readFileSync(marker)).toThrow(); await desktop.destroy(computer, ctx); rmSync(root, { recursive: true, force: true }); }); it("desktop executor aborts and kills a running command", async () => { const root = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-abort-")); const desktop = new DesktopSandboxProvider({ root }); const computer = await desktop.provision({ botId: "abort", homePath: "/unused" }, ctx); const controller = new AbortController(); const events: ProcessEvent[] = []; setTimeout(() => controller.abort(), 25); for await (const event of desktop.execute( computer, { argv: ["bash", "-lc", "sleep 10"], timeoutMs: 5_000 }, { ...ctx, signal: controller.signal }, )) { events.push(event); } expect(events).toContainEqual({ type: "exit", code: 130 }); expect(events).toContainEqual({ type: "stderr", data: "command aborted\n" }); await desktop.destroy(computer, ctx); rmSync(root, { recursive: true, force: true }); }); it("treats a repeated destroy as success so a stale deletion retry is safe", async () => { const root = mkdtempSync(path.join(tmpdir(), "rakazo-destroy-idempotent-")); const providers: SandboxProvider[] = [ new FakeSandboxProvider(), new ManagedSandboxEmulator(), new DaytonaSandboxEmulator(), new BoxSandboxEmulator(), new DesktopSandboxProvider({ root }), ]; for (const [index, provider] of providers.entries()) { const computer = await provisionPrepared( provider, { botId: `destroy-twice-${index}`, homePath: `/tmp/destroy-twice-${index}` }, ctx, ); await provider.destroy(computer, ctx); await expect(provider.destroy(computer, ctx)).resolves.toBeUndefined(); } rmSync(root, { recursive: true, force: true }); }); it("reuses one desktop machine per bot", async () => { const root = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-reuse-")); const desktop = new DesktopSandboxProvider({ root }); const first = await desktop.provision({ botId: "stable", homePath: "/unused" }, ctx); const second = await desktop.provision({ botId: "stable", homePath: "/unused" }, ctx); expect(first).toMatchObject({ fresh: true }); expect(second).toMatchObject({ id: first.id, providerRef: first.providerRef, fresh: false }); expect(desktop.boxes.size).toBe(1); await desktop.destroy(second, ctx); rmSync(root, { recursive: true, force: true }); }); it("desktop file writes can follow a symlink or write outside the workspace", async () => { const root = mkdtempSync(path.join(tmpdir(), "rakazo-desktop-symlink-")); const desktop = new DesktopSandboxProvider({ root }); const computer = await desktop.provision({ botId: "symlink", homePath: "/unused" }, ctx); const outside = path.join(root, "outside.txt"); writeFileSync(outside, "before"); symlinkSync(outside, path.join(computer.providerRef, "escape.txt")); await desktop.writeFile(computer, { path: "escape.txt", content: new TextEncoder().encode("after"), }); expect(readFileSync(outside, "utf8")).toBe("after"); await desktop.destroy(computer, ctx); rmSync(root, { recursive: true, force: true }); }); });