mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
757 lines
24 KiB
TypeScript
757 lines
24 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
passiveNetworkCapture,
|
|
withPassiveDroppedEvents,
|
|
} from "../../tests/fixtures/passiveNetworkCapture.js";
|
|
import {
|
|
browserCaptureComparisonInputSchema,
|
|
compareBrowserCaptures,
|
|
} from "./browserCaptureComparison.js";
|
|
import type { WebPageInspection } from "./browserObservationSchemas.js";
|
|
import { webCaptureDiffSchema } from "./webCaptureDiffSchemas.js";
|
|
|
|
const compare = (before: WebPageInspection, after: WebPageInspection) =>
|
|
webCaptureDiffSchema.parse(
|
|
compareBrowserCaptures(
|
|
browserCaptureComparisonInputSchema.parse({
|
|
before: { inspection: before },
|
|
after: { inspection: after },
|
|
}),
|
|
),
|
|
).dimensions.network;
|
|
|
|
const unselectedShapes = (capture: WebPageInspection): WebPageInspection => {
|
|
capture.completeness.excluded.push({
|
|
section: "json_body_shapes",
|
|
reason: "not_approved",
|
|
count: null,
|
|
});
|
|
for (const request of capture.network.requests)
|
|
request.body_shapes.status = "not_approved";
|
|
return capture;
|
|
};
|
|
|
|
const redirectCapture = (resourceType: string | null): WebPageInspection => {
|
|
const capture = unselectedShapes(passiveNetworkCapture());
|
|
capture.network.requests[0]!.redirects = [
|
|
{
|
|
url: "https://app.example.test/start",
|
|
response_url: "https://app.example.test/start",
|
|
method: "GET",
|
|
resource_type: resourceType,
|
|
status: 302,
|
|
mime_type: "text/html",
|
|
encoded_data_length: 20,
|
|
request_timestamp: 1,
|
|
redirect_event_timestamp: 2,
|
|
},
|
|
];
|
|
return capture;
|
|
};
|
|
|
|
describe("nullable redirect resource types", () => {
|
|
it.each([true, false])(
|
|
"retains uncertainty when a redirect type is omitted (reversed=%s)",
|
|
(reversed) => {
|
|
const before = redirectCapture("Document");
|
|
const after = structuredClone(before);
|
|
after.network.requests[0]!.redirects![0]!.resource_type = null;
|
|
const [left, right] = reversed
|
|
? ([after, before] as const)
|
|
: ([before, after] as const);
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["Document", "Fetch"])(
|
|
"keeps a known redirect type %s comparable",
|
|
(resourceType) => {
|
|
const before = redirectCapture("Document");
|
|
const after = structuredClone(before);
|
|
after.network.requests[0]!.redirects![0]!.resource_type = resourceType;
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: resourceType === "Document" ? "unchanged" : "changed",
|
|
total_changes: resourceType === "Document" ? 0 : 1,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("retains proven HTTP membership with incomplete redirect metadata", () => {
|
|
const before = unselectedShapes(passiveNetworkCapture([]));
|
|
const after = redirectCapture(null);
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "added" }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("passive network inventory changes", () => {
|
|
it.each(["websocket_connections", "websocket_frames"] as const)(
|
|
"does not infer duplicate WebSocket inventory changes from dropped %s",
|
|
(kind) => {
|
|
const before = unselectedShapes(passiveNetworkCapture());
|
|
before.network.websocket_connections = [
|
|
{
|
|
request_id: "ws-one",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
},
|
|
];
|
|
const after = structuredClone(before);
|
|
after.network.websocket_connections.push({
|
|
request_id: "ws-two",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
});
|
|
const limited = withPassiveDroppedEvents(before, { [kind]: 1 });
|
|
for (const [left, right] of [
|
|
[limited, after],
|
|
[after, limited],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "modified" }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["dropped", "truncated", "unavailable"] as const)(
|
|
"does not infer repeated-request count changes from %s HTTP inventory",
|
|
(limitation) => {
|
|
const after = unselectedShapes(passiveNetworkCapture(["/a", "/a"]));
|
|
let before = unselectedShapes(passiveNetworkCapture());
|
|
if (limitation === "dropped")
|
|
before = withPassiveDroppedEvents(before, { network_requests: 1 });
|
|
else if (limitation === "truncated")
|
|
before.completeness.truncated_sections.push("network_requests");
|
|
else before.completeness.unavailable_sections.push("network_requests");
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["unavailable", "partial", "truncated"] as const)(
|
|
"retains complete request additions/removals with %s selected shapes",
|
|
(coverage) => {
|
|
const before = passiveNetworkCapture(["/a"]);
|
|
const after = passiveNetworkCapture(["/a", "/b"]);
|
|
for (const capture of [before, after]) {
|
|
if (coverage === "truncated")
|
|
capture.completeness.truncated_sections.push("json_body_shapes");
|
|
else capture.completeness.unavailable_sections.push("json_body_shapes");
|
|
for (const request of capture.network.requests) {
|
|
const shape = {
|
|
root_type: "object" as const,
|
|
node_count: 1,
|
|
max_depth_observed: 0,
|
|
properties: [],
|
|
};
|
|
if (coverage !== "unavailable") request.body_shapes.request = shape;
|
|
if (coverage === "truncated") request.body_shapes.response = shape;
|
|
request.body_shapes.status =
|
|
coverage === "truncated" ? "included" : coverage;
|
|
}
|
|
}
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toEqual({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^net_/u), change }],
|
|
reason: null,
|
|
});
|
|
expect(compare(after, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("keeps request absence proof independent of WebSocket inventory and response metadata", () => {
|
|
const before = passiveNetworkCapture([]);
|
|
const after = passiveNetworkCapture(["/added"]);
|
|
for (const capture of [before, after]) {
|
|
capture.completeness.attach_limited_sections.push(
|
|
"websocket_connections",
|
|
"websocket_frames",
|
|
);
|
|
for (const request of capture.network.requests) {
|
|
request.status = null;
|
|
request.mime_type = null;
|
|
request.encoded_data_length = null;
|
|
}
|
|
}
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change }],
|
|
});
|
|
expect(compare(after, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
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",
|
|
"unavailable_sections",
|
|
"truncated_sections",
|
|
"policy_filtered_sections",
|
|
] as const)(
|
|
"does not infer request absence when its inventory is %s",
|
|
(section) => {
|
|
const before = passiveNetworkCapture();
|
|
const after = passiveNetworkCapture(["/a", "/b"]);
|
|
before.completeness[section].push("network_requests");
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
])
|
|
expect(compare(left!, right!)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
after.network.requests[0]!.status = 503;
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "modified" }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not promote unavailable shapes to a modification and preserves unselected-shape behavior", () => {
|
|
const before = passiveNetworkCapture();
|
|
const after = structuredClone(before);
|
|
after.network.requests[0]!.body_shapes = {
|
|
status: "included",
|
|
request: {
|
|
root_type: "object",
|
|
node_count: 1,
|
|
max_depth_observed: 0,
|
|
properties: [],
|
|
},
|
|
response: {
|
|
root_type: "string",
|
|
node_count: 1,
|
|
max_depth_observed: 0,
|
|
properties: [],
|
|
},
|
|
};
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
before.completeness.excluded.push({
|
|
section: "json_body_shapes",
|
|
reason: "not_approved",
|
|
count: null,
|
|
});
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "unchanged",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("independent WebSocket inventory evidence", () => {
|
|
it("does not use complete HTTP inventory to infer an unobserved WebSocket connection", () => {
|
|
const before = passiveNetworkCapture();
|
|
const after = structuredClone(before);
|
|
after.network.websocket_connections.push({
|
|
request_id: "ws",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
});
|
|
before.completeness.attach_limited_sections.push("websocket_connections");
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
expect(compare(after, before)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
after.network.requests.push({
|
|
...passiveNetworkCapture(["/b"]).network.requests[0]!,
|
|
request_id: "request-b",
|
|
});
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^net_/u), change: "added" }],
|
|
});
|
|
});
|
|
|
|
it("retains complete WebSocket membership changes when HTTP inventory is limited", () => {
|
|
const before = passiveNetworkCapture();
|
|
before.completeness.attach_limited_sections.push("network_requests");
|
|
const after = structuredClone(before);
|
|
after.network.websocket_connections.push({
|
|
request_id: "ws",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
});
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^ws_/u), change }],
|
|
});
|
|
});
|
|
|
|
it("does not turn missing WebSocket payload facts into membership changes", () => {
|
|
const before = passiveNetworkCapture();
|
|
before.network.websocket_connections.push({
|
|
request_id: "ws",
|
|
url: "wss://app.example.test/live",
|
|
events: [
|
|
{
|
|
direction: "sent",
|
|
opcode: 1,
|
|
payload_bytes: 4,
|
|
payload_shape: { format: "text", json_shape: null },
|
|
},
|
|
],
|
|
});
|
|
const after = structuredClone(before);
|
|
const event = after.network.websocket_connections[0]!.events[0]!;
|
|
event.payload_bytes = null;
|
|
event.payload_shape = null;
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
after.network.requests.push({
|
|
...passiveNetworkCapture(["/b"]).network.requests[0]!,
|
|
request_id: "request-b",
|
|
});
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^net_/u), change: "added" }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("repeated HTTP request identity uncertainty", () => {
|
|
it.each([true, false])(
|
|
"keeps repeated-request regrouping unknown when one type is missing (shapes selected=%s)",
|
|
(selected) => {
|
|
const before = passiveNetworkCapture(["/a", "/a"]);
|
|
const after = structuredClone(before);
|
|
after.network.requests[1]!.resource_type = null;
|
|
if (!selected)
|
|
for (const capture of [before, after]) unselectedShapes(capture);
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("retains a known repeated-request count change", () => {
|
|
const before = unselectedShapes(passiveNetworkCapture(["/a", "/a"]));
|
|
const after = unselectedShapes(passiveNetworkCapture());
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "modified" }],
|
|
});
|
|
});
|
|
|
|
it("does not assign observed content to an ambiguous type bucket", () => {
|
|
const before = unselectedShapes(passiveNetworkCapture(["/a", "/a"]));
|
|
before.network.requests[1]!.resource_type = null;
|
|
before.network.requests[1]!.status = 503;
|
|
const after = structuredClone(before);
|
|
after.network.requests[0]!.status = 503;
|
|
after.network.requests[1]!.status = 200;
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("nullable HTTP request identities", () => {
|
|
it.each([true, false])(
|
|
"keeps known/unknown resource-type matches uncertain (shapes selected=%s)",
|
|
(selected) => {
|
|
const before = passiveNetworkCapture();
|
|
const after = structuredClone(before);
|
|
after.network.requests[0]!.resource_type = null;
|
|
if (!selected)
|
|
for (const capture of [before, after]) unselectedShapes(capture);
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
reason: expect.stringContaining("request identity"),
|
|
});
|
|
},
|
|
);
|
|
|
|
it("does not prove equality from two unknown request types", () => {
|
|
const capture = unselectedShapes(passiveNetworkCapture());
|
|
capture.network.requests[0]!.resource_type = null;
|
|
expect(compare(capture, structuredClone(capture))).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
reason: expect.stringContaining("request identity"),
|
|
});
|
|
});
|
|
|
|
it("does not assign an unknown type to one of multiple same-method/URL candidates", () => {
|
|
const before = unselectedShapes(passiveNetworkCapture(["/a", "/a"]));
|
|
before.network.requests[1]!.resource_type = "Fetch";
|
|
const after = structuredClone(before);
|
|
after.network.requests[1]!.resource_type = null;
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["/b", "GET"],
|
|
["/a", "POST"],
|
|
])(
|
|
"retains a distinct %s %s request beside ambiguous identities",
|
|
(path, method) => {
|
|
const before = unselectedShapes(passiveNetworkCapture());
|
|
const after = unselectedShapes(passiveNetworkCapture(["/a", path]));
|
|
after.network.requests[0]!.resource_type = null;
|
|
after.network.requests[1]!.method = method;
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^net_/u), change }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("uses a complete empty peer inventory to prove absence despite an unknown type", () => {
|
|
const empty = unselectedShapes(passiveNetworkCapture([]));
|
|
const observed = unselectedShapes(passiveNetworkCapture());
|
|
observed.network.requests[0]!.resource_type = null;
|
|
for (const [left, right, change] of [
|
|
[empty, observed, "added"],
|
|
[observed, empty, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change }],
|
|
});
|
|
expect(compare(empty, empty)).toMatchObject({
|
|
status: "unchanged",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
|
|
it("retains definite type changes and independently observed metadata changes", () => {
|
|
const before = unselectedShapes(passiveNetworkCapture());
|
|
const changedType = structuredClone(before);
|
|
changedType.network.requests[0]!.resource_type = "Fetch";
|
|
expect(compare(before, changedType)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 2,
|
|
changes: expect.arrayContaining([
|
|
expect.objectContaining({ change: "added" }),
|
|
expect.objectContaining({ change: "removed" }),
|
|
]),
|
|
});
|
|
before.network.requests.push({
|
|
...passiveNetworkCapture(["/b"]).network.requests[0]!,
|
|
resource_type: null,
|
|
});
|
|
const changedStatus = structuredClone(before);
|
|
changedStatus.network.requests[0]!.status = 503;
|
|
expect(compare(before, changedStatus)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "modified" }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("passive dropped-event inventory coverage", () => {
|
|
it("does not infer request absence from a capture with independently counted drops", () => {
|
|
const before = withPassiveDroppedEvents(passiveNetworkCapture(), {
|
|
network_requests: 1,
|
|
});
|
|
const after = passiveNetworkCapture(["/a", "/b"]);
|
|
expect(before.completeness).toMatchObject({
|
|
status: "truncated",
|
|
truncated_sections: [],
|
|
unavailable_sections: [],
|
|
attach_limited_sections: [],
|
|
policy_filtered_sections: [],
|
|
dropped_events: { network_requests: 1, total: 1 },
|
|
});
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
const changed = structuredClone(before);
|
|
changed.network.requests[0]!.status = 503;
|
|
expect(compare(before, changed)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
// Once one complete inventory excludes the peer's observed response,
|
|
// omissions cannot explain that response difference.
|
|
changed.completeness.dropped_events.network_requests = 0;
|
|
changed.completeness.dropped_events.total = 0;
|
|
expect(compare(before, changed)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "modified" }],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"network_requests",
|
|
"websocket_connections",
|
|
"websocket_frames",
|
|
] as const)(
|
|
"cannot prove network equality after dropped %s even when shapes are unselected",
|
|
(kind) => {
|
|
const before = withPassiveDroppedEvents(
|
|
unselectedShapes(passiveNetworkCapture()),
|
|
{ [kind]: 1 },
|
|
);
|
|
expect(before.completeness.truncated_sections).toEqual([]);
|
|
expect(compare(before, structuredClone(before))).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["websocket_connections", "websocket_frames"] as const)(
|
|
"keeps %s drops separate from HTTP inventory",
|
|
(kind) => {
|
|
const before = withPassiveDroppedEvents(passiveNetworkCapture(), {
|
|
[kind]: 1,
|
|
});
|
|
const after = structuredClone(before);
|
|
after.network.websocket_connections.push({
|
|
request_id: "ws",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
});
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
after.network.requests.push({
|
|
...passiveNetworkCapture(["/b"]).network.requests[0]!,
|
|
request_id: "request-b",
|
|
});
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compare(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^net_/u), change }],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("retains complete WebSocket membership despite HTTP drops", () => {
|
|
const before = withPassiveDroppedEvents(passiveNetworkCapture(), {
|
|
network_requests: 1,
|
|
});
|
|
const after = structuredClone(before);
|
|
after.network.websocket_connections.push({
|
|
request_id: "ws",
|
|
url: "wss://app.example.test/live",
|
|
events: [],
|
|
});
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ identity: expect.stringMatching(/^ws_/u), change: "added" }],
|
|
});
|
|
expect(compare(after, before)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [
|
|
{ identity: expect.stringMatching(/^ws_/u), change: "removed" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"console_events",
|
|
"scripts",
|
|
"timeline_events",
|
|
"webmcp_tools",
|
|
] as const)(
|
|
"does not use the total or unrelated %s drops as HTTP absence limits",
|
|
(kind) => {
|
|
const before = withPassiveDroppedEvents(
|
|
unselectedShapes(passiveNetworkCapture()),
|
|
{ [kind]: 2 },
|
|
);
|
|
expect(before.completeness).toMatchObject({
|
|
status: "truncated",
|
|
dropped_events: { total: 2 },
|
|
});
|
|
expect(compare(before, structuredClone(before))).toMatchObject({
|
|
status: "unchanged",
|
|
total_changes: 0,
|
|
});
|
|
const after = unselectedShapes(passiveNetworkCapture(["/a", "/b"]));
|
|
expect(compare(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "added" }],
|
|
});
|
|
expect(compare(after, before)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [{ change: "removed" }],
|
|
});
|
|
},
|
|
);
|
|
});
|