evOo-local/packages/testkit/src/computer-replay.docker.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
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.
2026-09-20 17:15:16 +02:00

179 lines
7.0 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import type { AddressInfo } from "node:net";
import path from "node:path";
import { serve } from "@hono/node-server";
import type { ComputerRef } from "@rakazo/adapter-kit";
import {
ComputerBrowserProvider,
DockerSandboxProvider,
FakeSandboxProvider,
} from "@rakazo/adapters";
import { describe, expect, it } from "vitest";
import {
assertContactsExport,
createContactsRecorder,
createContactsReplayBrowser,
executeContactsJourney,
replayContactsRecording,
} from "./computer-recording.js";
import { computerReplayContext, runComputerReplay, waitForReplayFile } from "./computer-replay.js";
import {
CONTACTS_CSV,
CONTACTS_PATH,
EXPORT_RECEIPT_PATH,
installContactsFixture,
} from "./computer-replay-fixture.js";
describe.skipIf(process.env.RUN_COMPUTER_REPLAY_DOCKER !== "1")(
"computer replay with real Docker Chrome and emulated model",
() => {
it("downloads a CSV through real page actions and captures an actual screenshot", async () => {
if (!process.env.DATA_DIR || !process.env.SANDBOX_SUPERVISOR_TOKEN) {
throw new Error(
"Run through the computer-replay CLI to create an isolated supervisor environment",
);
}
const { supervisorApp } = await import("../../../infra/sandboxes/supervisor/src/index.js");
const server = serve({ fetch: supervisorApp.fetch, hostname: "127.0.0.1", port: 0 });
try {
if (!server.listening)
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.once("listening", () => {
server.off("error", reject);
resolve();
});
});
const port = (server.address() as AddressInfo).port;
const sandbox = new DockerSandboxProvider(
`http://127.0.0.1:${port}`,
process.env.SANDBOX_SUPERVISOR_TOKEN,
);
const context = {
...computerReplayContext(),
botId: `replay-${randomUUID()}`,
signal: AbortSignal.timeout(240_000),
};
const homePath = path.join(process.env.DATA_DIR, "homes", context.botId);
await mkdir(homePath, { recursive: true });
let computer: ComputerRef | undefined;
let ownedNetwork: string | undefined;
try {
// Optional escape hatch for a daemon whose automatic address pool is full.
// Docker rejects overlapping subnets; only this test's unique network is touched.
if (process.env.COMPUTER_REPLAY_SUBNET) {
const { computerNetworkNameFor } = await import(
"../../../infra/sandboxes/supervisor/src/computer-spec.js"
);
const name = computerNetworkNameFor(context.botId);
execFileSync(
"docker",
["network", "create", "--subnet", process.env.COMPUTER_REPLAY_SUBNET, name],
{ stdio: "pipe" },
);
ownedNetwork = name;
}
computer = await sandbox.provision({ botId: context.botId, homePath }, context);
await sandbox.prepare(computer, context);
await installContactsFixture(sandbox, computer, context);
if (process.env.COMPUTER_RECORD_OUTPUT) {
if (!process.env.OPENROUTER_API_KEY)
throw new Error("A manual capture requires an OpenRouter key");
const recorder = createContactsRecorder(
sandbox,
new ComputerBrowserProvider({ sandbox }),
computer,
context,
);
try {
await executeContactsJourney(
{
provider: "openrouter",
id: "openai/gpt-5.6-luna",
apiKey: process.env.OPENROUTER_API_KEY,
},
recorder,
context,
);
} finally {
// Retain safe decisions for failed attempts too, without promoting a failed fixture.
console.log(
JSON.stringify({ source: "luna-openrouter-docker", steps: recorder.decisions() }),
);
}
const recording = recorder.recording("luna-openrouter-docker");
await assertContactsExport(sandbox, computer, context);
const offlineSandbox = new FakeSandboxProvider();
const offlineContext = computerReplayContext();
const offlineComputer = await offlineSandbox.provision(
{ botId: "fixture-bot", homePath: "/fixture" },
offlineContext,
);
const offlineBrowser = createContactsReplayBrowser(offlineSandbox, recording);
try {
await replayContactsRecording(
recording,
offlineSandbox,
offlineBrowser,
offlineComputer,
offlineContext,
);
await assertContactsExport(offlineSandbox, offlineComputer, offlineContext);
// Only sanitized intent leaves memory, after both artifact checks pass.
await writeFile(
process.env.COMPUTER_RECORD_OUTPUT,
`${JSON.stringify(recording, null, 2)}\n`,
{ flag: "wx" },
);
} finally {
offlineBrowser.close();
await offlineSandbox.destroy(offlineComputer, offlineContext);
}
return;
}
const result = await runComputerReplay(
sandbox,
new ComputerBrowserProvider({ sandbox }),
computer,
context,
);
expect(result.modelRequests).toBe(9);
// These reads are independent of the agent's reply and tool-result assertions.
expect(await waitForReplayFile(sandbox, computer, context, CONTACTS_PATH)).toBe(
CONTACTS_CSV,
);
expect(await waitForReplayFile(sandbox, computer, context, EXPORT_RECEIPT_PATH)).toBe(
"1",
);
const files = await sandbox.listFiles(computer, "Downloads", context);
expect(files.filter((file) => file.path.endsWith(".csv"))).toHaveLength(1);
} finally {
try {
if (computer)
await sandbox.destroy(computer, { ...context, signal: AbortSignal.timeout(30_000) });
} finally {
if (ownedNetwork) {
// Successful destroy already removes it. A failed provision may leave it behind.
execFileSync("docker", ["network", "rm", "--force", ownedNetwork], {
stdio: "pipe",
timeout: 30_000,
});
}
}
}
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) =>
error && (!("code" in error) || error.code !== "ERR_SERVER_NOT_RUNNING")
? reject(error)
: resolve(),
);
server.closeAllConnections();
});
}
}, 300_000);
},
);