import type { ProcessEvent } from "@rakazo/adapter-kit"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DockerSandboxProvider, MAX_SANDBOX_ERROR_RESPONSE_BYTES, MAX_SANDBOX_SUCCESS_RESPONSE_BYTES, SCREEN_RELEASE_TIMEOUT_MS, } from "./docker-sandbox.js"; const context = { operationId: "docker-test", traceId: "docker-test", spaceId: "workspace", userId: "user", botId: "bot", screenLeaseId: "run-1:1", signal: new AbortController().signal, }; describe("Docker sandbox", () => { afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); it("sends the bounded timeout to the supervisor and preserves its honest result", async () => { const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => Response.json({ stdout: "partial output\n", stderr: "command timed out after 75 ms\n", code: 124, }), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); const events: ProcessEvent[] = []; for await (const event of provider.execute( { id: "computer", botId: "bot", kind: "docker", providerRef: "computer" }, { argv: ["sleep", "10"], timeoutMs: 75 }, context, )) { events.push(event); } expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({ argv: ["sleep", "10"], cwd: "/home/rakazo", timeoutMs: 75, }); expect(events).toEqual([ { type: "stdout", data: "partial output\n" }, { type: "stderr", data: "command timed out after 75 ms\n" }, { type: "exit", code: 124 }, ]); expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({ "x-rakazo-bot-id": "bot", "x-rakazo-screen-id": "bot", }); expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({ "x-request-id": expect.any(String), traceparent: expect.stringMatching(/^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/), }); }); it("rejects a declared oversized success response without buffering it", async () => { const cancel = vi.fn(); vi.stubGlobal( "fetch", vi.fn( async () => new Response(new ReadableStream({ cancel }), { headers: { "content-length": String(MAX_SANDBOX_SUCCESS_RESPONSE_BYTES + 1) }, }), ), ); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await expect( provider.provision({ botId: "bot", homePath: "/tmp/bot" }, context), ).rejects.toThrow(`sandbox response exceeds ${MAX_SANDBOX_SUCCESS_RESPONSE_BYTES} bytes`); await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce()); }); it("translates a 429 computer limit reached error from the supervisor", async () => { vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ error: "Computer limit reached for space (max: 5)" }, { status: 429 }), ), ); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await expect( provider.provision({ botId: "bot", homePath: "/tmp/bot" }, context), ).rejects.toThrow("Computer limit reached for space (max: 5)"); }); it("stops a streamed file response at the caller-derived encoded limit", async () => { const cancel = vi.fn(); vi.stubGlobal( "fetch", vi.fn( async () => new Response( new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(1029)); }, cancel, }), ), ), ); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await expect( provider.readFile( { id: "computer", botId: "bot", kind: "docker", providerRef: "computer" }, "small.txt", context, { maxBytes: 1 }, ), ).rejects.toThrow("sandbox response exceeds 1028 bytes"); await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce()); }); it("accepts a valid encoded envelope above the generic response cap", async () => { const maxBytes = 13 * 1024 * 1024; const encodedLimit = Math.ceil(maxBytes / 3) * 4 + 1024; vi.stubGlobal( "fetch", vi.fn( async () => new Response('{"content":""}', { headers: { "content-length": String(encodedLimit) }, }), ), ); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await expect( provider.readFile( { id: "computer", botId: "bot", kind: "docker", providerRef: "computer" }, "large.bin", context, { maxBytes }, ), ).resolves.toEqual(new Uint8Array()); }); it("releases this bot's screen assignment through the supervisor", async () => { const fetchMock = vi.fn(async () => new Response(null, { status: 200 })); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await provider.releaseScreen( { id: "computer", botId: "home-bot", kind: "docker", providerRef: "computer" }, context, ); expect(fetchMock).toHaveBeenCalledWith( "http://supervisor.test/computers/computer/screen", expect.objectContaining({ method: "DELETE", headers: expect.objectContaining({ authorization: "Bearer test-token", "x-rakazo-bot-id": "home-bot", "x-rakazo-screen-id": "bot", "x-rakazo-screen-lease-id": "run-1:1", "x-rakazo-space-id": "workspace", }), }), ); }); it("still releases the screen after the run abort signal has fired", async () => { const fetchMock = vi.fn( async (_input: string | URL | Request, _init?: RequestInit) => new Response(null, { status: 404 }), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); const abort = new AbortController(); abort.abort(); await expect( provider.releaseScreen( { id: "computer", botId: "home-bot", kind: "docker", providerRef: "computer" }, { ...context, signal: abort.signal }, ), ).resolves.toBeUndefined(); expect(fetchMock).toHaveBeenCalledOnce(); expect(fetchMock.mock.calls[0]?.[1]?.signal).not.toBe(abort.signal); expect(fetchMock.mock.calls[0]?.[1]?.signal?.aborted).toBe(false); }); it("bounds screen release even when fetch ignores cancellation", async () => { vi.useFakeTimers(); const fetchMock = vi.fn( async (_input: string | URL | Request, _init?: RequestInit) => new Promise(() => undefined), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); const pending = provider.releaseScreen( { id: "computer", botId: "home-bot", kind: "docker", providerRef: "computer" }, context, ); const rejected = expect(pending).rejects.toThrow("sandbox screen release timed out"); await vi.advanceTimersByTimeAsync(SCREEN_RELEASE_TIMEOUT_MS); await rejected; expect(fetchMock.mock.calls[0]?.[1]?.signal?.aborted).toBe(true); }); it("surfaces a supervisor failure from stop and destroy instead of swallowing it", async () => { const fetchMock = vi.fn(async () => Response.json({ error: "unauthorized" }, { status: 401 })); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "stale-token"); const computer = { id: "computer", botId: "bot", kind: "docker", providerRef: "computer", } as const; await expect(provider.stop(computer, context)).rejects.toThrow( 'sandbox stop failed: 401 {"error":"unauthorized"}', ); await expect(provider.destroy(computer, context)).rejects.toThrow( 'sandbox destroy failed: 401 {"error":"unauthorized"}', ); fetchMock.mockImplementation(async () => new Response("boom", { status: 500 })); await expect(provider.stop(computer, context)).rejects.toThrow("sandbox stop failed: 500 boom"); await expect(provider.destroy(computer, context)).rejects.toThrow( "sandbox destroy failed: 500 boom", ); }); it("does not buffer an oversized supervisor error body", async () => { const cancel = vi.fn(); const fetchMock = vi.fn( async () => new Response(new ReadableStream({ cancel }), { status: 500, headers: { "content-length": String(MAX_SANDBOX_ERROR_RESPONSE_BYTES + 1) }, }), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); const computer = { id: "computer", botId: "bot", kind: "docker", providerRef: "computer", } as const; await expect(provider.stop(computer, context)).rejects.toThrow("sandbox stop failed: 500"); await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce()); }); it("stops reading a streamed supervisor error at the byte limit", async () => { const fetchMock = vi.fn( async () => new Response( new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(MAX_SANDBOX_ERROR_RESPONSE_BYTES + 1)); }, }), { status: 500 }, ), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await expect( provider.stop( { id: "computer", botId: "bot", kind: "docker", providerRef: "computer" }, context, ), ).rejects.toThrow("sandbox stop failed: 500"); }); it("treats a computer the supervisor no longer knows as already stopped and destroyed", async () => { const fetchMock = vi.fn(async () => Response.json({ error: "computer not found" }, { status: 404 }), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); const computer = { id: "computer", botId: "bot", kind: "docker", providerRef: "computer", } as const; await expect(provider.stop(computer, context)).resolves.toBeUndefined(); await expect(provider.destroy(computer, context)).resolves.toBeUndefined(); expect(fetchMock).toHaveBeenCalledTimes(2); }); it("asks the supervisor to cancel orphaned run work when releasing a screen", async () => { const fetchMock = vi.fn(async () => Response.json({ ok: true })); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await provider.releaseScreen( { id: "computer-1", botId: "bot", kind: "docker", providerRef: "computer-1" }, { ...context, cancelRunWork: true }, ); expect(fetchMock).toHaveBeenCalledWith( "http://supervisor.test/computers/computer-1/screen", expect.objectContaining({ method: "DELETE", headers: expect.objectContaining({ "x-rakazo-cancel-run-work": "1" }), }), ); }); }); describe("Docker page browser", () => { afterEach(() => vi.unstubAllGlobals()); it("uses the owned computer screen and lease instead of the generic exec endpoint", async () => { const fetchMock = vi.fn(async () => Response.json({ ok: true, title: "Fixture", url: "https://example.test" }), ); vi.stubGlobal("fetch", fetchMock); const provider = new DockerSandboxProvider("http://supervisor.test", "test-token"); await provider.pageBrowser( { id: "computer", botId: "team-home", kind: "docker", providerRef: "computer" }, { command: "snapshot" }, context, ); expect(fetchMock).toHaveBeenCalledWith( "http://supervisor.test/computers/computer/browser", expect.objectContaining({ redirect: "error", signal: context.signal, body: JSON.stringify({ command: "snapshot" }), headers: expect.objectContaining({ "x-rakazo-bot-id": "team-home", "x-rakazo-screen-lease-id": "run-1:1", }), }), ); }); });