mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
* fix(browser): preserve unknown HTTP response comparison fields * fix(browser): retain proven request inventory changes * fix(javascript): preserve Electron identity and avoid quadratic property scans Share visible Electron mutations with wrapper expansion, index static object literal properties once, and align MCP assertions with current contracts. * fix(javascript): track writes through symbolic Electron aliases Resolve mutation targets through the same factories, module tables, containers, and conditional origins as call analysis. Share mutation coverage between direct and wrapper scans while isolating origin caches and preserving exact property paths, local rebinding semantics, and analysis resource limits. * fix(javascript): preserve container slots and imported mutation origins * perf(javascript): defer container mutation replay until property reads * fix(javascript): preserve captured object identities and write order * fix(javascript): preserve ordered mutation origins * fix(javascript): honor property mutation constraints * test(web): narrow PR to HTTP and CLI comparison regressions --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
256 lines
8.1 KiB
TypeScript
256 lines
8.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
|
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
|
import type { WebPageInspection } from "../../../src/domain/browserObservationSchemas.js";
|
|
import { compareWebCaptures } from "../../../src/domain/webCaptureDiff.js";
|
|
import { compareWebCapturesInputSchema } from "../../../src/domain/webCaptureDiffSchemas.js";
|
|
import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
|
import {
|
|
startHttpComparisonBrowser,
|
|
type HttpRequestObservation,
|
|
} from "../../fixtures/httpComparisonBrowser.js";
|
|
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
|
|
|
const inspect = async (browser: FakeCdpBrowser): Promise<WebPageInspection> => {
|
|
const result = await new CdpBrowserProvider().inspectPage(
|
|
inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
allowed_origins: [browser.allowedOrigin],
|
|
target_id: "allowed-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
return result.value;
|
|
};
|
|
|
|
const capturePair = async (
|
|
before: readonly HttpRequestObservation[],
|
|
after: readonly HttpRequestObservation[],
|
|
): Promise<readonly [WebPageInspection, WebPageInspection]> => {
|
|
const fixture = await startHttpComparisonBrowser(before);
|
|
trackBrowser(fixture.browser);
|
|
const left = await inspect(fixture.browser);
|
|
fixture.setRequests(after);
|
|
return [left, await inspect(fixture.browser)];
|
|
};
|
|
|
|
const compareNetwork = (before: WebPageInspection, after: WebPageInspection) =>
|
|
compareWebCaptures(
|
|
compareWebCapturesInputSchema.parse({
|
|
before: { inspection: before },
|
|
after: { inspection: after },
|
|
}),
|
|
).dimensions.network;
|
|
|
|
const response = {
|
|
requestId: "request-before",
|
|
status: 200,
|
|
mimeType: "application/json",
|
|
} as const;
|
|
|
|
describe("HTTP comparison response coverage", () => {
|
|
it("does not compare an unavailable byte count with a completed response", async () => {
|
|
const [before, after] = await capturePair(
|
|
[response],
|
|
[{ ...response, requestId: "request-after", encodedDataLength: 100 }],
|
|
);
|
|
expect(before.network.requests[0]?.encoded_data_length).toBeNull();
|
|
expect(after.network.requests[0]?.encoded_data_length).toBe(100);
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compareNetwork(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
});
|
|
|
|
it("does not treat an unobserved response as a changed response", async () => {
|
|
const [before, after] = await capturePair(
|
|
[{ requestId: "request-before" }],
|
|
[{ ...response, encodedDataLength: 100 }],
|
|
);
|
|
expect(before.network.requests).toEqual([
|
|
expect.objectContaining({
|
|
status: null,
|
|
mime_type: null,
|
|
encoded_data_length: null,
|
|
}),
|
|
]);
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compareNetwork(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
|
|
it("retains observed status changes when response bytes are unavailable", async () => {
|
|
const [before, after] = await capturePair(
|
|
[response],
|
|
[{ ...response, status: 500 }],
|
|
);
|
|
expect(compareNetwork(before, after)).toMatchObject({
|
|
status: "changed",
|
|
changes: [expect.objectContaining({ change: "modified" })],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ mimeType: "text/plain", encodedDataLength: 100 },
|
|
{ mimeType: "application/json", encodedDataLength: 200 },
|
|
])("retains observed response changes: %j", async (change) => {
|
|
const [before, after] = await capturePair(
|
|
[{ ...response, encodedDataLength: 100 }],
|
|
[{ ...response, ...change }],
|
|
);
|
|
expect(compareNetwork(before, after).status).toBe("changed");
|
|
});
|
|
});
|
|
|
|
describe("HTTP comparison repeated request coverage", () => {
|
|
it("does not assign missing evidence by request order or transient ID", async () => {
|
|
const [before, after] = await capturePair(
|
|
[
|
|
{ ...response, encodedDataLength: 100 },
|
|
{ ...response, requestId: "second-before" },
|
|
],
|
|
[
|
|
{ ...response, requestId: "second-after", encodedDataLength: 200 },
|
|
{ ...response, requestId: "first-after", encodedDataLength: 100 },
|
|
],
|
|
);
|
|
expect(before.network.requests).toHaveLength(2);
|
|
expect(after.network.requests).toHaveLength(2);
|
|
expect(compareNetwork(before, after)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
});
|
|
});
|
|
|
|
it("compares complete byte counts even when another field is incomplete", async () => {
|
|
const [before, after] = await capturePair(
|
|
[
|
|
{ ...response, encodedDataLength: 100 },
|
|
{ requestId: "second-before", encodedDataLength: 200 },
|
|
],
|
|
[
|
|
{ requestId: "second-after", encodedDataLength: 300 },
|
|
{ ...response, requestId: "first-after", encodedDataLength: 100 },
|
|
],
|
|
);
|
|
expect(compareNetwork(before, after).status).toBe("changed");
|
|
});
|
|
|
|
it("preserves the association between complete response fields", async () => {
|
|
const [before, after] = await capturePair(
|
|
[
|
|
{ ...response, encodedDataLength: 100 },
|
|
{
|
|
...response,
|
|
requestId: "second-before",
|
|
status: 201,
|
|
encodedDataLength: 200,
|
|
},
|
|
],
|
|
[
|
|
{ ...response, requestId: "first-after", encodedDataLength: 200 },
|
|
{
|
|
...response,
|
|
requestId: "second-after",
|
|
status: 201,
|
|
encodedDataLength: 100,
|
|
},
|
|
],
|
|
);
|
|
expect(compareNetwork(before, after).status).toBe("changed");
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a pending request in an empty inventory",
|
|
before: [],
|
|
after: [{ requestId: "pending" }],
|
|
},
|
|
{
|
|
name: "a known request beside an existing pending request",
|
|
before: [{ requestId: "pending" }],
|
|
after: [
|
|
{ requestId: "pending" },
|
|
{
|
|
...response,
|
|
requestId: "new",
|
|
path: "/api/new",
|
|
encodedDataLength: 100,
|
|
},
|
|
],
|
|
},
|
|
])("preserves additions and removals of $name", async (observations) => {
|
|
const [before, after] = await capturePair(
|
|
observations.before,
|
|
observations.after,
|
|
);
|
|
// Live CDP observations have attach-limited inventories. Their absence
|
|
// remains unknown even when a request is seen in the other capture.
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
] as const)
|
|
expect(compareNetwork(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
|
|
// Project explicitly complete inventory coverage without changing the
|
|
// producer's request facts; response fields remain independently unknown.
|
|
markNetworkInventoriesComplete(before, after);
|
|
for (const [left, right, change] of [
|
|
[before, after, "added"],
|
|
[after, before, "removed"],
|
|
] as const)
|
|
expect(compareNetwork(left, right)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 1,
|
|
changes: [expect.objectContaining({ change })],
|
|
});
|
|
});
|
|
|
|
it("requires response field coverage even when capture sections are complete", async () => {
|
|
const [before, after] = await capturePair(
|
|
[response],
|
|
[{ ...response, encodedDataLength: 100 }],
|
|
);
|
|
markNetworkInventoriesComplete(before, after);
|
|
expect(compareNetwork(before, before).status).toBe("unknown");
|
|
expect(compareNetwork(before, after).status).toBe("unknown");
|
|
expect(compareNetwork(after, after).status).toBe("unchanged");
|
|
});
|
|
|
|
const markNetworkInventoriesComplete = (
|
|
...inspections: WebPageInspection[]
|
|
): void => {
|
|
const sections = new Set([
|
|
"network_requests",
|
|
"websocket_connections",
|
|
"websocket_frames",
|
|
]);
|
|
for (const inspection of inspections)
|
|
for (const key of [
|
|
"policy_filtered_sections",
|
|
"attach_limited_sections",
|
|
"truncated_sections",
|
|
"unavailable_sections",
|
|
] as const)
|
|
inspection.completeness[key] = inspection.completeness[key].filter(
|
|
(section) => !sections.has(section),
|
|
);
|
|
};
|