Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/runtime/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 14 additions & 6 deletions packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading