Files
rea/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts
T
morluto 9c25dc4ed2 fix!: repair boundary contracts across CLI and MCP (#597)
* fix(comparisons)!: preserve unknowns across partial observations

Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback.

BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback.

* fix(javascript): preserve projected result binding semantics

Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases.

* fix(native)!: admit complete parser values and preserve typed failures

Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes.

BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field.

* fix(process): preserve host identity and actionable boundary failures

Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage.

* fix(browser): preserve cleanup and document-transition evidence

Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames.

* fix(adapters): align managed targets and typed CLI outcomes

Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results.

* docs: refresh boundary contracts and shipped roadmap behavior
2026-10-06 00:50:38 +08:00

503 lines
17 KiB
TypeScript

import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
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);
});
it("captures approved accessibility text inline", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const request = inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
include_accessibility_text: true,
});
const result = await new CdpBrowserProvider().inspectPage(request);
if (!result.ok) throw result.error;
expect(result.value.accessibility).toMatchObject({
text_capture: {
status: "included",
retained_bytes: 27,
},
nodes: [expect.objectContaining({ name: "Submit report" })],
});
});
it("includes allowed script source only after explicit approval", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
include_script_sources: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.scripts.items[0]?.source).toEqual(
expect.objectContaining({
included: true,
artifact: expect.objectContaining({
text: "export const observed = 'source-secret';",
bytes: 40,
sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
}),
}),
);
expect(browser.commands.map((command) => command.method)).toContain(
"Debugger.getScriptSource",
);
});
it("fetches and validates source maps only after separate approval", async () => {
const browser = await startFakeCdpBrowser({
sourceMapBody: JSON.stringify({
version: 3,
names: [],
sources: ["src/main.ts"],
sourcesContent: ["export const original = true;"],
mappings: "AAAA",
}),
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().analyzeBundle(
analyzeWebBundleInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
fetch_source_maps: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.observations.source_maps).toMatchObject({
status: "included",
items: [
{
status: "included",
original_sources: [
expect.objectContaining({
source: `${browser.allowedOrigin}/src/main.ts`,
}),
],
mappings: [expect.any(Object)],
},
],
});
const sourceMapRequest = browser.httpRequests.find(({ url }) =>
url.startsWith("/app.js.map"),
);
expect(sourceMapRequest).toEqual({
url: "/app.js.map?token=map-secret",
authorization: undefined,
cookie: undefined,
referer: undefined,
});
});
});
describeBrowser("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,
webMcpChildTransientBlank: true,
extraCollections: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.tools.items.map(({ name }) => name)).not.toContain(
"child_tool",
);
expect(result.value.completeness.attach_limited_sections).toContain(
"webmcp_tools",
);
expect(
result.value.completeness.excluded.find(
({ section }) => section === "webmcp_tools",
)?.count,
).toBe(1);
});
it("accepts fresh child registrations after a stable frame commit", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
webMcpChildRecoversAfterTransient: true,
webMcpChildTransientBlank: true,
extraCollections: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.tools.items).toContainEqual(
expect.objectContaining({ name: "child_tool" }),
);
expect(result.value.completeness.attach_limited_sections).not.toContain(
"webmcp_tools",
);
});
it("drops registrations from a child document when that frame commits a replacement", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
webMcpChildNavigatesAllowed: true,
extraCollections: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.tools.items.map(({ name }) => name)).not.toContain(
"child_tool",
);
expect(
result.value.completeness.excluded.find(
({ section }) => section === "webmcp_tools",
)?.count,
).toBe(1);
});
});
describeBrowser("CdpBrowserProvider: document script 2", () => {
it("discovers untrusted WebMCP declarations without registering or invoking them", async () => {
const browser = await startFakeCdpBrowser({ webMcpTools: true });
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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).toMatchObject({
status: "available",
tools: {
total: 1,
items: [
{
name: "search_orders",
description: "Search orders; authorization=Bearer tool-secret",
declaration_kind: "declarative",
owner_origin: browser.allowedOrigin,
annotations: {
read_only: true,
untrusted_content: true,
autosubmit: false,
},
trust: "page-declared-untrusted",
registration_source: {
url: `${browser.allowedOrigin}/app.js?token=tool-source-secret`,
line: 12,
column: 4,
},
},
],
},
});
const serialized = JSON.stringify(result.value);
expect(serialized).toContain("tool-secret");
expect(serialized).toContain("tool-source-secret");
for (const secret of ["schema-secret", "private-tool-secret"])
expect(serialized).not.toContain(secret);
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("WebMCP.enable");
expect(methods).not.toContain("WebMCP.invokeTool");
expect(methods).not.toContain("Runtime.evaluate");
});
it("reports WebMCP unavailable when the experimental domain is absent", async () => {
const browser = await startFakeCdpBrowser({
unsupportedMethods: ["WebMCP.enable"],
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.status).toBe("unavailable");
expect(result.value.tools.items).toEqual([]);
expect(result.value.completeness.unavailable_sections).toContain(
"webmcp_tools",
);
});
it("rejects WebMCP evidence when the page leaves its approved origin", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
frameUrlAfterFirstRead: "https://private.example.test/tools",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "BrowserObservationError",
operation: "discover_webmcp_tools",
reason: "target_not_allowed",
},
});
expect(browser.commands.map(({ method }) => method)).not.toContain(
"WebMCP.invokeTool",
);
});
it("returns every WebMCP registration in the replay", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
extraCollections: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.tools).toMatchObject({
total: 2,
items: [expect.any(Object), expect.any(Object)],
});
expect(result.value.completeness.truncated_sections).not.toContain(
"webmcp_tools",
);
});
it("retains registrations from authorized frames beyond the former frame cap", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
webMcpFrameCount: 1_001,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.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.tools.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "beyond_frame_limit_tool",
frame_id: "webmcp-frame-1000",
}),
]),
);
expect(result.value.completeness.truncated_sections).not.toContain(
"webmcp_tools",
);
});
});
describeBrowser("CdpBrowserProvider WebMCP inventory completeness", () => {
it("retains every field in a large declared WebMCP input schema", async () => {
const propertyCount = 5_001;
const browser = await startFakeCdpBrowser({
webMcpTools: true,
webMcpSchemaPropertyCount: propertyCount,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
const shape = result.value.tools.items[0]?.input_schema_shape;
expect(
shape?.properties.filter(({ path }) =>
path.startsWith("/properties/field_"),
),
).toHaveLength(propertyCount * 2);
expect(shape?.properties).toContainEqual({
path: "/properties/field_5000/type",
types: ["string"],
observations: 1,
});
});
});
describeBrowser("CdpBrowserProvider: document script 3", () => {
it("removes WebMCP declarations after their child frame leaves scope", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
extraCollections: true,
webMcpChildLeavesScope: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
const names = result.value.tools.items.map(({ name }) => name);
expect(names).toEqual(
expect.arrayContaining(["search_orders", "update_order"]),
);
expect(names).not.toEqual(
expect.arrayContaining(["child_tool", "escaped_child_tool"]),
);
expect(result.value.completeness.policy_filtered_sections).toContain(
"webmcp_tools",
);
expect(JSON.stringify(result.value)).not.toContain(
"cross-origin-child-secret",
);
});
it("captures an explicitly approved content-addressed viewport screenshot", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const result = await new CdpBrowserProvider().captureScreenshot(
captureWebScreenshotInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
}),
);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
viewport: { width: 1, height: 1 },
artifact: {
sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
bytes: 70,
media_type: "image/png",
},
});
expect(browser.commands.map(({ method }) => method)).toContain(
"Page.captureScreenshot",
);
});
it("discards screenshot pixels when the main frame navigates during capture", async () => {
const browser = await startFakeCdpBrowser({
navigateDuringScreenshotUrl: "https://private.example.test/screenshot",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().captureScreenshot(
captureWebScreenshotInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
}),
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "BrowserObservationError",
operation: "capture_web_screenshot",
reason: "target_not_allowed",
},
});
});
});
describeBrowser("CdpBrowserProvider: document script 4", () => {
it("returns every frame, resource, worker, accessibility, and storage inventory item", async () => {
const browser = await startFakeCdpBrowser({ extraCollections: true });
trackBrowser(browser);
const request = inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
observation_ms: 0,
include_storage_keys: true,
});
const result = await new CdpBrowserProvider().inspectPage(request);
if (!result.ok) throw result.error;
expect(result.value.completeness.truncated_sections).toEqual([]);
expect(result.value.frames.length).toBeGreaterThan(1);
expect(result.value.resources.length).toBeGreaterThan(1);
expect(result.value.workers.length).toBeGreaterThan(1);
expect(result.value.accessibility).toMatchObject({ total_nodes: 4 });
expect(result.value.accessibility.nodes).toHaveLength(4);
expect(result.value.storage.local_storage_keys.length).toBeGreaterThan(1);
expect(result.value.storage.session_storage_keys.length).toBeGreaterThan(1);
expect(result.value.storage.indexed_db_names).toHaveLength(2);
expect(result.value.storage.cache_names).toHaveLength(2);
expect(result.value.storage.content_fingerprints).toEqual([]);
});
});