mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(browser): compare only observed HTTP response facts
This commit is contained in:
@@ -249,10 +249,37 @@ const networkIdentity = (
|
||||
): string =>
|
||||
`net_${digestCanonicalValue({ method: request.method, url: request.url, resource_type: request.resource_type })}`;
|
||||
|
||||
const networkResponseFields = [
|
||||
"status",
|
||||
"mime_type",
|
||||
"encoded_data_length",
|
||||
] as const;
|
||||
type NetworkResponseField = (typeof networkResponseFields)[number];
|
||||
|
||||
const sharedNetworkResponseFields = (
|
||||
before: WebPageInspection,
|
||||
after: WebPageInspection,
|
||||
): ReadonlyMap<string, ReadonlySet<NetworkResponseField>> => {
|
||||
const shared = new Map<string, Set<NetworkResponseField>>();
|
||||
for (const inspection of [before, after])
|
||||
for (const request of inspection.network.requests) {
|
||||
const identity = networkIdentity(request);
|
||||
const fields = shared.get(identity) ?? new Set(networkResponseFields);
|
||||
for (const field of networkResponseFields)
|
||||
if (request[field] === null) fields.delete(field);
|
||||
shared.set(identity, fields);
|
||||
}
|
||||
return shared;
|
||||
};
|
||||
|
||||
const networkDimension = (
|
||||
before: WebPageInspection,
|
||||
after: WebPageInspection,
|
||||
): Dimension => {
|
||||
const responseFields = sharedNetworkResponseFields(before, after);
|
||||
const responseFieldsComplete = [...responseFields.values()].every(
|
||||
(fields) => fields.size === networkResponseFields.length,
|
||||
);
|
||||
const selected = bodyShapesSelected(before) && bodyShapesSelected(after);
|
||||
const webSocketShapes =
|
||||
webSocketShapesSelected(before) && webSocketShapesSelected(after);
|
||||
@@ -313,14 +340,14 @@ const networkDimension = (
|
||||
afterWebSockets,
|
||||
webSocketShapes,
|
||||
);
|
||||
return compareDimension(
|
||||
const compared = compareDimension(
|
||||
new Map([
|
||||
...networkMap(before, sources),
|
||||
...networkMap(before, sources, responseFields),
|
||||
...webSocketMetadataMap(beforeWebSockets),
|
||||
...beforeWebSocketPayload,
|
||||
]),
|
||||
new Map([
|
||||
...networkMap(after, sources),
|
||||
...networkMap(after, sources, responseFields),
|
||||
...webSocketMetadataMap(afterWebSockets),
|
||||
...afterWebSocketPayload,
|
||||
]),
|
||||
@@ -336,21 +363,31 @@ const networkDimension = (
|
||||
webSocketShapesComplete,
|
||||
"Network capture or selected HTTP/WebSocket payload-shape coverage is attach-window limited, unavailable, or incomplete.",
|
||||
);
|
||||
return compared.status === "unchanged" && !responseFieldsComplete
|
||||
? {
|
||||
...compared,
|
||||
status: "unknown",
|
||||
reason:
|
||||
"HTTP response status, MIME type, or byte-count evidence is incomplete.",
|
||||
}
|
||||
: compared;
|
||||
};
|
||||
|
||||
const networkMap = (
|
||||
inspection: WebPageInspection,
|
||||
sources: ReadonlyMap<string, BodyShapeSources>,
|
||||
responseFields: ReadonlyMap<string, ReadonlySet<NetworkResponseField>>,
|
||||
): ReadonlyMap<string, string> => {
|
||||
const grouped = new Map<string, unknown[]>();
|
||||
for (const request of inspection.network.requests) {
|
||||
const identity = networkIdentity(request);
|
||||
const shapes = sources.get(identity);
|
||||
const values = grouped.get(identity) ?? [];
|
||||
const response: Record<string, unknown> = {};
|
||||
for (const field of responseFields.get(identity) ?? [])
|
||||
response[field] = request[field];
|
||||
values.push({
|
||||
status: request.status,
|
||||
mime_type: request.mime_type,
|
||||
encoded_data_length: request.encoded_data_length,
|
||||
...response,
|
||||
redirects: (request.redirects ?? []).map(
|
||||
({
|
||||
request_timestamp: _requestTimestamp,
|
||||
|
||||
@@ -446,6 +446,92 @@ describe("web capture WebSocket payload byte coverage", () => {
|
||||
});
|
||||
|
||||
describe("web capture diff", () => {
|
||||
it("treats unavailable HTTP response facts as unknown while retaining observed changes", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
browsers.push(browser);
|
||||
const captured = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
if (!captured.ok) throw captured.error;
|
||||
const before = structuredClone(captured.value);
|
||||
const request = before.network.requests[0];
|
||||
if (request === undefined) throw new Error("Missing network request");
|
||||
request.status = null;
|
||||
request.mime_type = null;
|
||||
request.encoded_data_length = null;
|
||||
markSectionsComplete(before, [
|
||||
"network_requests",
|
||||
"websocket_connections",
|
||||
"websocket_frames",
|
||||
"json_body_shapes",
|
||||
]);
|
||||
for (const item of before.network.requests)
|
||||
item.body_shapes.status = "not_approved";
|
||||
|
||||
const after = structuredClone(before);
|
||||
const responseObserved = after.network.requests[0];
|
||||
if (responseObserved === undefined)
|
||||
throw new Error("Missing network request");
|
||||
responseObserved.status = 200;
|
||||
responseObserved.mime_type = "application/json";
|
||||
responseObserved.encoded_data_length = 100;
|
||||
expect(compareNetwork(before, after)).toMatchObject({
|
||||
status: "unknown",
|
||||
total_changes: 0,
|
||||
changes: [],
|
||||
});
|
||||
expect(compareNetwork(after, before)).toMatchObject({
|
||||
status: "unknown",
|
||||
total_changes: 0,
|
||||
});
|
||||
expect(compareNetwork(before, before).status).toBe("unknown");
|
||||
|
||||
for (const field of [
|
||||
"status",
|
||||
"mime_type",
|
||||
"encoded_data_length",
|
||||
] as const) {
|
||||
const partial = structuredClone(after);
|
||||
partial.network.requests[0]![field] = null;
|
||||
expect(compareNetwork(partial, after).status).toBe("unknown");
|
||||
expect(compareNetwork(after, partial).status).toBe("unknown");
|
||||
}
|
||||
|
||||
const inventoryBefore = structuredClone(before);
|
||||
inventoryBefore.network.requests = [];
|
||||
expect(compareNetwork(inventoryBefore, before)).toMatchObject({
|
||||
status: "changed",
|
||||
total_changes: 1,
|
||||
changes: [expect.objectContaining({ change: "added" })],
|
||||
});
|
||||
|
||||
const statusChanged = structuredClone(after);
|
||||
statusChanged.network.requests[0]!.status = 500;
|
||||
expect(compareNetwork(after, statusChanged)).toMatchObject({
|
||||
status: "changed",
|
||||
changes: [expect.objectContaining({ change: "modified" })],
|
||||
});
|
||||
after.network.requests[0]!.encoded_data_length = null;
|
||||
expect(compareNetwork(after, statusChanged).status).toBe("changed");
|
||||
|
||||
const repeated = structuredClone(statusChanged);
|
||||
const duplicate = structuredClone(repeated.network.requests[0]!);
|
||||
duplicate.status = 200;
|
||||
duplicate.mime_type = "text/plain";
|
||||
repeated.network.requests.push(duplicate);
|
||||
const reassociated = structuredClone(repeated);
|
||||
reassociated.network.requests[0]!.mime_type = "text/plain";
|
||||
reassociated.network.requests.at(-1)!.mime_type = "application/json";
|
||||
expect(compareNetwork(repeated, reassociated).status).toBe("changed");
|
||||
repeated.network.requests[0]!.status = null;
|
||||
expect(compareNetwork(repeated, reassociated).status).toBe("unknown");
|
||||
});
|
||||
|
||||
it("ignores transient request IDs and capture-approval state", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
browsers.push(browser);
|
||||
|
||||
Reference in New Issue
Block a user