test(browser): reject unmodeled CDP commands in protocol fixtures

This commit is contained in:
morluto committed 2026-10-09 03:13:53 +08:00
1 parent ffd9611308
commit 017297c40c
5 files changed
+71 -10

No files matched your search

+4
View File
@@ -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
@@ -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) }),
+19 -6
View File
@@ -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)
+19 -2
View File
@@ -6,7 +6,7 @@ export const resultFor = (
port: number,
options: FakeOptions,
frameTreeReads: number,
): Readonly<Record<string, unknown>> => {
): Readonly<Record<string, unknown>> | 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;
}
};
+7 -2
View File
@@ -21,6 +21,13 @@ const runtimeResult = (
origin: string,
): Readonly<Record<string, unknown>> | 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,