fix(e2e): wait for Parallels update cleanup

This commit is contained in:
Vincent Koc
2026-06-16 04:19:29 +02:00
parent 11a0ad10e9
commit d89ab2c014
2 changed files with 113 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
// Update Job Timeout script supports OpenClaw repository automation.
interface TimedUpdateJobOptions {
abortSettleMs?: number;
append(this: void, chunk: string): void;
label: string;
run(this: void, context: { signal: AbortSignal }): Promise<void> | void;
@@ -9,6 +10,7 @@ interface TimedUpdateJobOptions {
}
export async function runTimedUpdateJob({
abortSettleMs = 2_500,
append,
label,
run,
@@ -20,19 +22,35 @@ export async function runTimedUpdateJob({
const controller = new AbortController();
const timeoutMessage = `${label} update timed out after ${timeoutDescription}`;
let timeout: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
const runOutcome = Promise.resolve()
.then(() => run({ signal: controller.signal }))
.then(
() => ({ status: "pass" as const }),
(error: unknown) => ({ error, status: "fail" as const }),
);
const timeoutPromise = new Promise<"timeout">((resolve) => {
timeout = setTimeout(() => {
timedOut = true;
append(`${timeoutMessage}\n`);
controller.abort(new Error(timeoutMessage));
reject(new Error(timeoutMessage));
resolve("timeout");
}, timeoutMs);
});
try {
await Promise.race([Promise.resolve(run({ signal: controller.signal })), timeoutPromise]);
const outcome = await Promise.race([runOutcome, timeoutPromise]);
if (outcome === "timeout") {
await waitForAbortSettle(runOutcome, abortSettleMs);
await writeLog();
return 1;
}
if (outcome.status === "fail") {
append(`${outcome.error instanceof Error ? outcome.error.message : String(outcome.error)}\n`);
await writeLog();
return 1;
}
await writeLog();
return timedOut ? 1 : 0;
return 0;
} catch (error) {
if (!timedOut) {
append(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -45,3 +63,13 @@ export async function runTimedUpdateJob({
}
}
}
async function waitForAbortSettle<T>(runOutcome: Promise<T>, ms: number): Promise<T | undefined> {
return await new Promise((resolve) => {
const timeout = setTimeout(resolve, ms);
void runOutcome.then((outcome) => {
clearTimeout(timeout);
resolve(outcome);
});
});
}

View File

@@ -1,4 +1,7 @@
// Parallels Update Job Timeout tests cover parallels update job timeout script behavior.
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import { runTimedUpdateJob } from "../../scripts/e2e/parallels/update-job-timeout.ts";
@@ -76,6 +79,7 @@ describe("Parallels update job timeout", () => {
const writeLog = vi.fn(async () => undefined);
const result = runTimedUpdateJob({
abortSettleMs: 1,
append: (chunk) => chunks.push(chunk),
label: "Windows",
run: () => new Promise(() => {}),
@@ -84,7 +88,7 @@ describe("Parallels update job timeout", () => {
writeLog,
});
await vi.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(1001);
await expect(result).resolves.toBe(1);
expect(chunks).toEqual(["Windows update timed out after 1s\n"]);
expect(writeLog).toHaveBeenCalledTimes(1);
@@ -121,4 +125,80 @@ describe("Parallels update job timeout", () => {
expect(chunks).toEqual(["Linux update timed out after 1s plus cleanup backstop\n"]);
expect(writeLog).toHaveBeenCalledTimes(1);
});
it("waits for abort-aware cleanup before writing the job log", async () => {
vi.useFakeTimers();
const events: string[] = [];
const result = runTimedUpdateJob({
abortSettleMs: 250,
append: (chunk) => events.push(chunk.trim()),
label: "macOS",
run: ({ signal }) =>
new Promise<void>((resolve) => {
signal.addEventListener(
"abort",
() => {
events.push("abort");
setTimeout(() => {
events.push("cleanup");
resolve();
}, 25);
},
{ once: true },
);
}),
timeoutDescription: "1s plus cleanup backstop",
timeoutMs: 1000,
writeLog: async () => {
events.push("writeLog");
},
});
await vi.advanceTimersByTimeAsync(1025);
await expect(result).resolves.toBe(1);
expect(events).toEqual([
"macOS update timed out after 1s plus cleanup backstop",
"abort",
"cleanup",
"writeLog",
]);
});
it("keeps the process alive long enough to write logs for hung runners", () => {
const moduleUrl = pathToFileURL(
path.resolve("scripts/e2e/parallels/update-job-timeout.ts"),
).href;
const probe = `
import { runTimedUpdateJob } from ${JSON.stringify(moduleUrl)};
const events = [];
const result = await runTimedUpdateJob({
abortSettleMs: 25,
append: (chunk) => events.push(chunk.trim()),
label: "Linux",
run: () => new Promise(() => {}),
timeoutDescription: "10ms",
timeoutMs: 10,
writeLog: async () => events.push("writeLog"),
});
console.log(JSON.stringify({ events, result }));
`;
const child = spawnSync(
process.execPath,
["--import", "tsx", "--input-type=module", "--eval", probe],
{
cwd: process.cwd(),
encoding: "utf8",
timeout: 5_000,
},
);
expect(child.stderr).toBe("");
expect(child.status).toBe(0);
expect(JSON.parse(child.stdout)).toEqual({
events: ["Linux update timed out after 10ms", "writeLog"],
result: 1,
});
});
});