mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
test(browser): consolidate CDP workflows and colocate parser coverage
This commit is contained in:
1 parent
1fd1989d34
commit
26829045d0
18 files changed
+104
-397
No files matched your search
@@ -115,29 +115,6 @@ describe("DOM metadata document base URLs", () => {
|
||||
});
|
||||
|
||||
describe("empty form destinations with a document base URL", () => {
|
||||
it.each([
|
||||
{ nodeName: "FORM", attribute: "action" },
|
||||
{ nodeName: "BUTTON", attribute: "formaction" },
|
||||
{ nodeName: "INPUT", attribute: "formaction" },
|
||||
])(
|
||||
"resolves an empty $nodeName $attribute to the document",
|
||||
({ nodeName, attribute }) => {
|
||||
const pageUrl = `${documentUrl}?selected=fixture#section`;
|
||||
const baseUrl = `${origin}/assets/`;
|
||||
const value = snapshot(baseUrl, "/assets/", pageUrl, "");
|
||||
value.strings[6] = nodeName;
|
||||
value.strings[14] = attribute;
|
||||
value.strings[15] = "";
|
||||
const result = capture(value);
|
||||
expect(result.urls[3]).toMatchObject({
|
||||
attribute,
|
||||
url: pageUrl,
|
||||
destination_scope: "approved",
|
||||
});
|
||||
expect(result.urls[1]?.url).toBe(baseUrl);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an empty form action approved when the document base is foreign", () => {
|
||||
const foreign = "https://cdn.example.test/assets/";
|
||||
const value = snapshot(foreign, foreign);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { CdpCaptureEvents } from "../../../src/browser/CdpCaptureEvents.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { CdpCaptureEvents } from "./CdpCaptureEvents.js";
|
||||
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
|
||||
|
||||
it("preserves an already captured response shape without marking it unavailable", () => {
|
||||
const origin = "http://127.0.0.1:43127";
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { safeResponseMetadata } from "../../../src/browser/CdpSafeMetadata.js";
|
||||
import { safeResponseMetadata } from "./CdpSafeMetadata.js";
|
||||
|
||||
const origin = "https://app.example.test";
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { expect, it } from "vitest";
|
||||
import type { Page } from "playwright-core";
|
||||
|
||||
import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
|
||||
import { performPlaywrightScenarioAction } from "./PlaywrightScenarioActions.js";
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
|
||||
it("uses exact accessible-name matching without a caller confirmation field", async () => {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: "/opt/chromium/chrome" },
|
||||
start_url: { url: "https://app.example.test/" },
|
||||
actions: [
|
||||
{
|
||||
step_id: "submit",
|
||||
action: "click",
|
||||
locator: { kind: "role", role: "button", name: "Submit" },
|
||||
},
|
||||
],
|
||||
});
|
||||
const calls: unknown[][] = [];
|
||||
const page = {
|
||||
getByRole: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return { click: async () => undefined };
|
||||
},
|
||||
} as unknown as Page;
|
||||
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
|
||||
if (secrets === undefined) throw new Error("No secret declarations expected");
|
||||
const action = scenario.actions.at(0);
|
||||
if (action === undefined) throw new Error("Expected one scenario action");
|
||||
|
||||
await performPlaywrightScenarioAction({
|
||||
page,
|
||||
action,
|
||||
secrets,
|
||||
});
|
||||
|
||||
expect(calls).toEqual([["button", { name: "Submit", exact: true }]]);
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { listBrowserTargetsInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js";
|
||||
import { compareWebScreenshotsInputSchema } from "../../../src/domain/webScreenshot.js";
|
||||
import { BROWSER_TOOL_CONTRACTS } from "../../../src/contracts/browserToolContracts.js";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import {
|
||||
compareWebCaptureEvidence,
|
||||
compareWebScreenshotEvidence,
|
||||
listBrowserTargets,
|
||||
} from "../../../src/application/BrowserObservationService.js";
|
||||
|
||||
describe("browser observation service prerequisites", () => {
|
||||
it("reports a missing provider without requiring a permission grant", async () => {
|
||||
const endpoint = "http://127.0.0.1:9222";
|
||||
const origin = "http://[::1]:3000";
|
||||
const result = await listBrowserTargets(
|
||||
undefined,
|
||||
listBrowserTargetsInputSchema.parse({
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [origin],
|
||||
}),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("compares supplied scenarios and PNG artifacts without a CDP endpoint", async () => {
|
||||
const browser = new CdpBrowserProvider();
|
||||
const captures = await compareWebCaptureEvidence(
|
||||
browser,
|
||||
browserCaptureComparisonInputSchema.parse(
|
||||
example("compare_web_captures"),
|
||||
),
|
||||
);
|
||||
const screenshots = await compareWebScreenshotEvidence(
|
||||
browser,
|
||||
compareWebScreenshotsInputSchema.parse(
|
||||
example("compare_web_screenshots"),
|
||||
),
|
||||
);
|
||||
for (const result of [captures, screenshots]) {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.parameters).not.toHaveProperty("cdp_endpoint");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const example = (name: string) => {
|
||||
const input = BROWSER_TOOL_CONTRACTS.find((tool) => tool.name === name)
|
||||
?.examples[0]?.input;
|
||||
if (input === undefined) throw new Error(`Missing example for ${name}`);
|
||||
return input;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import {
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
listBrowserTargetsInputSchema,
|
||||
} from "../../../src/domain/browserObservation.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider: discovery authorization 1", () => {
|
||||
describe("CdpBrowserProvider: discovery authorization 1", () => {
|
||||
it("discovers pages and captures a selected target without an origin list", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
trackBrowser(browser);
|
||||
@@ -158,7 +158,7 @@ describeBrowser("CdpBrowserProvider: discovery authorization 1", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: discovery authorization 2", () => {
|
||||
describe("CdpBrowserProvider: discovery authorization 2", () => {
|
||||
it("rejects empty browser attachment session identifiers", async () => {
|
||||
const browser = await startFakeCdpBrowser({ invalidAttachedSession: true });
|
||||
trackBrowser(browser);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
@@ -6,20 +6,9 @@ import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalys
|
||||
import { discoverWebMcpToolsInputSchema } from "../../../src/domain/webMcpDiscovery.js";
|
||||
import { captureWebScreenshotInputSchema } from "../../../src/domain/webScreenshot.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider: document script 1", () => {
|
||||
it("rejects unknown WebMCP discovery fields", () => {
|
||||
expect(
|
||||
discoverWebMcpToolsInputSchema.safeParse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://example.test"],
|
||||
target_id: "target",
|
||||
unknown_field: true,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describe("CdpBrowserProvider: approved accessibility and source capture", () => {
|
||||
it("captures approved accessibility text inline", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
trackBrowser(browser);
|
||||
@@ -120,7 +109,7 @@ describeBrowser("CdpBrowserProvider: document script 1", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: transient WebMCP frames", () => {
|
||||
describe("CdpBrowserProvider: transient WebMCP frames", () => {
|
||||
it("drops stale child tools and reports partial coverage when a blank commit remains", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
webMcpTools: true,
|
||||
@@ -205,7 +194,7 @@ describeBrowser("CdpBrowserProvider: transient WebMCP frames", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: document script 2", () => {
|
||||
describe("CdpBrowserProvider: WebMCP discovery and completeness", () => {
|
||||
it("discovers untrusted WebMCP declarations without registering or invoking them", async () => {
|
||||
const browser = await startFakeCdpBrowser({ webMcpTools: true });
|
||||
trackBrowser(browser);
|
||||
@@ -360,7 +349,7 @@ describeBrowser("CdpBrowserProvider: document script 2", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider WebMCP inventory completeness", () => {
|
||||
describe("CdpBrowserProvider WebMCP inventory completeness", () => {
|
||||
it("retains every field in a large declared WebMCP input schema", async () => {
|
||||
const propertyCount = 5_001;
|
||||
const browser = await startFakeCdpBrowser({
|
||||
@@ -392,7 +381,7 @@ describeBrowser("CdpBrowserProvider WebMCP inventory completeness", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: document script 3", () => {
|
||||
describe("CdpBrowserProvider: frame scope and screenshots", () => {
|
||||
it("removes WebMCP declarations after their child frame leaves scope", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
webMcpTools: true,
|
||||
@@ -474,7 +463,7 @@ describeBrowser("CdpBrowserProvider: document script 3", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: document script 4", () => {
|
||||
describe("CdpBrowserProvider: complete passive inventories", () => {
|
||||
it("returns every frame, resource, worker, accessibility, and storage inventory item", async () => {
|
||||
const browser = await startFakeCdpBrowser({ extraCollections: true });
|
||||
trackBrowser(browser);
|
||||
@@ -501,7 +490,7 @@ describeBrowser("CdpBrowserProvider: document script 4", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: WebMCP registration owners", () => {
|
||||
describe("CdpBrowserProvider: WebMCP registration owners", () => {
|
||||
it.each(["retain", "remove-second"] as const)(
|
||||
"keeps same-URL frame owners separate: %s",
|
||||
async (mode) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import {
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
startFakeCdpBrowser,
|
||||
type FakeCdpBrowser,
|
||||
} from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => {
|
||||
describe("CdpBrowserProvider: cancellation and session events", () => {
|
||||
it("projects direct-session cancellation onto the requested operation", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
pageScopedVersionWebSocket: true,
|
||||
@@ -132,7 +132,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => {
|
||||
describe("CdpBrowserProvider: session termination and scope changes", () => {
|
||||
it("ends a browser session when its flat target session detaches", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
sessionTimeline: "target_detached",
|
||||
@@ -243,7 +243,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => {
|
||||
describe("CdpBrowserProvider: protocol failures and cancellation cleanup", () => {
|
||||
it("degrades optional domains but propagates protocol and payload failures", async () => {
|
||||
const optional = await startFakeCdpBrowser({
|
||||
unsupportedMethods: ["Accessibility.getFullAXTree"],
|
||||
@@ -373,7 +373,7 @@ describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: lifecycle failures 4", () => {
|
||||
describe("CdpBrowserProvider: navigation commit and target identity", () => {
|
||||
it("waits for navigation commit and rechecks the attached main-frame origin", async () => {
|
||||
const transitioning = await startFakeCdpBrowser({
|
||||
transitionalFrameReads: 2,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider: network 1", () => {
|
||||
describe("CdpBrowserProvider: network 1", () => {
|
||||
it("retains same-origin redirect hops on the one final request", async () => {
|
||||
const browser = await startFakeCdpBrowser({ redirectWithinOrigin: true });
|
||||
trackBrowser(browser);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import type { FakeOptions } from "../../fixtures/fakeCdpBrowserTypes.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
const inspectNetwork = async (options: FakeOptions) => {
|
||||
const browser = await startFakeCdpBrowser(options);
|
||||
@@ -22,7 +22,7 @@ const inspectNetwork = async (options: FakeOptions) => {
|
||||
return { browser, inspection: result.value };
|
||||
};
|
||||
|
||||
describeBrowser("CdpBrowserProvider: redirect errors", () => {
|
||||
describe("CdpBrowserProvider: redirect errors", () => {
|
||||
it.each([
|
||||
{ label: "missing", url: undefined, reason: "invalid_protocol_value" },
|
||||
{ label: "malformed", url: "http://%", reason: "invalid_protocol_value" },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import {
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
startFakeCdpBrowser,
|
||||
type FakeCdpBrowser,
|
||||
} from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider: sensitive data 1", () => {
|
||||
describe("CdpBrowserProvider: passive evidence redaction", () => {
|
||||
it("captures bounded passive evidence without retaining sensitive values", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
binaryWebSocketEvent: true,
|
||||
@@ -158,7 +158,7 @@ describeBrowser("CdpBrowserProvider: sensitive data 1", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: page cookie scope", () => {
|
||||
describe("CdpBrowserProvider: page cookie scope", () => {
|
||||
it("uses the attached page URL for cookies and its origin for DOM storage", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
trackBrowser(browser);
|
||||
@@ -190,7 +190,7 @@ describeBrowser("CdpBrowserProvider: page cookie scope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: complete storage fingerprints", () => {
|
||||
describe("CdpBrowserProvider: complete storage fingerprints", () => {
|
||||
it("fingerprints cache bodies above the former byte ceiling", async () => {
|
||||
const body = "x".repeat(64 * 1_024 + 1);
|
||||
const browser = await startFakeCdpBrowser({ cachedResponseBody: body });
|
||||
@@ -217,7 +217,7 @@ describeBrowser("CdpBrowserProvider: complete storage fingerprints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: sensitive data 2", () => {
|
||||
describe("CdpBrowserProvider: explicitly requested text and payload shapes", () => {
|
||||
it("captures requested console text verbatim and value-free payload shapes", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
sensitiveShapes: true,
|
||||
@@ -309,45 +309,7 @@ describeBrowser("CdpBrowserProvider: sensitive data 2", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describeBrowser("CdpBrowserProvider: complete sensitive data", () => {
|
||||
it("returns complete requested text and payload shapes verbatim", async () => {
|
||||
const browser = await startFakeCdpBrowser({ sensitiveShapes: true });
|
||||
trackBrowser(browser);
|
||||
const request = inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
include_console_text: true,
|
||||
include_json_body_shapes: true,
|
||||
include_websocket_shapes: true,
|
||||
});
|
||||
const result = await new CdpBrowserProvider().inspectPage(request);
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.console.events[0]?.text_capture).toEqual({
|
||||
status: "included",
|
||||
values: [
|
||||
{
|
||||
argument_index: 0,
|
||||
type: "string",
|
||||
text: "authorization=Bearer console-secret",
|
||||
},
|
||||
{ argument_index: 1, type: "number", text: "42" },
|
||||
],
|
||||
retained_bytes:
|
||||
Buffer.byteLength("authorization=Bearer console-secret") +
|
||||
Buffer.byteLength("42"),
|
||||
});
|
||||
expect(result.value.network.requests[0]?.body_shapes.status).toBe(
|
||||
"included",
|
||||
);
|
||||
expect(result.value.network.websocket_events[0]?.payload_shape).toEqual({
|
||||
format: "json",
|
||||
json_shape: expect.objectContaining({ root_type: "object" }),
|
||||
});
|
||||
});
|
||||
|
||||
describe("CdpBrowserProvider: malformed approved payloads", () => {
|
||||
it("fails closed on malformed approved response and binary payload encodings", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
invalidResponseBodyBase64: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, describe } from "vitest";
|
||||
import { afterEach } from "vitest";
|
||||
|
||||
import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
|
||||
@@ -11,5 +11,3 @@ afterEach(async () => {
|
||||
export const trackBrowser = (browser: FakeCdpBrowser): void => {
|
||||
browsers.push(browser);
|
||||
};
|
||||
|
||||
export const describeBrowser = describe;
|
||||
@@ -1,29 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describeBrowser("CdpBrowserProvider navigation timeline", () => {
|
||||
it("accepts caller-selected windows and rejects the removed event-count option", () => {
|
||||
const accepted = observeWebSessionInputSchema.safeParse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://app.example.test"],
|
||||
target_id: "page-1",
|
||||
observation_ms: 120_000,
|
||||
});
|
||||
expect(accepted.success).toBe(true);
|
||||
expect(
|
||||
observeWebSessionInputSchema.safeParse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://app.example.test"],
|
||||
target_id: "page-1",
|
||||
max_timeline_events: 1,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
describe("CdpBrowserProvider navigation timeline", () => {
|
||||
it("returns every event beyond the former default collection cap", async () => {
|
||||
const additionalEvents = 2_001;
|
||||
const browser = await startFakeCdpBrowser({
|
||||
|
||||
@@ -1,52 +1,61 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { CdpCaptureCompleteness } from "../../../src/browser/CdpCaptureCompleteness.js";
|
||||
import {
|
||||
captureFrames,
|
||||
captureResources,
|
||||
} from "../../../src/browser/CdpCaptureDocuments.js";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
it("reports malformed child frame entries instead of silently completing inventory", () => {
|
||||
const completeness = new CdpCaptureCompleteness();
|
||||
const captured = captureFrames(
|
||||
{
|
||||
frameTree: {
|
||||
frame: {
|
||||
id: "main",
|
||||
url: "https://example.test/",
|
||||
},
|
||||
childFrames: [
|
||||
null,
|
||||
{ frame: { id: "child", url: "https://example.test/child" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
new Set(["https://example.test"]),
|
||||
undefined,
|
||||
completeness,
|
||||
it("reports malformed child frame entries through the CDP boundary", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandResult: (command, origin) =>
|
||||
command.method === "Page.getFrameTree"
|
||||
? {
|
||||
frameTree: {
|
||||
frame: { id: "main", url: `${origin}/` },
|
||||
childFrames: [
|
||||
null,
|
||||
{ frame: { id: "child", url: `${origin}/child` } },
|
||||
],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
trackBrowser(browser);
|
||||
|
||||
const result = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(captured.items.map(({ frame_id }) => frame_id)).toEqual([
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.frames.map(({ frame_id }) => frame_id)).toEqual([
|
||||
"main",
|
||||
"child",
|
||||
]);
|
||||
expect(completeness.snapshot().unavailable_sections).toContain("frames");
|
||||
expect(result.value.completeness.unavailable_sections).toContain("frames");
|
||||
});
|
||||
|
||||
it("reports missing top-level frame trees for frame and resource inventories", () => {
|
||||
const completeness = new CdpCaptureCompleteness();
|
||||
expect(
|
||||
captureFrames(
|
||||
{},
|
||||
new Set(["https://example.test"]),
|
||||
undefined,
|
||||
completeness,
|
||||
).items,
|
||||
).toEqual([]);
|
||||
expect(
|
||||
captureResources({}, new Set(["https://example.test"]), completeness).items,
|
||||
).toEqual([]);
|
||||
expect(completeness.snapshot().unavailable_sections).toEqual(
|
||||
expect.arrayContaining(["frames", "resources"]),
|
||||
it("marks a missing resource frame tree incomplete through CDP", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandResult: (command) =>
|
||||
command.method === "Page.getResourceTree" ? {} : undefined,
|
||||
});
|
||||
trackBrowser(browser);
|
||||
|
||||
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;
|
||||
expect(result.value.resources).toEqual([]);
|
||||
expect(result.value.completeness.unavailable_sections).toContain("resources");
|
||||
});
|
||||
@@ -1,65 +1,8 @@
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import { CdpCaptureEvents } from "../../../src/browser/CdpCaptureEvents.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import type { CdpEvent } from "../../../src/browser/CdpConnection.js";
|
||||
|
||||
const events = (): CdpCaptureEvents =>
|
||||
new CdpCaptureEvents(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://example.test"],
|
||||
target_id: "page-1",
|
||||
}),
|
||||
new Set(["https://example.test"]),
|
||||
);
|
||||
|
||||
const frameNavigated = (url: string, loaderId: string): CdpEvent => ({
|
||||
method: "Page.frameNavigated",
|
||||
params: {
|
||||
frame: { id: "main", url, loaderId },
|
||||
},
|
||||
});
|
||||
|
||||
it("ignores a repeated frameNavigated for the document already committed", () => {
|
||||
const state = events();
|
||||
state.beginAuthorizedFrame("main");
|
||||
state.ingest(frameNavigated("https://example.test/page", "loader-1"));
|
||||
expect(state.navigationDuringCapture).toBe(true);
|
||||
state.navigationDuringCapture = false;
|
||||
|
||||
// A re-fire with the same URL and loader is not a navigation.
|
||||
state.ingest(frameNavigated("https://example.test/page", "loader-1"));
|
||||
expect(state.navigationDuringCapture).toBe(false);
|
||||
});
|
||||
|
||||
it("still reports a genuine change of document", () => {
|
||||
const state = events();
|
||||
state.beginAuthorizedFrame("main");
|
||||
state.ingest(frameNavigated("https://example.test/page", "loader-1"));
|
||||
state.navigationDuringCapture = false;
|
||||
state.ingest(frameNavigated("https://example.test/other", "loader-2"));
|
||||
expect(state.navigationDuringCapture).toBe(true);
|
||||
});
|
||||
|
||||
it("does not forget a navigation that happened during the observation window", () => {
|
||||
const state = events();
|
||||
state.beginAuthorizedFrame("main");
|
||||
state.ingest(frameNavigated("https://example.test/page", "loader-1"));
|
||||
// The final capture re-attaches to the resolved frame; clearing the flag here
|
||||
// used to report a clean capture while carrying pre-navigation state.
|
||||
state.beginFinalCapture("main");
|
||||
expect(state.navigationDuringCapture).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a settled observation window clean", () => {
|
||||
const state = events();
|
||||
state.beginAuthorizedFrame("main");
|
||||
state.beginFinalCapture("main");
|
||||
expect(state.navigationDuringCapture).toBe(false);
|
||||
});
|
||||
|
||||
it("captures a page without aborting on a duplicate frame event", async () => {
|
||||
const browser = await startFakeCdpBrowser({ sessionTimeline: "same_origin" });
|
||||
|
||||
@@ -199,25 +199,6 @@ describe("CdpElectronProvider target selection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron request schemas", () => {
|
||||
it("rejects unknown request fields", () => {
|
||||
expect(
|
||||
listElectronTargetsInputSchema.safeParse({
|
||||
cdp_endpoint: "http://127.0.0.1:9223",
|
||||
unknown_field: true,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
inspectElectronPageInputSchema.safeParse({
|
||||
cdp_endpoint: "http://127.0.0.1:9223",
|
||||
target_id: "page-1",
|
||||
observation_ms: 0,
|
||||
unknown_field: true,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CdpElectronProvider capture", () => {
|
||||
it("captures Electron script source only after separate approval", async () => {
|
||||
const root = await electronFixture();
|
||||
|
||||
@@ -122,8 +122,11 @@ it("retains active hook lifecycle and IPC evidence without a count ceiling", asy
|
||||
const script = join(root, "exercise.cjs");
|
||||
await writeFile(script, activeCaptureSource);
|
||||
const hook = join(process.cwd(), "scripts/electron-active-hook.cjs");
|
||||
const output = await runNode(hook, script, root, root);
|
||||
const result = JSON.parse(output.trim().split("\n").at(-1) ?? "null") as {
|
||||
const execution = await runNode(hook, script, root, root);
|
||||
expect(execution.code).toBe(0);
|
||||
const result = JSON.parse(
|
||||
execution.stdout.trim().split("\n").at(-1) ?? "null",
|
||||
) as {
|
||||
readonly default_popup_decision: { readonly action: string };
|
||||
readonly popup_decision: { readonly action: string };
|
||||
readonly snapshot: {
|
||||
@@ -181,7 +184,7 @@ it("preserves fatal process termination after recording uncaught exceptions", as
|
||||
const script = join(root, "fatal.cjs");
|
||||
await writeFile(script, 'throw new Error("fatal-hook-secret");\n');
|
||||
const hook = join(process.cwd(), "scripts/electron-active-hook.cjs");
|
||||
const result = await runNodeStatus(hook, script, root, root);
|
||||
const result = await runNode(hook, script, root, root);
|
||||
|
||||
expect(result.code).not.toBe(0);
|
||||
expect(result.stderr).toContain("fatal-hook-secret");
|
||||
@@ -192,7 +195,11 @@ const runNode = (
|
||||
script: string,
|
||||
moduleRoot: string,
|
||||
cwd: string,
|
||||
): Promise<string> =>
|
||||
): Promise<{
|
||||
readonly code: number | null;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["-r", hook, script], {
|
||||
cwd,
|
||||
@@ -207,44 +214,11 @@ const runNode = (
|
||||
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
|
||||
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
if (code === 0) {
|
||||
const text = Buffer.concat(stdout).toString("utf8");
|
||||
if (text.length > 0) resolve(text);
|
||||
else
|
||||
reject(
|
||||
new Error(
|
||||
`hook fixture produced no output: ${Buffer.concat(stderr).toString("utf8")}`,
|
||||
),
|
||||
);
|
||||
} else
|
||||
reject(
|
||||
new Error(
|
||||
`hook fixture exited with ${String(code)}: ${Buffer.concat(stderr).toString("utf8")}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const runNodeStatus = (
|
||||
hook: string,
|
||||
script: string,
|
||||
moduleRoot: string,
|
||||
cwd: string,
|
||||
): Promise<{ readonly code: number | null; readonly stderr: string }> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["-r", hook, script], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: moduleRoot,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const stderr: Buffer[] = [];
|
||||
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) =>
|
||||
resolve({ code, stderr: Buffer.concat(stderr).toString("utf8") }),
|
||||
resolve({
|
||||
code,
|
||||
stdout: Buffer.concat(stdout).toString("utf8"),
|
||||
stderr: Buffer.concat(stderr).toString("utf8"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -22,16 +22,6 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("web capture diff", () => {
|
||||
it("rejects caller limits that would omit difference records", () => {
|
||||
expect(
|
||||
compareWebCapturesInputSchema.safeParse({
|
||||
before: {},
|
||||
after: {},
|
||||
max_changes: 1,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("reports stable observed changes while preserving unknown dimensions", async () => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
browsers.push(browser);
|
||||
|
||||
Reference in new issue
Block a user