diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts index 733702f288ef..75de3e48619a 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts @@ -22,17 +22,30 @@ function isProcessRunning(pid: number): boolean { } } -async function waitForFile(pathToCheck: string, timeoutMs: number): Promise { +async function waitForPidFile(pathToCheck: string, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { - await readFile(pathToCheck, "utf8"); - return; - } catch { - await sleep(25); - } + const value = (await readFile(pathToCheck, "utf8")).trim(); + const pid = Number(value); + if (/^[1-9]\d*$/u.test(value) && Number.isSafeInteger(pid)) { + return pid; + } + } catch {} + await sleep(25); } - throw new Error(`Timed out waiting for ${pathToCheck}`); + throw new Error(`Timed out waiting for a PID in ${pathToCheck}`); +} + +async function waitForProcessExit(pid: number, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isProcessRunning(pid)) { + return; + } + await sleep(25); + } + throw new Error(`Timed out waiting for process ${pid} to exit`); } describe("Matrix QA CLI runtime", () => { @@ -235,15 +248,9 @@ describe("Matrix QA CLI runtime", () => { env: process.env, timeoutMs: 250, }), - ).rejects.toThrow(/stdout:\nwaiting for verification/); - await expect( - runMatrixQaOpenClawCli({ - args: ["matrix", "verify", "self"], - cwd: root, - env: process.env, - timeoutMs: 250, - }), - ).rejects.toThrow(/stderr:\nmatrix sdk still syncing/); + ).rejects.toThrow( + /stderr:\nmatrix sdk still syncing[\s\S]*stdout:\nwaiting for verification/u, + ); } finally { await rm(root, { force: true, recursive: true }); } @@ -312,12 +319,12 @@ describe("Matrix QA CLI runtime", () => { env: process.env, timeoutMs: 500, }); - await sleep(850); + childPid = await waitForPidFile(pidPath, 2_000); + await waitForProcessExit(childPid, 2_000); await expect(session.wait()).rejects.toThrow(/timed out after 500ms/u); await expect(session.wait()).rejects.toThrow(/late wait timeout marker/u); - childPid = Number(await readFile(pidPath, "utf8")); expect(isProcessRunning(childPid)).toBe(false); } finally { if (childPid && isProcessRunning(childPid)) { @@ -409,12 +416,11 @@ describe("Matrix QA CLI runtime", () => { env: process.env, timeoutMs: 500, }); - await waitForFile(grandchildPidPath, 2_000); + grandchildPid = await waitForPidFile(grandchildPidPath, 2_000); await expect(run).rejects.toThrow(/timed out after 500ms/u); - childPid = Number(await readFile(childPidPath, "utf8")); - grandchildPid = Number(await readFile(grandchildPidPath, "utf8")); + childPid = await waitForPidFile(childPidPath, 2_000); expect(isProcessRunning(childPid)).toBe(false); expect(isProcessRunning(grandchildPid)).toBe(false); } finally { @@ -460,14 +466,17 @@ describe("Matrix QA CLI runtime", () => { env: process.env, timeoutMs: 10_000, }); - await waitForFile(grandchildPidPath, 2_000); - await sleep(300); + childPid = await waitForPidFile(childPidPath, 2_000); + grandchildPid = await waitForPidFile(grandchildPidPath, 2_000); + const sessionExit = session.wait().catch(() => undefined); session.kill(); - await sleep(500); - childPid = Number(await readFile(childPidPath, "utf8")); - grandchildPid = Number(await readFile(grandchildPidPath, "utf8")); + await Promise.all([ + waitForProcessExit(childPid, 2_000), + waitForProcessExit(grandchildPid, 2_000), + ]); + await sessionExit; expect(isProcessRunning(childPid)).toBe(false); expect(isProcessRunning(grandchildPid)).toBe(false); } finally { diff --git a/extensions/telegram/src/send.transport-close-proof.test.ts b/extensions/telegram/src/send.transport-close-proof.test.ts index c72d2674b419..c0862a0c5aad 100644 --- a/extensions/telegram/src/send.transport-close-proof.test.ts +++ b/extensions/telegram/src/send.transport-close-proof.test.ts @@ -17,6 +17,7 @@ describe("telegram transport cache eviction over real sockets", () => { let sendMessageCalls = 0; let slowMode = false; let slowRequestReceived: () => void = () => {}; + let releaseSlowResponse: (() => void) | undefined; beforeAll(async () => { server = createServer((req, res) => { @@ -34,7 +35,9 @@ describe("telegram transport cache eviction over real sockets", () => { sendMessageCalls += 1; if (slowMode) { slowRequestReceived(); - setTimeout(() => respond({ message_id: sendMessageCalls, chat: { id: 123 } }), 800); + releaseSlowResponse = () => { + respond({ message_id: sendMessageCalls, chat: { id: 123 } }); + }; return; } respond({ message_id: sendMessageCalls, chat: { id: 123 } }); @@ -114,12 +117,24 @@ describe("telegram transport cache eviction over real sockets", () => { await inFlight; slowMode = false; - // New cache key -> evicts acct-0 while its send holds the lease. - const evictor = await sendMessageTelegram("123", "evictor", { cfg, accountId: "acct-64" }); - expect(evictor.messageId).toBeTruthy(); - // Deferred close: the evicted transport must NOT be closed mid-request. - expect(sockets.closed).toBe(0); - + const releaseResponse = releaseSlowResponse; + if (!releaseResponse) { + throw new Error("slow Telegram response was not captured"); + } + try { + // New cache key -> evicts acct-0 while its send holds the lease. + const evictor = await sendMessageTelegram("123", "evictor", { + cfg, + accountId: "acct-64", + }); + expect(evictor.messageId).toBeTruthy(); + // Deferred close: the evicted transport must NOT be closed mid-request. + expect(sockets.closed).toBe(0); + } finally { + releaseSlowResponse = undefined; + releaseResponse(); + await slowSend.catch(() => undefined); + } const slow = await slowSend; expect(slow.messageId).toBeTruthy(); // Lease released -> the retired acct-0 transport closes its real socket. diff --git a/test/scripts/pr-prepare-gates.test.ts b/test/scripts/pr-prepare-gates.test.ts index 670dd028a833..033328e008cd 100644 --- a/test/scripts/pr-prepare-gates.test.ts +++ b/test/scripts/pr-prepare-gates.test.ts @@ -208,6 +208,43 @@ async function waitForExit(child: ChildProcess): Promise { }); } +async function waitForStderr( + child: ChildProcess, + expected: string, + timeoutMs: number, +): Promise { + const stderr = child.stderr; + if (!stderr) { + throw new Error("child stderr is not piped"); + } + stderr.setEncoding("utf8"); + let output = ""; + return await new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(timeout); + stderr.off("data", onData); + child.off("exit", onExit); + }; + const onData = (chunk: string) => { + output += chunk; + if (output.includes(expected)) { + cleanup(); + resolve(output); + } + }; + const onExit = () => { + cleanup(); + reject(new Error(`child exited before writing ${JSON.stringify(expected)}: ${output}`)); + }; + const timeout = setTimeout(() => { + cleanup(); + reject(new Error(`timed out waiting for ${JSON.stringify(expected)}: ${output}`)); + }, timeoutMs); + stderr.on("data", onData); + child.once("exit", onExit); + }); +} + afterEach(async () => { for (const child of children.splice(0)) { child.kill("SIGKILL"); @@ -759,7 +796,7 @@ describe("pr-gates-lock helper", () => { const second = spawnGateLockHolder(repoDir, secondStatus, { OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS: "50", }); - await new Promise((resolve) => setTimeout(resolve, 400)); + await waitForStderr(second, "queued behind the local heavy-check lock", 5_000); expect(existsSync(secondStatus)).toBe(false); first.kill("SIGTERM");