Skip to content

Commit 8e37524

Browse files
committed
fix(qa-lab): handle gateway child spawn errors
1 parent 93cbd16 commit 8e37524

2 files changed

Lines changed: 55 additions & 0 deletions

File tree

extensions/qa-lab/src/gateway-child.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,32 @@ describe("buildQaRuntimeEnv", () => {
134134
await expect(readdir(tempParent)).resolves.toStrictEqual([]);
135135
});
136136

137+
it("reports command spawn errors instead of leaking unhandled child errors", async () => {
138+
const tempParent = await mkdtemp(path.join(os.tmpdir(), "qa-gateway-spawn-fail-"));
139+
cleanups.push(async () => {
140+
await rm(tempParent, { recursive: true, force: true });
141+
});
142+
qaTempPathState.preferredTmpDir = tempParent;
143+
const missingExecutable = path.join(tempParent, "missing-openclaw-node");
144+
145+
await expect(
146+
startQaGatewayChild({
147+
repoRoot: process.cwd(),
148+
command: {
149+
executablePath: missingExecutable,
150+
usePackagedPlugins: true,
151+
},
152+
transport: {
153+
requiredPluginIds: [],
154+
createGatewayConfig: () => ({}),
155+
},
156+
transportBaseUrl: "http://127.0.0.1:43123",
157+
}),
158+
).rejects.toThrow(/gateway failed to spawn: .*ENOENT/u);
159+
160+
await expect(readdir(tempParent)).resolves.toStrictEqual([]);
161+
});
162+
137163
it("keeps the slow-reply QA opt-out enabled under fast mode", () => {
138164
const env = buildQaRuntimeEnv({
139165
...createParams(),

extensions/qa-lab/src/gateway-child.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,20 @@ function createQaGatewayChildLogCollector() {
279279
};
280280
}
281281

282+
function monitorQaGatewayChildSpawnError(
283+
child: ChildProcess,
284+
output: { push(chunk: Buffer): void },
285+
) {
286+
let spawnError: unknown = null;
287+
child.once("error", (error) => {
288+
spawnError = error;
289+
output.push(
290+
Buffer.from(`[qa-lab] gateway child process error: ${formatErrorMessage(error)}\n`),
291+
);
292+
});
293+
return () => spawnError;
294+
}
295+
282296
async function fetchLocalGatewayHealth(params: {
283297
baseUrl: string;
284298
healthPath: "/readyz" | "/healthz";
@@ -487,10 +501,19 @@ async function waitForGatewayReady(params: {
487501
exitCode: number | null;
488502
signalCode: NodeJS.Signals | null;
489503
};
504+
getSpawnError?: () => unknown;
490505
timeoutMs?: number;
491506
}) {
492507
const startedAt = Date.now();
493508
while (Date.now() - startedAt < (params.timeoutMs ?? 60_000)) {
509+
const spawnError = params.getSpawnError?.();
510+
if (spawnError) {
511+
throw new QaSuiteInfraError(
512+
"gateway_startup_unhealthy",
513+
`gateway failed to spawn: ${formatErrorMessage(spawnError)}\n${params.logs()}`,
514+
{ cause: spawnError },
515+
);
516+
}
494517
if (params.child.exitCode !== null || params.child.signalCode !== null) {
495518
throw new QaSuiteInfraError(
496519
"gateway_startup_unhealthy",
@@ -768,12 +791,14 @@ export async function startQaGatewayChild(params: {
768791
stderrLog.write(buffer);
769792
});
770793
child = attemptChild;
794+
const getAttemptSpawnError = monitorQaGatewayChildSpawnError(attemptChild, output);
771795

772796
try {
773797
await waitForGatewayReady({
774798
baseUrl,
775799
logs,
776800
child: attemptChild,
801+
getSpawnError: getAttemptSpawnError,
777802
timeoutMs: 120_000,
778803
});
779804
const attemptRpcClient = await startQaGatewayRpcClient({
@@ -805,6 +830,7 @@ export async function startQaGatewayChild(params: {
805830
baseUrl,
806831
logs,
807832
child: attemptChild,
833+
getSpawnError: getAttemptSpawnError,
808834
timeoutMs: QA_GATEWAY_CHILD_RPC_RETRY_HEALTH_TIMEOUT_MS,
809835
});
810836
}
@@ -871,12 +897,14 @@ export async function startQaGatewayChild(params: {
871897
output.push(buffer);
872898
stderrLog.write(buffer);
873899
});
900+
const getNextSpawnError = monitorQaGatewayChildSpawnError(nextChild, output);
874901

875902
try {
876903
await waitForGatewayReady({
877904
baseUrl,
878905
logs,
879906
child: nextChild,
907+
getSpawnError: getNextSpawnError,
880908
timeoutMs: 120_000,
881909
});
882910
const nextRpcClient = await startQaGatewayRpcClient({
@@ -908,6 +936,7 @@ export async function startQaGatewayChild(params: {
908936
baseUrl,
909937
logs,
910938
child: nextChild,
939+
getSpawnError: getNextSpawnError,
911940
timeoutMs: 15_000,
912941
});
913942
}

0 commit comments

Comments
 (0)