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.
179 lines
7.0 KiB
TypeScript
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);
|
|
},
|
|
);
|