Skip to content

Commit bd2512e

Browse files
Merge pull request #1563 from corbitsdev/cl-11355-fix-the-race-in-the-exec-sigterm-fixture-that-flakes-ci
test(exec): wait for the active-run handle in the signal fixture
2 parents ed0565f + ba409cf commit bd2512e

1 file changed

Lines changed: 36 additions & 18 deletions

File tree

‎fixtures/crash-run/simulate-exec-signal.ts‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Spawned as a subprocess by e2e/exec-signal-finalize.test.ts.
22
// Installs process-level signal handlers the way import.meta.main does, then
3-
// calls production runExec. Does not register the active-run handle itself —
4-
// that is the product path under test.
3+
// calls production runExec. The fixture never registers the active-run handle
4+
// itself, because runExec registering it is the product path under test.
55
import { existsSync, readFileSync } from "node:fs";
66
import { join } from "node:path";
77

@@ -20,22 +20,33 @@ const runDir = sessionDir(cwd, sessionId);
2020
const runJsonPath = join(runDir, "run.json");
2121
const turnsUsed = Number(process.env["SIGNAL_TEST_TURNS_USED"] ?? "5");
2222

23-
async function waitForRunningRunJson(): Promise<void> {
23+
async function pollUntil<T>(
24+
probe: () => T | null,
25+
timeoutMs: number,
26+
errorMessage: string,
27+
): Promise<T> {
28+
const deadline = Date.now() + timeoutMs;
2429
for (;;) {
25-
if (existsSync(runJsonPath)) {
26-
try {
27-
const state = JSON.parse(readFileSync(runJsonPath, "utf8")) as {
28-
status?: string;
29-
};
30-
if (state.status === "running") return;
31-
} catch {
32-
// rename/parse race on the first persist
33-
}
34-
}
30+
const value = probe();
31+
if (value !== null) return value;
32+
if (Date.now() >= deadline) throw new Error(errorMessage);
3533
await new Promise((resolve) => setTimeout(resolve, 20));
3634
}
3735
}
3836

37+
function runJsonIsRunning(): true | null {
38+
if (!existsSync(runJsonPath)) return null;
39+
try {
40+
const state = JSON.parse(readFileSync(runJsonPath, "utf8")) as {
41+
status?: string;
42+
};
43+
return state.status === "running" ? true : null;
44+
} catch {
45+
// The first persist can be read mid-rename; retry on the next poll.
46+
return null;
47+
}
48+
}
49+
3950
await withMockedModuleDuring(
4051
import.meta.resolve("../../src/session/assemble-runtime.js"),
4152
(real: typeof import("../../src/session/assemble-runtime.js")) => ({
@@ -64,11 +75,18 @@ await withMockedModuleDuring(
6475
sessionId,
6576
} as unknown as Config;
6677
void runExec(config);
67-
await waitForRunningRunJson();
68-
const handle = getActiveRun();
69-
if (handle === null) {
70-
throw new Error("runExec did not register an active-run handle");
71-
}
78+
await pollUntil(
79+
runJsonIsRunning,
80+
10_000,
81+
`run.json at ${runJsonPath} did not reach status "running"`,
82+
);
83+
// runExec registers the handle after saveState resolves, so run.json can
84+
// show "running" on disk before the handle exists.
85+
const handle = await pollUntil(
86+
getActiveRun,
87+
5_000,
88+
"runExec did not register an active-run handle",
89+
);
7290
// Advance the live counter the way a mid-run snapshot would, without
7391
// reading run.json on the signal path under test.
7492
syncRunStateHandle(handle, {

0 commit comments

Comments
 (0)