fix(browser)!: preserve unknown WebSocket frame facts

Keep malformed or missing CDP opcodes and payload facts unknown, retain observed zero and empty payloads, and avoid false capture differences while preserving independently known facts.

Validation: 59 source tests; compiled public CLI and stdio MCP fake-CDP acceptance; check:fast; docs:check; changed-file formatting. Real Chromium malformed-event behavior was not tested.

BREAKING CHANGE: WebSocket event opcode is nullable and numeric values are restricted to the four-bit protocol range.

Fixes #5666
This commit is contained in:
morluto committed 2026-10-21 05:00:37 +08:00
1 parent c38f96fa69
commit 0bc2b42001
7 files changed
+458 -26

No files matched your search

+68
View File
@@ -219,6 +219,74 @@ describe("passive network inventory changes", () => {
});
});
describe("unknown WebSocket opcode evidence", () => {
const captureWithWebSocketFrames = (
events: WebPageInspection["network"]["websocket_connections"][number]["events"],
): WebPageInspection => {
const capture = unselectedShapes(passiveNetworkCapture([]));
capture.network.websocket_connections.push({
request_id: "capture-local-socket-id",
url: "wss://app.example.test/live",
events: structuredClone(events),
});
return capture;
};
it("does not compare an unknown opcode as a real continuation frame", () => {
const malformed = captureWithWebSocketFrames([
{
direction: "received",
opcode: null,
payload_bytes: null,
payload_shape: null,
},
]);
const observedZero = captureWithWebSocketFrames([
{
direction: "received",
opcode: 0,
payload_bytes: 0,
payload_shape: { format: "binary", json_shape: null },
},
]);
for (const [before, after] of [
[malformed, observedZero],
[observedZero, malformed],
] as const)
expect(compare(before, after)).toMatchObject({
status: "unknown",
total_changes: 0,
changes: [],
});
});
it("still compares known payload facts in a stream with another unknown frame", () => {
const before = captureWithWebSocketFrames([
{
direction: "received",
opcode: null,
payload_bytes: null,
payload_shape: null,
},
{
direction: "sent",
opcode: 1,
payload_bytes: 3,
payload_shape: { format: "text", json_shape: null },
},
]);
const after = structuredClone(before);
after.network.websocket_connections[0]!.events[1]!.payload_bytes = 4;
expect(compare(before, after)).toMatchObject({
status: "changed",
total_changes: 1,
changes: [{ change: "modified" }],
});
});
});
describe("passive network coverage uncertainty", () => {
it.each([
"attach_limited_sections",