mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { chmod, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
import { expect, it, onTestFinished } from "vitest";
|
|
|
|
import { fakeBuildComputers as setUp } from "../../fixtures/fakeAgentEnvComputer.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
it("joins a start still in progress when the owner closes, and refuses later starts", async () => {
|
|
const { computers, runner, start } = await setUp(
|
|
{},
|
|
{ FAKE_DEPLOY_SECONDS: "30" },
|
|
);
|
|
const order: string[] = [];
|
|
const pending = start().then((result) => {
|
|
order.push("start settled");
|
|
return result;
|
|
});
|
|
while ((await runner.notes("runs.jsonl")).length === 0) await delay(50);
|
|
await computers.closeAll();
|
|
order.push("closeAll returned");
|
|
expect(order).toEqual(["start settled", "closeAll returned"]);
|
|
const started = await pending;
|
|
expect(started.ok).toBe(false);
|
|
if (!started.ok) expect(started.error._tag).toBe("AnalysisCancelledError");
|
|
// Shutdown cancelled the deploy, whose own interruption tore it down.
|
|
expect(await runner.notes("interrupted.jsonl")).toHaveLength(1);
|
|
const later = await start();
|
|
expect(later.ok).toBe(false);
|
|
expect(await runner.notes("runs.jsonl")).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps a failed start whose teardown is unconfirmed, so close and shutdown can retry it", async () => {
|
|
const state = await createTestTempDirectory("fake-docker-");
|
|
const down = join(state, "docker-down");
|
|
const docker = join(state, "docker");
|
|
await writeFile(down, "");
|
|
await writeFile(
|
|
docker,
|
|
`#!/bin/sh\n[ -e '${down}' ] && { echo "Cannot connect to the Docker daemon" >&2; exit 1; }\nexit 0\n`,
|
|
);
|
|
await chmod(docker, 0o755);
|
|
const { computers, runner, start } = await setUp(
|
|
{ readinessFails: true },
|
|
{ FAKE_TEARDOWN_FAILS: "1", REA_DOCKER_COMMAND: docker },
|
|
);
|
|
const started = await start();
|
|
if (started.ok) throw new Error("readiness failed, so the start must fail");
|
|
// The primary failure stays the error; the unconfirmed teardown rides on it.
|
|
expect(started.error.userMessage).toContain("Xvfb exited");
|
|
expect(started.error.cleanupIncomplete).toBe(true);
|
|
expect(started.error.cleanup?.reason).toContain(
|
|
"Cannot connect to the Docker daemon",
|
|
);
|
|
const id = started.error.diagnostics?.computer_id;
|
|
if (typeof id !== "string") throw new Error("the failure names its owner");
|
|
// The retained owner retries the same sandbox; it cannot be driven.
|
|
expect((await computers.close(id)).ok).toBe(false);
|
|
expect(
|
|
(
|
|
await computers.act(
|
|
{ computer_id: id, action: "wait" },
|
|
new AbortController().signal,
|
|
)
|
|
).ok,
|
|
).toBe(false);
|
|
await rm(down);
|
|
await computers.closeAll();
|
|
expect(await computers.close(id)).toMatchObject({ ok: false });
|
|
expect(await runner.notes("teardowns.jsonl")).toHaveLength(1);
|
|
});
|
|
|
|
it("waits for a command the computer answers after fetch's wait for headers ends", async () => {
|
|
const home = await createTestTempDirectory("fake-exec-home-");
|
|
const { computers, start } = await setUp({ exec: { home, bin: home } });
|
|
onTestFinished(() => computers.closeAll());
|
|
const started = await start();
|
|
if (!started.ok) throw started.error;
|
|
const steps = computers.steps(started.value.value.computer_id);
|
|
if (steps === undefined) throw new Error("the started computer is held");
|
|
// fetch's dispatcher with undici's 300 s wait for headers cut to 200 ms, which fires after about 1 s.
|
|
const key = Symbol.for("undici.globalDispatcher.1");
|
|
const global = globalThis as Record<symbol, unknown>;
|
|
const dispatcher = global[key] as {
|
|
readonly constructor: new (options: object) => unknown;
|
|
};
|
|
global[key] = new dispatcher.constructor({ headersTimeout: 200 });
|
|
try {
|
|
expect(await steps.computer.exec(["sleep", "3"], 10)).toMatchObject({
|
|
exit_code: 0,
|
|
timed_out: false,
|
|
});
|
|
} finally {
|
|
global[key] = dispatcher;
|
|
}
|
|
});
|
|
|
|
it("cancels a call in progress and the one waiting behind it when the owner closes", async () => {
|
|
const app = {
|
|
name: "notes",
|
|
state: {},
|
|
seed: {},
|
|
methods: [],
|
|
unreadyAfterReset: 600,
|
|
};
|
|
const { computers, runner, start } = await setUp({ app });
|
|
const started = await start();
|
|
if (!started.ok) throw started.error;
|
|
const act = (action: "reset" | "screenshot") =>
|
|
computers.act(
|
|
{ computer_id: started.value.value.computer_id, action },
|
|
new AbortController().signal,
|
|
);
|
|
const reset = act("reset");
|
|
while (app.methods.length === 0) await delay(50);
|
|
const waiting = act("screenshot");
|
|
await computers.closeAll();
|
|
for (const result of await Promise.all([reset, waiting]))
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { _tag: "AnalysisCancelledError" },
|
|
});
|
|
expect(await runner.notes("teardowns.jsonl")).toHaveLength(1);
|
|
});
|