mirror of
https://github.com/hanzoai/puppeteer.git
synced 2026-09-24 17:02:14 +00:00
fix(webmcp): invalidate webmcp tools on context destruction (#15068)
This commit is contained in:
@@ -15,7 +15,6 @@ import {debugError} from '../common/util.js';
|
||||
|
||||
import type {CdpFrame} from './Frame.js';
|
||||
import type {FrameManager} from './FrameManager.js';
|
||||
import {FrameManagerEvent} from './FrameManagerEvents.js';
|
||||
import {MAIN_WORLD} from './IsolatedWorlds.js';
|
||||
|
||||
/**
|
||||
@@ -317,6 +316,7 @@ export class WebMCP extends EventEmitter<{
|
||||
const frameTools = this.#tools.get(tool.frameId) ?? new Map();
|
||||
if (!this.#tools.has(tool.frameId)) {
|
||||
this.#tools.set(tool.frameId, frameTools);
|
||||
this.#listenToContextDestroyed(frame as CdpFrame);
|
||||
}
|
||||
|
||||
const addedTool = new WebMCPTool(this, tool, frame);
|
||||
@@ -366,7 +366,7 @@ export class WebMCP extends EventEmitter<{
|
||||
this.emit('toolresponded', response);
|
||||
};
|
||||
|
||||
#onFrameNavigated = (frame: Frame) => {
|
||||
#onContextDisposed = (frame: CdpFrame) => {
|
||||
this.#pendingCalls.clear();
|
||||
const frameTools = this.#tools.get(frame._id);
|
||||
if (!frameTools) {
|
||||
@@ -379,6 +379,12 @@ export class WebMCP extends EventEmitter<{
|
||||
}
|
||||
};
|
||||
|
||||
#listenToContextDestroyed(frame: CdpFrame): void {
|
||||
frame.mainRealm().context?.once('disposed', () => {
|
||||
this.#onContextDisposed(frame);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -386,10 +392,6 @@ export class WebMCP extends EventEmitter<{
|
||||
super();
|
||||
this.#client = client;
|
||||
this.#frameManager = frameManager;
|
||||
this.#frameManager.on(
|
||||
FrameManagerEvent.FrameNavigated,
|
||||
this.#onFrameNavigated,
|
||||
);
|
||||
this.#bindListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,89 @@ describe('Page.webmcp', function () {
|
||||
expect(page.webmcp.tools().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle multiple navigations and report tools correctly', async () => {
|
||||
const {page, httpsServer} = state;
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
|
||||
// 1. Register tool on C1
|
||||
let toolsAddedPromise = new Promise<void>(resolve => {
|
||||
page.webmcp.once('toolsadded', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('toolname', 'tool-1');
|
||||
form.setAttribute('tooldescription', 'desc-1');
|
||||
document.body.appendChild(form);
|
||||
});
|
||||
await toolsAddedPromise;
|
||||
expect(page.webmcp.tools().length).toBe(1);
|
||||
expect(page.webmcp.tools()[0]!.name).toBe('tool-1');
|
||||
|
||||
// 2. Navigate to C2
|
||||
let toolsRemovedPromise = new Promise<void>(resolve => {
|
||||
page.webmcp.once('toolsremoved', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
await toolsRemovedPromise;
|
||||
expect(page.webmcp.tools().length).toBe(0);
|
||||
|
||||
// 3. Register tool on C2
|
||||
toolsAddedPromise = new Promise<void>(resolve => {
|
||||
page.webmcp.once('toolsadded', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('toolname', 'tool-2');
|
||||
form.setAttribute('tooldescription', 'desc-2');
|
||||
document.body.appendChild(form);
|
||||
});
|
||||
await toolsAddedPromise;
|
||||
expect(page.webmcp.tools().length).toBe(1);
|
||||
expect(page.webmcp.tools()[0]!.name).toBe('tool-2');
|
||||
|
||||
// 4. Navigate to C3
|
||||
toolsRemovedPromise = new Promise<void>(resolve => {
|
||||
page.webmcp.once('toolsremoved', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
await toolsRemovedPromise;
|
||||
expect(page.webmcp.tools().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not reset tools on same-document navigation', async () => {
|
||||
const {page, httpsServer} = state;
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
|
||||
const toolsAddedPromise = new Promise<void>(resolve => {
|
||||
page.webmcp.once('toolsadded', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
const form = document.createElement('form');
|
||||
form.setAttribute('toolname', 'declarative tool name');
|
||||
form.setAttribute('tooldescription', 'tool description');
|
||||
document.body.appendChild(form);
|
||||
});
|
||||
await toolsAddedPromise;
|
||||
expect(page.webmcp.tools().length).toBe(1);
|
||||
|
||||
// Same document/hash navigation should not reset tools.
|
||||
await page.goto(httpsServer.EMPTY_PAGE + '#hash');
|
||||
|
||||
// Tools should still be present because context was not destroyed.
|
||||
expect(page.webmcp.tools().length).toBe(1);
|
||||
expect(page.webmcp.tools()[0]!.name).toBe('declarative tool name');
|
||||
});
|
||||
|
||||
it('should fire toolinvoked events', async () => {
|
||||
const {page, httpsServer} = state;
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
|
||||
Reference in New Issue
Block a user