script: fix segfault when a tool call triggers navigation

A press/click that submits a form or follows a link commits a
replacement Page inside the tool call itself (finalizeAction ->
awaitQueuedNavigation), freeing the Frame that
Session._tool_frame_override still pointed at; finalizeAction then
dereferenced it via requireFrame -> currentFrame. Any script whose
press/click navigated crashed on replay.

Store a frame id instead of a pointer and resolve it at every
currentFrame call. The replacement page keeps the frame id (see
commitPendingPage), so the same handle stays routable across the swap.
This commit is contained in:
Adrià Arrufat committed 2026-07-03 23:52:29 +02:00
1 parent 933dc61bf6
commit ae2359c1af
2 files changed
+32 -7

No files matched your search

+23 -2
View File
@@ -449,10 +449,10 @@ fn invoke(self: *Runtime, tool: BrowserTool, info: *const v8.FunctionCallbackInf
// `pages[0]` is not necessarily this handle's page.
const frame_id = self.receiverFrameId(context, info) orelse
return self.throwError("page is not navigated or has been closed; call page.goto(url) first");
const frame = self.session.findFrameByFrameId(frame_id) orelse
if (self.session.findFrameByFrameId(frame_id) == null)
return self.throwError("page handle is no longer valid; the page was closed");
self.session.setToolFrameOverride(frame);
self.session.setToolFrameOverride(frame_id);
defer self.session.setToolFrameOverride(null);
const result = self.callTool(arena, tool, args) catch |err| switch (err) {
@@ -1083,6 +1083,27 @@ test "agent script runtime: parallel gotos coexist and route per page" {
);
}
test "agent script runtime: a tool-triggered navigation keeps the handle routable" {
defer testing.reset();
defer testing.test_session.closeAllPages();
var registry = CDPNode.Registry.init(testing.allocator);
defer registry.deinit();
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
// click commits a replacement Page mid-call; the frame override and the
// handle's frame id must both survive the swap.
try runTestScript(runtime,
\\const page = new Page();
\\await page.goto("http://localhost:9582/src/browser/tests/mcp_nav.html");
\\page.click("#navlink");
\\const href = page.evaluate("location.href");
\\if (href !== "about:blank") throw new Error("expected about:blank, got: " + href);
);
}
test "agent script runtime: re-goto on the same page object replaces its page" {
defer testing.reset();
defer testing.test_session.closeAllPages();