262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
inspectWebPageInputSchema,
|
|
webPageInspectionSchema,
|
|
} from "./browserObservation.js";
|
|
import { analyzeCapturedWebBundle } from "./webBundleAnalyzer.js";
|
|
import {
|
|
analyzeWebBundleInputSchema,
|
|
webBundleAnalysisSchema,
|
|
} from "./webBundleAnalysis.js";
|
|
import { createWebTextArtifact } from "./webContentArtifact.js";
|
|
|
|
const origin = "https://app.example.test";
|
|
|
|
describe("web bundle analyzer", () => {
|
|
it("extracts bounded 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), input());
|
|
|
|
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=%5BREDACTED%5D`,
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "dynamic_import",
|
|
specifier: "./lazy.js",
|
|
}),
|
|
]),
|
|
);
|
|
expect(result.observations.routes).toEqual([
|
|
expect.objectContaining({ value: "/users/:id?token=%5BREDACTED%5D" }),
|
|
]);
|
|
expect(result.observations.endpoints).toEqual([
|
|
expect.objectContaining({
|
|
value: "/api/users?authorization=%5BREDACTED%5D",
|
|
}),
|
|
]);
|
|
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)).not.toContain("authorization=secret");
|
|
expect(result.completeness.status).toBe("complete_within_limits");
|
|
});
|
|
|
|
it("reports parser gaps without imposing a finding cap", () => {
|
|
const malformed = inspection("function broken( {");
|
|
const failed = analyzeCapturedWebBundle(malformed, input());
|
|
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');"),
|
|
input(),
|
|
);
|
|
expect(analyzed.observations.endpoints).toHaveLength(3);
|
|
expect(analyzed.completeness.status).toBe("complete_within_limits");
|
|
});
|
|
|
|
it("propagates source-map truncation into completeness and unknowns", () => {
|
|
const analysis = analyzeCapturedWebBundle(
|
|
inspection("export {};"),
|
|
input(),
|
|
{
|
|
status: "truncated",
|
|
requested: 2,
|
|
processed: 1,
|
|
dropped: 1,
|
|
dropped_script_keys: [`scr_${"2".repeat(64)}`],
|
|
items: [
|
|
{
|
|
script_key: `scr_${"1".repeat(64)}`,
|
|
declared_url: "https://app.example.test/app.js.map",
|
|
status: "truncated",
|
|
artifact: null,
|
|
original_sources: [],
|
|
original_module_edges: [],
|
|
mappings: [],
|
|
limitation: "Source-map response exceeded the byte budget.",
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
expect(analysis.completeness.status).toBe("truncated");
|
|
expect(analysis.unknowns).toContainEqual({
|
|
dimension: "source_maps",
|
|
reason: "Source-map evidence was truncated by provider safety limits",
|
|
affected_script_keys: [`scr_${"1".repeat(64)}`, `scr_${"2".repeat(64)}`],
|
|
});
|
|
});
|
|
|
|
it("rejects inconsistent source-map coverage counts", () => {
|
|
const result = analyzeCapturedWebBundle(inspection("export {};"), input());
|
|
expect(
|
|
webBundleAnalysisSchema.safeParse({
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
source_maps: {
|
|
...result.observations.source_maps,
|
|
requested: 1,
|
|
},
|
|
},
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
const input = (overrides: Record<string, unknown> = {}) =>
|
|
analyzeWebBundleInputSchema.parse({
|
|
...inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: "http://127.0.0.1:9222",
|
|
allowed_origins: [origin],
|
|
target_id: "page-1",
|
|
include_script_sources: true,
|
|
}),
|
|
...overrides,
|
|
});
|
|
|
|
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,
|
|
truncated_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: [],
|
|
});
|