Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
242 lines
9.5 KiB
TypeScript
242 lines
9.5 KiB
TypeScript
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 refuses paths outside the computer home", async () => {
|
|
const desktop = new DesktopSandboxProvider();
|
|
const computer = await desktop.provision({ botId: "grant", homePath: "/tmp/grant" }, ctx);
|
|
let stderr = "";
|
|
let code = 0;
|
|
for await (const event of desktop.execute(
|
|
computer,
|
|
{ argv: ["echo", "nope"], cwd: "/etc" },
|
|
ctx,
|
|
)) {
|
|
if (event.type === "stderr") stderr += event.data;
|
|
if (event.type === "exit") code = event.code;
|
|
}
|
|
expect(code).toBe(1);
|
|
expect(stderr).toMatch(/outside this computer's home/i);
|
|
await desktop.destroy(computer, ctx);
|
|
});
|
|
|
|
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 do not follow a final symlink 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 expect(
|
|
desktop.writeFile(computer, {
|
|
path: "escape.txt",
|
|
content: new TextEncoder().encode("after"),
|
|
}),
|
|
).rejects.toThrow();
|
|
expect(readFileSync(outside, "utf8")).toBe("before");
|
|
|
|
await desktop.destroy(computer, ctx);
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
});
|