diff --git a/src/browser/CdpObservationSession.ts b/src/browser/CdpObservationSession.ts index 74f457cc4c..1680acf1dd 100644 --- a/src/browser/CdpObservationSession.ts +++ b/src/browser/CdpObservationSession.ts @@ -144,6 +144,8 @@ export const observeCdpSession = async ( message: "Browser observation window ended", terminal: true, }); + // Terminal progress is awaited and may admit more protocol events. + capture.assertRequestOwnershipWithinLimit(); if (capture.scopeExcluded) endReason = "target_left_scope"; return { browser: context.discovery.version, diff --git a/tests/boundary/browser/cdpSessionRequestBudget.test.ts b/tests/boundary/browser/cdpSessionRequestBudget.test.ts index bdeae17e8a..b469fd8639 100644 --- a/tests/boundary/browser/cdpSessionRequestBudget.test.ts +++ b/tests/boundary/browser/cdpSessionRequestBudget.test.ts @@ -1,4 +1,5 @@ import { expect, it, onTestFinished, vi } from "vitest"; +import { CdpConnection } from "../../../src/browser/CdpConnection.js"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; @@ -17,53 +18,73 @@ vi.mock("../../../src/domain/webRuntime.js", async (importOriginal) => { }; }); -it("refuses active document identity retention before reporting a complete session", async () => { - const browser = await startFakeCdpBrowser({ commandEvents: () => [] }); - const provider = new CdpBrowserProvider(); - onTestFinished(async () => { - await provider.close(); - await browser.close(); - }); - const result = await provider.observeSession( - observeWebSessionInputSchema.parse({ - cdp_endpoint: browser.endpoint, - target_id: "allowed-page", - allowed_origins: [browser.allowedOrigin], - observation_ms: 1_000, - }), - { - signal: AbortSignal.timeout(2_000), - progress: { - report: async (update) => { - if (update.completed === 1) - browser.emitEvent({ - method: "Network.requestWillBeSent", - sessionId: "session-1", - params: { - requestId: "r".repeat(128), - type: "Document", - frameId: "frame-main", - loaderId: "loader", - timestamp: 100, - request: { - url: `${browser.allowedOrigin}/page`, - method: "GET", +it.each([1, 2])( + "refuses active document identity retention at progress stage %s before reporting a complete session", + async (stage) => { + let received: () => void = () => {}; + const delivered = new Promise((resolve) => { + received = resolve; + }); + const onEvent = CdpConnection.prototype.onEvent; + const delivery = vi + .spyOn(CdpConnection.prototype, "onEvent") + .mockImplementation(function (this: CdpConnection, listener) { + return onEvent.call(this, (event) => { + listener(event); + if (event.method === "Network.requestWillBeSent") received(); + }); + }); + const browser = await startFakeCdpBrowser({ commandEvents: () => [] }); + const provider = new CdpBrowserProvider(); + onTestFinished(async () => { + delivery.mockRestore(); + await provider.close(); + await browser.close(); + }); + const result = await provider.observeSession( + observeWebSessionInputSchema.parse({ + cdp_endpoint: browser.endpoint, + target_id: "allowed-page", + allowed_origins: [browser.allowedOrigin], + observation_ms: 5, + }), + { + signal: AbortSignal.timeout(2_000), + progress: { + report: async (update) => { + if (update.completed === stage) { + browser.emitEvent({ + method: "Network.requestWillBeSent", + sessionId: "session-1", + params: { + requestId: "r".repeat(128), + type: "Document", + frameId: "frame-main", + loaderId: "loader", + timestamp: 100, + request: { + url: `${browser.allowedOrigin}/page`, + method: "GET", + }, }, - }, - }); + }); + // Observe actual parser/listener delivery before final progress resolves. + await delivered; + } + }, }, }, - }, - ); - expect(result).toMatchObject({ - ok: false, - error: { - _tag: "BrowserObservationError", - reason: "payload_limit", - detail: expect.stringContaining("Active document request identities"), - }, - }); - expect(browser.commands.map(({ method }) => method)).toContain( - "Target.detachFromTarget", - ); -}); + ); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "BrowserObservationError", + reason: "payload_limit", + detail: expect.stringContaining("Active document request identities"), + }, + }); + expect(browser.commands.map(({ method }) => method)).toContain( + "Target.detachFromTarget", + ); + }, +);