From 017297c40c0c38b9d9c17f5194f0f4c51748f0c3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:13:53 +0800 Subject: [PATCH] test(browser): reject unmodeled CDP commands in protocol fixtures --- docs/testing.md | 4 ++++ tests/boundary/browser/cdpConnection.test.ts | 22 +++++++++++++++++ tests/fixtures/fakeCdpBrowser.ts | 25 +++++++++++++++----- tests/fixtures/fakeCdpBrowserResults.ts | 21 ++++++++++++++-- tests/fixtures/webRuntime.ts | 9 +++++-- 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index bb7a5cd774..1673a47012 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -30,6 +30,10 @@ one golden malformed-input case and a real filesystem lifecycle that proves configure, update, readback, and removal; an add-only journey does not cover the whole lifecycle. +Protocol fixtures must model the commands they support and their producer reply +shapes. Reject unmodeled commands instead of returning a generic success; an +empty result is appropriate only for a supported command with no result fields. + Keep transport-only in-memory MCP sessions in the MCP boundary lane and direct filesystem workflows in the filesystem boundary lane. Real process-tree capture belongs in the serial process boundary lane, including captures invoked through diff --git a/tests/boundary/browser/cdpConnection.test.ts b/tests/boundary/browser/cdpConnection.test.ts index fee720d90e..5df64c46b1 100644 --- a/tests/boundary/browser/cdpConnection.test.ts +++ b/tests/boundary/browser/cdpConnection.test.ts @@ -62,6 +62,28 @@ describe("CDP connection", () => { await connection.close(); } }); + + it("surfaces an unmodeled fake command as a CDP method rejection", async () => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const connection = await CdpConnection.connect( + browser.browserWebSocketUrl, + "observe_web_session", + ); + try { + await expect( + connection.send("Fixture.unmodeledMethod"), + ).rejects.toMatchObject({ + _tag: "BrowserObservationError", + command: "Fixture.unmodeledMethod", + code: -32_601, + reportedMessage: "Method not found: Fixture.unmodeledMethod", + }); + } finally { + await connection.close(); + } + }); + it("preserves the selected payload limit reason for pending and subsequent commands", async () => { const browser = await startFakeCdpBrowser({ commandResult: () => ({ oversized: "x".repeat(2_048) }), diff --git a/tests/fixtures/fakeCdpBrowser.ts b/tests/fixtures/fakeCdpBrowser.ts index 726e02d84e..f9d2ddf085 100644 --- a/tests/fixtures/fakeCdpBrowser.ts +++ b/tests/fixtures/fakeCdpBrowser.ts @@ -178,15 +178,28 @@ const replyToCommand = ( options: FakeOptions, frameTreeReads: number, ): void => { + const result = + options.commandResult?.( + command, + `http://127.0.0.1:${String(port)}`, + frameTreeReads, + ) ?? resultFor(command, port, options, frameTreeReads); + if (result === undefined) { + socket.send( + JSON.stringify({ + id: command.id, + error: { + code: -32_601, + message: `Method not found: ${command.method}`, + }, + }), + ); + return; + } socket.send( JSON.stringify({ id: command.id, - result: - options.commandResult?.( - command, - `http://127.0.0.1:${String(port)}`, - frameTreeReads, - ) ?? resultFor(command, port, options, frameTreeReads), + result, }), ); if (options.malformedEventOnMethod === command.method) diff --git a/tests/fixtures/fakeCdpBrowserResults.ts b/tests/fixtures/fakeCdpBrowserResults.ts index d73d15e42d..5b5f38bbb3 100644 --- a/tests/fixtures/fakeCdpBrowserResults.ts +++ b/tests/fixtures/fakeCdpBrowserResults.ts @@ -6,7 +6,7 @@ export const resultFor = ( port: number, options: FakeOptions, frameTreeReads: number, -): Readonly> => { +): Readonly> | undefined => { switch (command.method) { case "Target.attachToTarget": return attachToTargetResult(options); @@ -44,8 +44,25 @@ export const resultFor = ( return cacheEntriesResult(port); case "CacheStorage.requestCachedResponse": return cachedResponseResult(options); - default: + case "Page.enable": + case "Page.disable": + case "Page.setLifecycleEventsEnabled": + case "Network.enable": + case "Network.disable": + case "Debugger.disable": + case "Runtime.enable": + case "Runtime.disable": + case "Runtime.releaseObjectGroup": + case "DOM.enable": + case "DOM.disable": return {}; + case "Debugger.enable": + return { debuggerId: "fake-debugger" }; + case "Target.detachFromTarget": + case "WebMCP.enable": + return {}; + default: + return undefined; } }; diff --git a/tests/fixtures/webRuntime.ts b/tests/fixtures/webRuntime.ts index 24efa936b6..5b93bccd6d 100644 --- a/tests/fixtures/webRuntime.ts +++ b/tests/fixtures/webRuntime.ts @@ -21,6 +21,13 @@ const runtimeResult = ( origin: string, ): Readonly> | undefined => { switch (command.method) { + case "Profiler.enable": + case "Profiler.disable": + return {}; + case "Profiler.startPreciseCoverage": + return { timestamp: 12 }; + case "Profiler.stopPreciseCoverage": + return {}; case "Page.getFrameTree": return { frameTree: { @@ -31,8 +38,6 @@ const runtimeResult = ( }, }, }; - case "Profiler.startPreciseCoverage": - return { timestamp: 12 }; case "Profiler.takePreciseCoverage": return { timestamp: 13,