From 7b3a8dba147df9b90142668ab53747dce99c8e1a Mon Sep 17 00:00:00 2001 From: xingzhou Date: Sun, 19 Jul 2026 16:17:46 +0800 Subject: [PATCH] fix(meeting-bot): preserve spawn failure diagnostics (#107614) --- extensions/google-meet/node-host.test.ts | 87 ++++++++++++++++++++++++ src/meeting-bot/node-host.ts | 9 ++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/extensions/google-meet/node-host.test.ts b/extensions/google-meet/node-host.test.ts index 1b4194416a01..90d764193442 100644 --- a/extensions/google-meet/node-host.test.ts +++ b/extensions/google-meet/node-host.test.ts @@ -163,6 +163,93 @@ describe("google-meet node host bridge sessions", () => { } }); + it("rejects Chrome launch when the command exits by signal", async () => { + const originalPlatform = process.platform; + vi.mocked(spawnSync).mockImplementationOnce(() => ({ + pid: 123, + output: [null, "", ""], + stdout: "", + stderr: "", + status: null, + signal: "SIGTERM", + })); + + Object.defineProperty(process, "platform", { configurable: true, value: "darwin" }); + try { + await expect( + handleGoogleMeetNodeHostCommand( + JSON.stringify({ + action: "start", + url: "https://meet.google.com/xyz-abcd-uvw", + mode: "transcribe", + }), + ), + ).rejects.toThrow("failed to launch Chrome for Meet: terminated by SIGTERM"); + } finally { + Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform }); + } + }); + + it("preserves timeout diagnostics when Chrome launch stderr is empty", async () => { + const originalPlatform = process.platform; + const error = Object.assign(new Error("spawnSync open ETIMEDOUT"), { code: "ETIMEDOUT" }); + vi.mocked(spawnSync).mockImplementationOnce(() => ({ + pid: 123, + output: [null, "", ""], + stdout: "", + stderr: "", + status: null, + signal: "SIGTERM", + error, + })); + + Object.defineProperty(process, "platform", { configurable: true, value: "darwin" }); + try { + await expect( + handleGoogleMeetNodeHostCommand( + JSON.stringify({ + action: "start", + url: "https://meet.google.com/xyz-abcd-uvw", + mode: "transcribe", + }), + ), + ).rejects.toThrow("failed to launch Chrome for Meet: spawnSync open ETIMEDOUT"); + } finally { + Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform }); + } + }); + + it("preserves timeout diagnostics when Chrome launch also writes stderr", async () => { + const originalPlatform = process.platform; + const error = Object.assign(new Error("spawnSync open ETIMEDOUT"), { code: "ETIMEDOUT" }); + vi.mocked(spawnSync).mockImplementationOnce(() => ({ + pid: 123, + output: [null, "", "child warning"], + stdout: "", + stderr: "child warning", + status: null, + signal: "SIGTERM", + error, + })); + + Object.defineProperty(process, "platform", { configurable: true, value: "darwin" }); + try { + await expect( + handleGoogleMeetNodeHostCommand( + JSON.stringify({ + action: "start", + url: "https://meet.google.com/xyz-abcd-uvw", + mode: "transcribe", + }), + ), + ).rejects.toThrow( + "failed to launch Chrome for Meet: spawnSync open ETIMEDOUT: child warning", + ); + } finally { + Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform }); + } + }); + it("clears output playback without closing the active bridge when the old output exits", async () => { const originalPlatform = process.platform; children.length = 0; diff --git a/src/meeting-bot/node-host.ts b/src/meeting-bot/node-host.ts index f06985e6b012..2cdc15ee84c9 100644 --- a/src/meeting-bot/node-host.ts +++ b/src/meeting-bot/node-host.ts @@ -81,10 +81,15 @@ function runCommandWithTimeout(argv: string[], timeoutMs: number) { throw new Error("command must not be empty"); } const result = spawnSync(command, args, { encoding: "utf8", timeout: timeoutMs }); + const errorMessage = result.error ? formatErrorMessage(result.error) : ""; + const stderr = + errorMessage && result.stderr + ? `${errorMessage}: ${result.stderr}` + : errorMessage || result.stderr || (result.signal ? `terminated by ${result.signal}` : ""); return { - code: typeof result.status === "number" ? result.status : result.error ? 1 : 0, + code: typeof result.status === "number" ? result.status : 1, stdout: result.stdout ?? "", - stderr: result.stderr ?? (result.error ? formatErrorMessage(result.error) : ""), + stderr, }; }