diff --git a/packages/runtime/src/index.test.ts b/packages/runtime/src/index.test.ts index 0b0c708..fc76b5f 100644 --- a/packages/runtime/src/index.test.ts +++ b/packages/runtime/src/index.test.ts @@ -138,6 +138,43 @@ describe("BrowserRuntime", () => { await rt.shutdown(); }); + it("disables Playwright process signal handlers so the host owns shutdown", async () => { + const { browser } = createMockBrowserTree(); + mockChromiumLaunch.mockResolvedValueOnce(browser); + + const rt = new BrowserRuntime({ headless: true }); + await rt.createSession(); + expect(mockChromiumLaunch).toHaveBeenCalledWith( + expect.objectContaining({ + headless: true, + handleSIGINT: false, + handleSIGTERM: false, + handleSIGHUP: false, + }), + ); + await rt.shutdown(); + }); + + it("disables Playwright signal handlers on webkit fallback launch", async () => { + const { browser } = createMockBrowserTree(); + mockChromiumLaunch.mockRejectedValueOnce( + new Error("browserType.launch: Executable doesn't exist"), + ); + mockWebkitLaunch.mockResolvedValueOnce(browser); + + const rt = new BrowserRuntime({ headless: false }); + await rt.createSession(); + expect(mockWebkitLaunch).toHaveBeenCalledWith( + expect.objectContaining({ + headless: false, + handleSIGINT: false, + handleSIGTERM: false, + handleSIGHUP: false, + }), + ); + await rt.shutdown(); + }); + it("snapshots HTML with summarizeHtml and layered fields", async () => { const { browser } = createMockBrowserTree(); mockChromiumLaunch.mockResolvedValueOnce(browser); diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 6e1c945..e6a5224 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -50,19 +50,27 @@ export class BrowserRuntime { } private async launchBrowser() { + /** + * Companion/MCP own process lifecycle (SIGINT/SIGTERM → graceful shutdown). + * Playwright defaults handleSIGINT/TERM to true and will close the browser + * then process.exit(130) on SIGINT, racing the host signal handlers. + */ + const launchOptions = { + headless: this.options.headless ?? true, + handleSIGINT: false, + handleSIGTERM: false, + handleSIGHUP: false, + } as const; + try { - return await chromium.launch({ - headless: this.options.headless ?? true, - }); + return await chromium.launch(launchOptions); } catch (chromiumError) { if (!shouldFallbackToWebkit(chromiumError)) { throw mapPlaywrightLaunchError(chromiumError); } try { - return await webkit.launch({ - headless: this.options.headless ?? true, - }); + return await webkit.launch(launchOptions); } catch (webkitError) { throw mapPlaywrightLaunchError(webkitError); }