diff --git a/extensions/qa-lab/src/gateway-child.test.ts b/extensions/qa-lab/src/gateway-child.test.ts index c340a4089cf..6c042046a1c 100644 --- a/extensions/qa-lab/src/gateway-child.test.ts +++ b/extensions/qa-lab/src/gateway-child.test.ts @@ -328,6 +328,32 @@ describe("buildQaRuntimeEnv", () => { expect(__testing.isRetryableGatewayCallError("permission denied")).toBe(false); }); + it("waits for a fresh in-process restart boundary after the current log offset", async () => { + let logs = "old restart mode: in-process restart\n"; + const offset = logs.length; + const wait = __testing.waitForQaGatewayRestartBoundary({ + logs: () => logs, + offset, + pollMs: 1, + timeoutMs: 100, + }); + + logs += "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({ + logs: () => "signal SIGUSR1 received\n", + offset: 0, + pollMs: 1, + timeoutMs: 1, + }), + ).rejects.toThrow("qa gateway child did not reach restart boundary"); + }); + it("stages a live Anthropic setup-token profile for isolated QA workers", async () => { const stateDir = await mkdtemp(path.join(os.tmpdir(), "qa-setup-token-state-")); cleanups.push(async () => { diff --git a/extensions/qa-lab/src/gateway-child.ts b/extensions/qa-lab/src/gateway-child.ts index a41dc1ef011..9dc7b938676 100644 --- a/extensions/qa-lab/src/gateway-child.ts +++ b/extensions/qa-lab/src/gateway-child.ts @@ -268,6 +268,24 @@ async function fetchLocalGatewayHealth(params: { } } +async function waitForQaGatewayRestartBoundary(params: { + logs: () => string; + offset: number; + pollMs?: number; + timeoutMs?: number; +}) { + const timeoutMs = params.timeoutMs ?? 30_000; + const pollMs = params.pollMs ?? 100; + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + if (params.logs().slice(params.offset).includes("restart mode:")) { + return; + } + await sleep(pollMs); + } + throw new Error(`qa gateway child did not reach restart boundary within ${timeoutMs}ms`); +} + export const __testing = { assertQaArtifactDirWithinRepo, buildQaRuntimeEnv, @@ -283,6 +301,7 @@ export const __testing = { stageQaLiveAnthropicSetupToken, stageQaMockAuthProfiles, resolveQaLiveCliAuthEnv, + waitForQaGatewayRestartBoundary, resolveQaOwnerPluginIdsForProviderIds, resolveQaBundledPluginSourceDir, resolveQaRuntimeHostVersion, @@ -816,7 +835,20 @@ export async function startQaGatewayChild(params: { if (!activeChild.pid) { throw new Error("qa gateway child has no pid"); } + const restartLogOffset = logs().length; process.kill(activeChild.pid, signal); + if (signal === "SIGUSR1") { + await waitForQaGatewayRestartBoundary({ + logs, + offset: restartLogOffset, + }); + await waitForGatewayReady({ + baseUrl, + logs, + child: activeChild, + timeoutMs: 120_000, + }); + } }, async restartAfterStateMutation( mutateState: (context: QaGatewayChildStateMutationContext) => Promise,