From 1e87f6bf703dd3571bb719900e29a160d2e911a7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 00:44:11 +0100 Subject: [PATCH] fix(qa-lab): preserve gateway log offset order --- extensions/qa-lab/src/gateway-child.test.ts | 17 +++++++++++++++++ extensions/qa-lab/src/gateway-child.ts | 21 +++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/extensions/qa-lab/src/gateway-child.test.ts b/extensions/qa-lab/src/gateway-child.test.ts index 2036bb064ef..f05082c54ba 100644 --- a/extensions/qa-lab/src/gateway-child.test.ts +++ b/extensions/qa-lab/src/gateway-child.test.ts @@ -344,6 +344,23 @@ describe("buildQaRuntimeEnv", () => { await expect(wait).resolves.toBeUndefined(); }); + it("keeps restart offsets stable after stderr output", async () => { + const output = __testing.createQaGatewayChildLogCollector(); + output.push(Buffer.from("gateway ready\n")); + output.push(Buffer.from("stderr warning\n")); + const offset = output.text().length; + const wait = __testing.waitForQaGatewayRestartBoundary({ + logs: () => output.text(), + offset, + pollMs: 1, + timeoutMs: 100, + }); + + output.push(Buffer.from("signal SIGUSR1 received\nrestart mode: in-process restart\n")); + + await expect(wait).resolves.toBeUndefined(); + }); + it("times out when a SIGUSR1 restart never reaches the boundary", async () => { await expect( __testing.waitForQaGatewayRestartBoundary({ diff --git a/extensions/qa-lab/src/gateway-child.ts b/extensions/qa-lab/src/gateway-child.ts index a62e0f2ef16..238250504ac 100644 --- a/extensions/qa-lab/src/gateway-child.ts +++ b/extensions/qa-lab/src/gateway-child.ts @@ -247,6 +247,18 @@ function isRetryableGatewayCallError(details: string): boolean { ); } +function createQaGatewayChildLogCollector() { + const chunks: Buffer[] = []; + return { + push(chunk: Buffer) { + chunks.push(Buffer.from(chunk)); + }, + text() { + return Buffer.concat(chunks).toString("utf8").trim(); + }, + }; +} + async function fetchLocalGatewayHealth(params: { baseUrl: string; healthPath: "/readyz" | "/healthz"; @@ -307,6 +319,7 @@ export const __testing = { resolveQaOwnerPluginIdsForProviderIds, resolveQaBundledPluginSourceDir, resolveQaRuntimeHostVersion, + createQaGatewayChildLogCollector, createQaBundledPluginsDir, stopQaGatewayChildProcessTree, }; @@ -574,13 +587,13 @@ export async function startQaGatewayChild(params: { }; const stdout: Buffer[] = []; const stderr: Buffer[] = []; + const output = createQaGatewayChildLogCollector(); const stdoutLogPath = path.join(tempRoot, "gateway.stdout.log"); const stderrLogPath = path.join(tempRoot, "gateway.stderr.log"); const stdoutLog = createWriteStream(stdoutLogPath, { flags: "a" }); const stderrLog = createWriteStream(stderrLogPath, { flags: "a" }); - const logs = () => - `${Buffer.concat(stdout).toString("utf8")}\n${Buffer.concat(stderr).toString("utf8")}`.trim(); + const logs = () => output.text(); const keepTemp = process.env.OPENCLAW_QA_KEEP_TEMP === "1"; let gatewayPort = 0; let baseUrl = ""; @@ -667,11 +680,13 @@ export async function startQaGatewayChild(params: { attemptChild.stdout.on("data", (chunk) => { const buffer = Buffer.from(chunk); stdout.push(buffer); + output.push(buffer); stdoutLog.write(buffer); }); attemptChild.stderr.on("data", (chunk) => { const buffer = Buffer.from(chunk); stderr.push(buffer); + output.push(buffer); stderrLog.write(buffer); }); child = attemptChild; @@ -760,11 +775,13 @@ export async function startQaGatewayChild(params: { nextChild.stdout.on("data", (chunk) => { const buffer = Buffer.from(chunk); stdout.push(buffer); + output.push(buffer); stdoutLog.write(buffer); }); nextChild.stderr.on("data", (chunk) => { const buffer = Buffer.from(chunk); stderr.push(buffer); + output.push(buffer); stderrLog.write(buffer); });