rea/src/domain/webBundleAnalyzer.test.ts

457 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { webPageInspectionSchema } from "./browserObservation.js";
import { analyzeCapturedWebBundle } from "./webBundleAnalyzer.js";
import { webBundleAnalysisSchema } from "./webBundleAnalysis.js";
import { createWebTextArtifact } from "./webContentArtifact.js";
const origin = "https://app.example.test";
describe("web bundle analyzer", () => {
it("extracts graph, route, endpoint, vendor, and WebMCP evidence", () => {
const source = `
import { createApp } from "./chunk.js?token=secret";
const lazy = import("./lazy.js");
const routes = [{ path: "/users/:id?token=secret" }];
fetch("/api/users?authorization=secret");
document.modelContext.registerTool({
name: "lookup-user",
description: "Untrusted page declaration",
inputSchema: {
type: "object",
properties: {
userId: { type: "string" },
verbose: { type: "boolean" }
}
}
});
const __webpack_require__ = () => createApp(routes, lazy);
`;
const result = analyzeCapturedWebBundle(inspection(source));
expect(result.observations.chunks.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "static_import",
specifier: "./chunk.js?token=secret",
resolved_url: `${origin}/assets/chunk.js?token=secret`,
}),
expect.objectContaining({
kind: "dynamic_import",
specifier: "./lazy.js",
}),
]),
);
expect(result.observations.routes).toEqual([
expect.objectContaining({ value: "/users/:id?token=secret" }),
]);
expect(result.observations.endpoints).toEqual([
expect.objectContaining({
value: "/api/users?authorization=secret",
}),
]);
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
name: "lookup-user",
trust: "page-declared-untrusted",
schema_property_names: ["userId", "verbose"],
}),
]);
expect(result.inferences).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: "webpack", confidence: "high" }),
expect.objectContaining({ value: "Vue" }),
]),
);
expect(JSON.stringify(result)).toContain("authorization=secret");
expect(result.completeness.status).toBe("complete");
});
it("reports parser gaps without imposing a finding cap", () => {
const malformed = inspection("function broken( {");
const failed = analyzeCapturedWebBundle(malformed);
expect(failed.completeness).toMatchObject({
status: "partial",
parse_failures: 1,
});
expect(failed.unknowns[0]?.dimension).toBe("javascript_ast");
const analyzed = analyzeCapturedWebBundle(
inspection("fetch('/one'); fetch('/two'); fetch('/three');"),
);
expect(analyzed.observations.endpoints).toHaveLength(3);
expect(analyzed.completeness.status).toBe("complete");
});
it("analyzes AST nodes after the former fixed node ceiling", () => {
const source = `${";".repeat(250_001)}fetch('/last');`;
const result = analyzeCapturedWebBundle(inspection(source));
expect(result.observations.endpoints).toContainEqual(
expect.objectContaining({ value: "/last" }),
);
expect(result.completeness.visited_ast_nodes).toBeGreaterThan(250_000);
expect(result.completeness.status).toBe("complete");
});
it("retains complete long chunk and WebMCP declaration text", () => {
const specifier = `./${"chunk".repeat(1_000)}.js`;
const name = "tool".repeat(100);
const description = "untrusted declaration ".repeat(150);
const propertyName = "property".repeat(100);
const result = analyzeCapturedWebBundle(
inspection(`
import "${specifier}";
document.modelContext.registerTool({
name: "${name}",
description: "${description}",
inputSchema: { properties: { "${propertyName}": { type: "string" } } }
});
`),
);
expect(result.observations.chunks.edges[0]?.specifier).toBe(specifier);
expect(result.observations.webmcp_declarations[0]).toMatchObject({
name,
description,
schema_property_names: [propertyName],
trust: "page-declared-untrusted",
});
});
it("reports unavailable source maps as partial without dropping requested maps", () => {
const analysis = analyzeCapturedWebBundle(inspection("export {};"), {
status: "unavailable",
requested: 1,
processed: 1,
items: [
{
script_key: `scr_${"1".repeat(64)}`,
declared_url: "https://app.example.test/app.js.map",
status: "fetch_failed",
artifact: null,
original_sources: [],
original_module_edges: [],
mappings: [],
limitation: "Source-map fetch failed.",
},
],
});
expect(analysis.completeness.status).toBe("partial");
expect(analysis.unknowns).toContainEqual({
dimension: "source_maps",
reason:
"One or more requested source maps were unavailable or incomplete",
affected_script_keys: [`scr_${"1".repeat(64)}`],
});
});
it("rejects inconsistent source-map coverage counts", () => {
const result = analyzeCapturedWebBundle(inspection("export {};"));
expect(
webBundleAnalysisSchema.safeParse({
...result,
observations: {
...result.observations,
source_maps: {
...result.observations.source_maps,
requested: 1,
},
},
}).success,
).toBe(false);
});
});
describe("web bundle static-analysis parity", () => {
it("recognizes endpoints through nested dynamic receivers like static analysis", () => {
const result = analyzeCapturedWebBundle(
inspection('window[recv].fetch("/nested-fetch");'),
);
expect(result.observations.endpoints).toContainEqual(
expect.objectContaining({ value: "/nested-fetch" }),
);
});
it("reads open endpoints only from XMLHttpRequest method and URL pairs", () => {
const result = analyzeCapturedWebBundle(
inspection(`
xhr.open("GET", "/xhr-get");
xhr.open("post", "https://xhr.example.test/submit");
dav.open("PROPFIND", "/dav/");
fs.open("/tmp/data.txt", "r", done);
window.open("https://help.example.test/", "_blank");
xhr.open(method, "/dynamic-method");
fs.open(path, "r", done);
window.open(url, "_blank");
`),
);
expect(
result.observations.endpoints.map(({ value }) => value).sort(),
).toEqual([
"/dav/",
"/dynamic-method",
"/xhr-get",
"https://xhr.example.test/submit",
]);
});
it("recognizes loadURL endpoints like static analysis", () => {
const result = analyzeCapturedWebBundle(
inspection('window.win.loadURL("https://embedded.test/app");'),
);
expect(result.observations.endpoints).toContainEqual(
expect.objectContaining({ value: "https://embedded.test/app" }),
);
});
});
describe("web bundle artifact metadata", () => {
it("preserves long provider media types in artifact summaries", () => {
const result = analyzeCapturedWebBundle(inspection("export {};"));
const mediaType = `application/${"x".repeat(300)}`;
const artifact = result.capture.source_artifacts[0] ?? {
sha256: "0".repeat(64),
bytes: 0,
media_type: "application/javascript",
charset: "utf-8" as const,
text_available: true as const,
};
expect(
webBundleAnalysisSchema.parse({
...result,
capture: {
...result.capture,
source_artifacts: [{ ...artifact, media_type: mediaType }],
},
}).capture.source_artifacts[0]?.media_type,
).toBe(mediaType);
});
});
const inspection = (source: string) =>
webPageInspectionSchema.parse({
browser: {
product: "Fake Chrome",
protocol_version: "1.3",
revision: "1",
user_agent: "fake",
js_version: "1",
},
target: {
target_id: "page-1",
type: "page",
title: "App",
url: `${origin}/app`,
origin,
attached: false,
},
capture_window: {
started_at: "2026-07-14T00:00:00.000Z",
ended_at: "2026-07-14T00:00:01.000Z",
observation_ms: 1_000,
},
completeness: {
status: "attach_limited",
conditions: ["attach_limited"],
policy_filtered_sections: [],
attach_limited_sections: ["network_requests"],
truncated_sections: [],
unavailable_sections: [],
excluded: [],
dropped_events: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: 0,
total: 0,
},
},
frames: [],
dom: { total_nodes: 0, nodes: [] },
accessibility: {
total_nodes: 0,
text_capture: {
status: "not_approved",
retained_bytes: 0,
excluded_fields: 0,
},
nodes: [],
},
scripts: {
total: 1,
items: [
{
script_key: `scr_${"1".repeat(64)}`,
url: `${origin}/assets/app.js`,
origin,
cdp_hash: "hash",
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source_map_url: null,
resource_reconciliation: {
status: "unmatched",
reason: "no_exact_sanitized_url",
},
source: {
included: true,
artifact: createWebTextArtifact(source, "text/javascript"),
},
},
],
},
resources: [],
network: {
requests: [],
websocket_events: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
console: {
events: [],
coverage_started_at: "2026-07-14T00:00:00.000Z",
prior_activity_available: false,
},
workers: [],
metadata: {
responses: [],
dom_urls: [],
agent_hints: [],
excluded_dom_urls: 0,
headers_allowlisted: true,
},
storage: {
origin,
usage_bytes: null,
quota_bytes: null,
local_storage_keys: [],
session_storage_keys: [],
indexed_db_names: [],
cache_names: [],
values_redacted: true,
},
limitations: [],
});
it("keeps bare module locations unresolved without inventing URL-relative paths", () => {
const result = analyzeCapturedWebBundle(
inspection(`
import "package-name";
import("@scope/package");
require("package-name");
import "./actual.js";
importScripts("worker.js");
`),
);
for (const specifier of ["package-name", "@scope/package"])
expect(
result.observations.chunks.edges
.filter((edge) => edge.specifier === specifier)
.every((edge) => edge.resolved_url === null),
).toBe(true);
expect(
result.observations.chunks.edges.find(
(edge) => edge.specifier === "./actual.js",
)?.resolved_url,
).toBe(`${origin}/assets/actual.js`);
expect(
result.observations.chunks.edges.find(
(edge) => edge.specifier === "worker.js",
)?.resolved_url,
).toBe(`${origin}/assets/worker.js`);
});
it("retains each literal importScripts argument in source order", () => {
const result = analyzeCapturedWebBundle(
inspection(
'importScripts("first.js", dynamicValue, "second.js", "first.js");',
),
);
expect(
result.observations.chunks.edges
.filter(({ kind }) => kind === "worker_import")
.map(({ specifier }) => specifier),
).toEqual(["first.js", "second.js"]);
});
describe("web metadata key semantics", () => {
it("keeps computed metadata keys and overwrite order conservative", () => {
const result = analyzeCapturedWebBundle(
inspection(`
const path = "description";
const routes = [{ [path]: "/not-a-route" }, { ["path"]: "/real" }];
document.modelContext.registerTool({
name: "old", name: "new",
inputSchema: { properties: { [name]: {}, literal: {} } }
});
document.modelContext.registerTool({ name: "old", ...dynamic });
document.modelContext.registerTool({ name: "old", [name]: "dynamic" });
document.modelContext.registerTool({ name: "old", get name() {} });
document.modelContext.registerTool({ ...dynamic, ["name"]: "restored" });
document.modelContext.registerTool({ name: "valid", [""]: 0 });
`),
);
expect(result.observations.routes.map(({ value }) => value)).toEqual([
"/real",
]);
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
name: "new",
schema_property_names: ["literal"],
}),
expect.objectContaining({ name: null }),
expect.objectContaining({ name: "restored" }),
expect.objectContaining({ name: "valid" }),
]);
});
});
it("does not infer call names from dynamic computed identifiers", () => {
const result = analyzeCapturedWebBundle(
inspection(`
const fetch = "render";
const require = "render";
const registerTool = "render";
window[fetch]("/render-only");
window[require]("./not-a-module.js");
document.modelContext[registerTool]({ name: "not-a-tool" });
`),
);
expect(result.observations.endpoints).toEqual([]);
expect(result.observations.chunks.edges).toEqual([]);
expect(result.observations.webmcp_declarations).toEqual([]);
});
it("retains direct and literal computed call names", () => {
const result = analyzeCapturedWebBundle(
inspection(`
window.fetch("/direct");
window["fetch"]("/literal");
window[receiver].fetch("/nested-receiver");
window.require("./direct.js");
window["require"]("./literal.js");
document.modelContext["registerTool"]({ name: "literal-tool" });
`),
);
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
"/direct",
"/literal",
"/nested-receiver",
]);
expect(
result.observations.chunks.edges.map(({ specifier }) => specifier),
).toEqual(["./direct.js", "./literal.js"]);
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({ name: "literal-tool" }),
]);
});