2186 lines
70 KiB
TypeScript
2186 lines
70 KiB
TypeScript
import {
|
|
projectAnalysisView,
|
|
analysisViewRequestSchema,
|
|
} from "./analysisView/analysisView.js";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
|
|
|
|
import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js";
|
|
import { analyzeCapturedWebScript } from "./webBundleAnalyzer.js";
|
|
import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js";
|
|
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
|
|
import { webBundleAnalysisSchema } from "./webBundleAnalysis.js";
|
|
import { isIncludedScript } from "./webBundleAnalyzerState.js";
|
|
import { webBundleScriptAnalysisSchema } from "./webBundleScriptAnalysis.js";
|
|
import { createWebTextArtifact } from "./webContentArtifact.js";
|
|
|
|
const origin = "https://app.example.test";
|
|
|
|
it("shares runtime module declarations with source analysis while retaining inline type side effects", () => {
|
|
const source = `
|
|
import type {T} from "./erased-import";
|
|
export type {T} from "./erased-export";
|
|
export type * from "./erased-all";
|
|
import {type X} from "./inline-import";
|
|
export {type Y} from "./inline-export";
|
|
import "";
|
|
export * from "./runtime";
|
|
export const local = 1;
|
|
`;
|
|
const expected = ["./inline-import", "./inline-export", "", "./runtime"];
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
expect(result.completeness.parse_failures).toBe(0);
|
|
expect(
|
|
result.observations.chunks.edges.map(({ specifier }) => specifier),
|
|
).toEqual(expected);
|
|
expect(
|
|
analyzeJavaScriptStaticSource(source)
|
|
.references.filter(({ kind }) => kind === "static-import")
|
|
.map(({ specifier }) => specifier),
|
|
).toEqual(expected);
|
|
});
|
|
|
|
it("retains standalone GraphQL Source operations in captured scripts", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
const source = {
|
|
body: "query GetUser { user { id } }",
|
|
locationOffset: { line: 1, column: 1 },
|
|
};
|
|
`),
|
|
);
|
|
expect(result.observations.endpoints).toEqual([
|
|
expect.objectContaining({ value: "GetUser", mechanism: "graphql:query" }),
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
kind: "recovered",
|
|
source: "function legacy(a, a) { return a; } fetch('/partial');",
|
|
parsed: 2,
|
|
failures: 0,
|
|
reason: "recovered",
|
|
endpoints: ["/partial", "/complete"],
|
|
},
|
|
{
|
|
kind: "failed",
|
|
source: "function broken( {",
|
|
parsed: 1,
|
|
failures: 1,
|
|
reason: "could not be parsed",
|
|
endpoints: ["/complete"],
|
|
},
|
|
])(
|
|
"attributes $kind parse uncertainty only to its affected source",
|
|
({ source, parsed, failures, reason, endpoints }) => {
|
|
const captured = inspection(source);
|
|
const good = inspection("fetch('/complete');").scripts.items[0];
|
|
if (good === undefined) throw new Error("Missing valid script fixture");
|
|
captured.scripts.items.push({ ...good, script_key: "valid-script" });
|
|
captured.scripts.total += 1;
|
|
const result = analyzeWebBundleFixture(captured);
|
|
expect(result.completeness).toMatchObject({
|
|
status: "partial",
|
|
parsed_scripts: parsed,
|
|
parse_failures: failures,
|
|
});
|
|
expect(result.unknowns).toContainEqual({
|
|
dimension: "javascript_ast",
|
|
reason: expect.stringContaining(reason),
|
|
affected_script_keys: [captured.scripts.items[0]?.script_key],
|
|
});
|
|
expect(result.observations.endpoints.map(({ value }) => value)).toEqual(
|
|
endpoints,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[
|
|
"const records = new Map(); const alias = records; alias.get('/accounts');",
|
|
false,
|
|
],
|
|
["new Map().delete('/accounts');", false],
|
|
[
|
|
"const records = new Map(); records.get('/accounts'); records.delete('/accounts');",
|
|
false,
|
|
],
|
|
[
|
|
"const records = new Map(); records.get = fetch; records.get('/accounts');",
|
|
true,
|
|
],
|
|
[
|
|
"Object.getPrototypeOf = () => Map.prototype; Object.getPrototypeOf({}).get = fetch; new Map().get('/accounts');",
|
|
true,
|
|
],
|
|
["client.get('/accounts');", true],
|
|
["eval('Map.prototype.get = fetch'); new Map().get('/accounts');", true],
|
|
] as const)(
|
|
"shares Map receiver certainty with application analysis: %s",
|
|
(source, candidate) => {
|
|
const input = source + " fetch('/network');";
|
|
const result = analyzeWebBundleFixture(inspection(input));
|
|
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
|
|
"/network",
|
|
]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value),
|
|
).toEqual(candidate ? ["/accounts"] : []);
|
|
const staticResult = analyzeJavaScriptStaticSource(input);
|
|
expect(
|
|
staticResult.endpoints
|
|
.map(({ value, interpretation }) => [value, interpretation ?? null])
|
|
.sort(),
|
|
).toEqual(
|
|
candidate
|
|
? [
|
|
["/accounts", "candidate"],
|
|
["/network", null],
|
|
]
|
|
: [["/network", null]],
|
|
);
|
|
},
|
|
);
|
|
|
|
it("discloses widened returned-reference effects only for the affected script", () => {
|
|
const captured = inspection(`
|
|
function expose() { return Map.prototype; }
|
|
let cursor = { target: expose };
|
|
function walk(flag) {
|
|
while (flag) cursor = cursor.next;
|
|
patch(cursor.target());
|
|
}
|
|
new Map().get('/candidate');
|
|
fetch('/preserved');
|
|
`);
|
|
const independent = inspection(
|
|
"new Map().get('/intrinsic'); fetch('/independent');",
|
|
).scripts.items[0];
|
|
if (independent === undefined) throw new Error("Missing independent script");
|
|
captured.scripts.items.push({ ...independent, script_key: "independent" });
|
|
captured.scripts.total += 1;
|
|
const result = analyzeWebBundleFixture(captured);
|
|
expect(result.completeness).toMatchObject({
|
|
status: "partial",
|
|
parsed_scripts: 2,
|
|
parse_failures: 0,
|
|
});
|
|
expect(result.unknowns).toContainEqual({
|
|
dimension: "javascript_ast",
|
|
reason: expect.stringContaining("returned-reference projections"),
|
|
affected_script_keys: [captured.scripts.items[0]?.script_key],
|
|
});
|
|
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
|
|
"/preserved",
|
|
"/independent",
|
|
]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value),
|
|
).toEqual(["/candidate"]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "inline declaration and schema spreads",
|
|
source: `document.modelContext.registerTool({
|
|
...{
|
|
name: "spread-tool",
|
|
description: "Copied from a literal",
|
|
inputSchema: {
|
|
...{
|
|
type: "object",
|
|
properties: { query: { type: "string" } }
|
|
}
|
|
}
|
|
}
|
|
});`,
|
|
name: "spread-tool",
|
|
description: "Copied from a literal",
|
|
properties: ["query"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "unrelated trailing literal spread",
|
|
source: `document.modelContext.registerTool({
|
|
name: "direct-tool",
|
|
description: "Explicit metadata",
|
|
inputSchema: { properties: { query: { type: "string" } } },
|
|
...{ timeout: 5 }
|
|
});`,
|
|
name: "direct-tool",
|
|
description: "Explicit metadata",
|
|
properties: ["query"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "null spread after explicit metadata",
|
|
source: `document.modelContext.registerTool({
|
|
name: "null-safe-tool",
|
|
description: "Null spread is inert",
|
|
inputSchema: { properties: { query: { type: "string" } } },
|
|
...null
|
|
});`,
|
|
name: "null-safe-tool",
|
|
description: "Null spread is inert",
|
|
properties: ["query"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "snake-case schema property from a spread",
|
|
source: `document.modelContext.registerTool({
|
|
...{
|
|
name: "snake-tool",
|
|
description: "Snake-case schema",
|
|
input_schema: { properties: { item_id: { type: "string" } } }
|
|
}
|
|
});`,
|
|
name: "snake-tool",
|
|
description: "Snake-case schema",
|
|
properties: ["item_id"],
|
|
coverage: "complete",
|
|
},
|
|
])(
|
|
"preserves WebMCP facts through $label",
|
|
({ source, name, description, properties, coverage }) => {
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({
|
|
name,
|
|
description,
|
|
schema_property_names: properties,
|
|
schema_property_names_coverage: coverage,
|
|
trust: "page-declared-untrusted",
|
|
}),
|
|
]);
|
|
expect(result.completeness.status).toBe("complete");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
label: "unknown spread after explicit facts",
|
|
source: `document.modelContext.registerTool({
|
|
name: "possibly-overridden",
|
|
description: "Possibly overridden",
|
|
inputSchema: { properties: { query: { type: "string" } } },
|
|
...dynamicOptions
|
|
});`,
|
|
name: null,
|
|
description: null,
|
|
properties: [],
|
|
coverage: "unknown",
|
|
},
|
|
{
|
|
label: "explicit facts after an unknown spread",
|
|
source: `document.modelContext.registerTool({
|
|
...dynamicOptions,
|
|
name: "known-after-spread",
|
|
description: "Explicitly restored",
|
|
inputSchema: { properties: { query: { type: "string" } } }
|
|
});`,
|
|
name: "known-after-spread",
|
|
description: "Explicitly restored",
|
|
properties: ["query"],
|
|
coverage: "complete",
|
|
},
|
|
])(
|
|
"preserves rightmost uncertainty for $label",
|
|
({ source, name, description, properties, coverage }) => {
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({
|
|
name,
|
|
description,
|
|
schema_property_names: properties,
|
|
schema_property_names_coverage: coverage,
|
|
trust: "page-declared-untrusted",
|
|
}),
|
|
]);
|
|
expect(result.completeness.status).toBe("complete");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
label: "an unknown camel-case schema override",
|
|
source: `document.modelContext.registerTool({
|
|
...dynamicOptions,
|
|
inputSchema: { properties: { camel: {} } },
|
|
...anotherDynamicOptions,
|
|
input_schema: { properties: { snake: {} } },
|
|
name: "tool",
|
|
description: "Explicit metadata"
|
|
});`,
|
|
name: "tool",
|
|
properties: [],
|
|
coverage: "unknown",
|
|
},
|
|
{
|
|
label: "an explicit null camel-case schema",
|
|
source: `document.modelContext.registerTool({
|
|
name: "null-alias",
|
|
description: "Fallback applies",
|
|
inputSchema: null,
|
|
input_schema: { properties: { snake: {} } }
|
|
});`,
|
|
name: "null-alias",
|
|
properties: ["snake"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "literal property spreads",
|
|
source: `document.modelContext.registerTool({
|
|
name: "spread-properties",
|
|
inputSchema: { properties: { ...{ query: {} }, direct: {} } }
|
|
});`,
|
|
name: "spread-properties",
|
|
properties: ["direct", "query"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "prototype setters are not schema properties",
|
|
source: `document.modelContext.registerTool({
|
|
name: "prototype-properties",
|
|
inputSchema: { properties: { __proto__: { hidden: {} }, direct: {}, ["__proto__"]: {} } }
|
|
});`,
|
|
name: "prototype-properties",
|
|
properties: ["__proto__", "direct"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "own schema properties before a prototype setter",
|
|
source: `document.modelContext.registerTool({
|
|
name: "own-before-prototype",
|
|
inputSchema: { properties: { known: {} }, __proto__: { properties: { inherited: {} } } }
|
|
});`,
|
|
name: "own-before-prototype",
|
|
properties: ["known"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "own schema properties after a prototype setter",
|
|
source: `document.modelContext.registerTool({
|
|
name: "own-after-prototype",
|
|
inputSchema: { __proto__: { properties: { inherited: {} } }, properties: { known: {} } }
|
|
});`,
|
|
name: "own-after-prototype",
|
|
properties: ["known"],
|
|
coverage: "complete",
|
|
},
|
|
{
|
|
label: "an unresolved property spread",
|
|
source: `document.modelContext.registerTool({
|
|
name: "unknown-properties",
|
|
inputSchema: { properties: { ...dynamicProperties, direct: {} } }
|
|
});`,
|
|
name: "unknown-properties",
|
|
properties: ["direct"],
|
|
coverage: "partial",
|
|
},
|
|
{
|
|
label: "an unresolved computed property key",
|
|
source: `document.modelContext.registerTool({
|
|
name: "computed-properties",
|
|
inputSchema: { properties: { [propertyName]: {}, direct: {} } }
|
|
});`,
|
|
name: "computed-properties",
|
|
properties: ["direct"],
|
|
coverage: "partial",
|
|
},
|
|
])(
|
|
"preserves WebMCP schema ownership for $label",
|
|
({ source, name, properties, coverage }) => {
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({
|
|
name,
|
|
schema_property_names: properties,
|
|
schema_property_names_coverage: coverage,
|
|
trust: "page-declared-untrusted",
|
|
}),
|
|
]);
|
|
},
|
|
);
|
|
|
|
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 = analyzeWebBundleFixture(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"],
|
|
schema_property_names_coverage: "complete",
|
|
}),
|
|
]);
|
|
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 = analyzeWebBundleFixture(malformed);
|
|
expect(failed.completeness).toMatchObject({
|
|
status: "partial",
|
|
parse_failures: 1,
|
|
});
|
|
expect(failed.unknowns[0]?.dimension).toBe("javascript_ast");
|
|
|
|
const analyzed = analyzeWebBundleFixture(
|
|
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 = analyzeWebBundleFixture(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 = analyzeWebBundleFixture(
|
|
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],
|
|
schema_property_names_coverage: "complete",
|
|
trust: "page-declared-untrusted",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("web bundle source-map coverage", () => {
|
|
it("reports unavailable source maps as partial without dropping requested maps", () => {
|
|
const analysis = analyzeWebBundleFixture(inspection("export {};"), {
|
|
status: "unavailable",
|
|
requested: 1,
|
|
processed: 1,
|
|
items: [
|
|
{
|
|
script_key: `scr_${"1".repeat(64)}`,
|
|
declared_url: "https://app.example.test/app.js.map",
|
|
effective_url: null,
|
|
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("keeps map-less captured source complete when no maps were requested", () => {
|
|
const analysis = analyzeWebBundleFixture(inspection("export {};"), {
|
|
status: "not_requested",
|
|
requested: 0,
|
|
processed: 0,
|
|
items: [],
|
|
});
|
|
|
|
expect(analysis.completeness.status).toBe("complete");
|
|
expect(analysis.unknowns).toEqual([]);
|
|
});
|
|
|
|
it("rejects inconsistent source-map coverage counts", () => {
|
|
const result = analyzeWebBundleFixture(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("qualifies network spellings through nested dynamic receivers", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection('window[recv].fetch("/nested-fetch");'),
|
|
);
|
|
expect(result.observations.endpoints).toEqual([]);
|
|
expect(result.inferences).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "endpoint_candidate",
|
|
value: "/nested-fetch",
|
|
confidence: "low",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reads open endpoints only from XMLHttpRequest method and URL pairs", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
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");
|
|
window.open(url, "/preview");
|
|
globalThis.open(url, "./frame");
|
|
document.open("text/html", "/replace");
|
|
parent.open("GET", "/parent-xhr");
|
|
self.open("post", "/self-xhr");
|
|
top.open("GET", "/top-target");
|
|
opener.open("GET", "/opener-target");
|
|
frames.open("GET", "/frames-target");
|
|
globalThis.open("GET", "/global-target");
|
|
document.open("GET", "/document-target");
|
|
const parentWindow = parent;
|
|
parentWindow.open("GET", "/alias-target");
|
|
{
|
|
const parent = new XMLHttpRequest();
|
|
parent.open(method, "/shadowed-parent-xhr");
|
|
}
|
|
xhr.open(method, "api/relative");
|
|
fs.open(path, "w+", done);
|
|
popup.open(url, "_TOP");
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value }) => value).sort(),
|
|
).toEqual(["/shadowed-parent-xhr"]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual([
|
|
"/dav/",
|
|
"/dynamic-method",
|
|
"/xhr-get",
|
|
"api/relative",
|
|
"https://xhr.example.test/submit",
|
|
]);
|
|
expect(result.inferences).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "endpoint_candidate",
|
|
value: "api/relative",
|
|
confidence: "low",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("web bundle API classification parity", () => {
|
|
it("reads storage open versions as storage rather than endpoints in both analyzers", () => {
|
|
const source = `
|
|
indexedDB.open(databaseName, "2");
|
|
window.indexedDB.open(databaseName, "/v3");
|
|
indexedDB.open("APP", "4");
|
|
indexedDB.open("GET", "/storage-named-xhr");
|
|
`;
|
|
expect(
|
|
analyzeWebBundleFixture(inspection(source))
|
|
.inferences.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value),
|
|
).toEqual(["/storage-named-xhr"]);
|
|
const analysis = analyzeJavaScriptStaticSource(source);
|
|
expect(analysis.endpoints.map(({ value }) => value)).toEqual([
|
|
"/storage-named-xhr",
|
|
]);
|
|
expect(analysis.storage.map(({ kind }) => kind)).toEqual([
|
|
"indexed-db",
|
|
"indexed-db",
|
|
"indexed-db",
|
|
"indexed-db",
|
|
]);
|
|
});
|
|
|
|
it("uses lexical receiver facts for overloaded open calls", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
const popup = window;
|
|
popup.open(url, "/preview");
|
|
{
|
|
const indexedDB = { open() {} };
|
|
indexedDB.open("PROPFIND", "/dav");
|
|
indexedDB.open(databaseName, "2");
|
|
}
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open(method, "/api");
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value }) => value).sort(),
|
|
).toEqual(["/api"]);
|
|
});
|
|
|
|
it("classifies optional and one-hop aliased open calls", () => {
|
|
const source = `
|
|
const popup = window;
|
|
popup?.open(url, "/preview");
|
|
const idb = indexedDB;
|
|
idb.open(databaseName, "2");
|
|
const xhr = new XMLHttpRequest();
|
|
xhr?.open(method, "/api");
|
|
`;
|
|
const bundle = analyzeWebBundleFixture(inspection(source));
|
|
expect(bundle.observations.endpoints.map(({ value }) => value)).toEqual([
|
|
"/api",
|
|
]);
|
|
const staticAnalysis = analyzeJavaScriptStaticSource(source);
|
|
expect(staticAnalysis.endpoints.map(({ value }) => value)).toEqual([
|
|
"/api",
|
|
]);
|
|
expect(staticAnalysis.storage.map(({ kind }) => kind)).toEqual([
|
|
"indexed-db",
|
|
]);
|
|
});
|
|
|
|
it("keeps HTTP-method-named reads of provable keyed collections out of endpoints", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
axios.get("https://api.example.test/users");
|
|
api.get("users");
|
|
client.delete("cache-key");
|
|
new Map().get("map-key");
|
|
new URLSearchParams(location.search).get("q");
|
|
new Headers(init).delete("x-trace");
|
|
new FormData(form).get("file");
|
|
response.headers.get("content-type");
|
|
request?.headers.delete("cookie");
|
|
new URL(location.href).searchParams.get("page");
|
|
api.headers.post("/v1/headers");
|
|
api.searchParams.request("/search");
|
|
new Headers().get("/users");
|
|
api.headers.get("https://api.example.test/headers");
|
|
api.searchParams.delete("../search");
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value }) => value).sort(),
|
|
).toEqual([]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual([
|
|
"../search",
|
|
"/search",
|
|
"/users",
|
|
"/v1/headers",
|
|
"cache-key",
|
|
"https://api.example.test/headers",
|
|
"https://api.example.test/users",
|
|
"users",
|
|
]);
|
|
});
|
|
|
|
it("keeps undici header-store reads out of endpoint observations", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
response.headersList.get("location", true);
|
|
request.headersList.delete("authorization");
|
|
request.headersList.get("/header-list");
|
|
api.get("/api/users");
|
|
`),
|
|
);
|
|
expect(result.observations.endpoints).toEqual([]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value),
|
|
).toEqual(["/header-list", "/api/users"]);
|
|
});
|
|
});
|
|
|
|
describe("web bundle explicit request reference forms", () => {
|
|
it("recognizes URL, path, query and fragment forms after excluding lookup receivers", () => {
|
|
const references = [
|
|
"http://api.example.test/users",
|
|
"https://api.example.test/users?q=1#part",
|
|
"ws://api.example.test/events",
|
|
"wss://api.example.test/events",
|
|
"http:api.example.test/users",
|
|
"https:api.example.test/users?q=1&tag=a&tag=b#part",
|
|
"https:api.example.test:443/a/../b?x=a+b&x=two#part",
|
|
"ws:api.example.test/events",
|
|
"wss:api.example.test/events",
|
|
"HtTpS:/api.example.test/users",
|
|
String.raw`https:\api.example.test\users`,
|
|
"https://[invalid",
|
|
"//api.example.test/users",
|
|
"/api/users",
|
|
"./users",
|
|
"../users",
|
|
"?",
|
|
"?page=2&tag=a&tag=b#section",
|
|
"#",
|
|
"#section",
|
|
"#frag?tag=a&tag=b",
|
|
];
|
|
const ambiguous = [
|
|
"",
|
|
".",
|
|
"..",
|
|
"users",
|
|
"%23section",
|
|
"%3Fpage=2",
|
|
" #section",
|
|
" https:api.example.test/users",
|
|
"https:",
|
|
"http:?page=2",
|
|
"ws:#section",
|
|
"https:[invalid",
|
|
"https:api.example.test:99999",
|
|
"ftp:api.example.test/users",
|
|
"file:/users",
|
|
"data:text/plain,users",
|
|
];
|
|
const source = `
|
|
${references.map((value) => `api.get(${JSON.stringify(value)});`).join("\n")}
|
|
${ambiguous.map((value) => `api.get(${JSON.stringify(value)});`).join("\n")}
|
|
xhr.open(method, "?view=1");
|
|
xhr.open(method, "#view=1");
|
|
xhr.open("GET", "#literal=1");
|
|
xhr.open(method, "https:api.example.test/xhr");
|
|
request("POST", target);
|
|
snackBar.open(message, "X");
|
|
new Map().get("?map-key");
|
|
new Map().get("#map-key");
|
|
new Map().get("https:api.example.test/map-key");
|
|
new Headers().delete("?header-key");
|
|
new Headers().delete("#header-key");
|
|
new URLSearchParams().get("?parameter-key");
|
|
new URLSearchParams().get("#parameter-key");
|
|
response.headers.get("?response-key");
|
|
response.headers.get("#response-key");
|
|
const config = vscode.workspace.getConfiguration("git");
|
|
config.get("?setting");
|
|
config.get("#setting");
|
|
config.get("https:api.example.test/setting");
|
|
window.open(url, "?window-target");
|
|
window.open(url, "#window-target");
|
|
indexedDB.open(name, "?database-version");
|
|
indexedDB.open(name, "#database-version");
|
|
caches.open(name, "#cache-name");
|
|
fs.open(path, "r");
|
|
`;
|
|
const expected: string[] = [];
|
|
const expectedCandidates = [
|
|
"?view=1",
|
|
"#view=1",
|
|
"#literal=1",
|
|
"https:api.example.test/xhr",
|
|
...references,
|
|
...ambiguous,
|
|
"POST",
|
|
"X",
|
|
].sort();
|
|
const web = analyzeWebBundleFixture(inspection(source));
|
|
expect(web.observations.endpoints.map(({ value }) => value).sort()).toEqual(
|
|
expected,
|
|
);
|
|
expect(
|
|
web.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(expectedCandidates);
|
|
const analyzed = analyzeJavaScriptStaticSource(source);
|
|
expect(
|
|
analyzed.endpoints
|
|
.filter(({ interpretation }) => interpretation === undefined)
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(expected);
|
|
expect(
|
|
analyzed.endpoints
|
|
.filter(({ interpretation }) => interpretation === "candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(expectedCandidates);
|
|
});
|
|
});
|
|
|
|
describe("web bundle endpoint candidate classification", () => {
|
|
it("keeps ambiguous HTTP-method arguments as candidates instead of endpoint observations", () => {
|
|
const source = `
|
|
fetch("login");
|
|
api.get("/api/users");
|
|
api.get("users");
|
|
form.get("passwordControl");
|
|
translate.get("ADDRESS_ADDED");
|
|
cookieService.put("token", value);
|
|
http.request("POST", target);
|
|
snackBar.open(message, "X");
|
|
xhr.open(method, "login");
|
|
xhr.open("GET", "ping");
|
|
`;
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
expect(
|
|
result.observations.endpoints.map(({ value }) => value).sort(),
|
|
).toEqual(["login"]);
|
|
const candidates = result.inferences.filter(
|
|
({ kind }) => kind === "endpoint_candidate",
|
|
);
|
|
expect(candidates.map(({ value }) => value).sort()).toEqual([
|
|
"/api/users",
|
|
"ADDRESS_ADDED",
|
|
"POST",
|
|
"X",
|
|
"login",
|
|
"passwordControl",
|
|
"ping",
|
|
"token",
|
|
"users",
|
|
]);
|
|
expect(
|
|
candidates.every(
|
|
({ confidence, basis }) =>
|
|
confidence === "low" &&
|
|
basis.length === 1 &&
|
|
basis[0]?.artifact_sha256 ===
|
|
createWebTextArtifact(source, "text/javascript").sha256 &&
|
|
basis[0]?.line !== null &&
|
|
basis[0]?.detector.startsWith("call:"),
|
|
),
|
|
).toBe(true);
|
|
const staticEndpoints = analyzeJavaScriptStaticSource(source).endpoints;
|
|
expect(
|
|
staticEndpoints
|
|
.filter(({ interpretation }) => interpretation === "candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(candidates.map(({ value }) => value).sort());
|
|
expect(
|
|
staticEndpoints
|
|
.filter(({ interpretation }) => interpretation === undefined)
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(result.observations.endpoints.map(({ value }) => value).sort());
|
|
expect(webBundleAnalysisSchema.parse(result)).toEqual(result);
|
|
});
|
|
|
|
it("deduplicates repeated candidate sites while keeping script, value and detector identity", () => {
|
|
const source = `${'form.get("name");\n'.repeat(5_000)}translate.get("name");\nform.get("other");`;
|
|
const captured = inspection(source);
|
|
const first = captured.scripts.items[0];
|
|
if (first === undefined) throw new Error("Missing captured script fixture");
|
|
const secondKey = `scr_${"2".repeat(64)}`;
|
|
const result = analyzeWebBundleFixture({
|
|
...captured,
|
|
scripts: {
|
|
total: 2,
|
|
items: [
|
|
first,
|
|
{
|
|
...first,
|
|
script_key: secondKey,
|
|
url: `${origin}/assets/second.js`,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const candidates = result.inferences.filter(
|
|
({ kind }) => kind === "endpoint_candidate",
|
|
);
|
|
expect(candidates).toHaveLength(6);
|
|
for (const scriptKey of [first.script_key, secondKey]) {
|
|
const local = candidates.filter(
|
|
({ basis }) => basis[0]?.script_key === scriptKey,
|
|
);
|
|
expect(
|
|
local.map(({ value, basis }) => [value, basis[0]?.detector]),
|
|
).toEqual([
|
|
["name", "call:form.get:unverified-network-receiver"],
|
|
["name", "call:translate.get:unverified-network-receiver"],
|
|
["other", "call:form.get:unverified-network-receiver"],
|
|
]);
|
|
expect(local[0]?.basis[0]).toMatchObject({
|
|
line: 1,
|
|
column: 0,
|
|
artifact_sha256: createWebTextArtifact(source, "text/javascript")
|
|
.sha256,
|
|
});
|
|
}
|
|
expect(result.observations.endpoints).toEqual([]);
|
|
expect(result.capture.source_artifacts).toHaveLength(2);
|
|
expect(result.completeness).toMatchObject({
|
|
status: "complete",
|
|
parsed_scripts: 2,
|
|
});
|
|
expect(result.limitations).toContain(
|
|
'Endpoint candidates are deduplicated by script, value and detector; basis retains only the first matching source coordinate. They are not a complete call-site inventory or a runtime occurrence count. completeness.status="complete" describes examination of captured source, not a call-site census.',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("web bundle configuration and navigation parity", () => {
|
|
it("keeps configuration setting reads out of endpoints in both analyzers", () => {
|
|
const source = `
|
|
import vscodeDefault from "vscode";
|
|
vscodeDefault.workspace.getConfiguration("git").get("default-setting");
|
|
vscode.workspace.getConfiguration("git").get("autofetch");
|
|
workspace.getConfiguration("git").get("enabled", true);
|
|
const config = vscode.workspace.getConfiguration("git");
|
|
config.get("autofetchPeriod", 180);
|
|
config.get("/v1/settings");
|
|
let c = Mt.workspace.getConfiguration("git", uri), l = c.get("timeline.date");
|
|
let changed = vscode.workspace.getConfiguration("git");
|
|
changed = api;
|
|
changed.get("reassigned");
|
|
const maybe = flag ? vscode.workspace.getConfiguration("git") : api;
|
|
maybe.get("conditional");
|
|
{
|
|
const config = api;
|
|
config.get("shadowed");
|
|
}
|
|
api.get("users");
|
|
const client = getConfiguration("/api/v1");
|
|
client.get("accounts");
|
|
http.getConfiguration(baseUrl).get("orders");
|
|
{
|
|
const workspace = { getConfiguration: () => api };
|
|
workspace.getConfiguration("/api").get("local-users");
|
|
}
|
|
{
|
|
const vscode = { workspace: { getConfiguration: () => api } };
|
|
vscode.workspace.getConfiguration("/api").get("local-orders");
|
|
}
|
|
`;
|
|
const expected = [
|
|
"/v1/settings",
|
|
"accounts",
|
|
"conditional",
|
|
"local-orders",
|
|
"local-users",
|
|
"orders",
|
|
"reassigned",
|
|
"shadowed",
|
|
"users",
|
|
];
|
|
const web = analyzeWebBundleFixture(inspection(source));
|
|
expect(web.observations.endpoints).toEqual([]);
|
|
expect(
|
|
web.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(expected);
|
|
expect(
|
|
analyzeJavaScriptStaticSource(source)
|
|
.endpoints.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual(expected);
|
|
});
|
|
|
|
it("classifies proven VS Code extension state reads as non-endpoints", () => {
|
|
const reads = `
|
|
context.globalState.get("lastVersion");
|
|
this.workspaceState.get("closedRepositories", []);
|
|
context?.globalState.get("onboarded");
|
|
context.globalState.get("/v1/state");
|
|
context.secrets.get("token");
|
|
api.get("users");
|
|
`;
|
|
const analyze = (source: string) => {
|
|
const web = analyzeWebBundleFixture(inspection(source));
|
|
const staticAnalysis = analyzeJavaScriptStaticSource(source);
|
|
return {
|
|
web: {
|
|
endpoints: web.observations.endpoints
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
candidates: web.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
},
|
|
application: {
|
|
endpoints: staticAnalysis.endpoints
|
|
.filter(({ interpretation }) => interpretation === undefined)
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
candidates: staticAnalysis.endpoints
|
|
.filter(({ interpretation }) => interpretation === "candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
},
|
|
};
|
|
};
|
|
const proven = {
|
|
endpoints: [],
|
|
candidates: ["/v1/state", "token", "users"],
|
|
};
|
|
expect(analyze(`const vscode = require("vscode");${reads}`)).toEqual({
|
|
web: proven,
|
|
application: proven,
|
|
});
|
|
expect(analyze(`import * as vscode from "vscode";${reads}`)).toEqual({
|
|
web: proven,
|
|
application: proven,
|
|
});
|
|
const unproven = {
|
|
endpoints: [],
|
|
candidates: [
|
|
"/v1/state",
|
|
"closedRepositories",
|
|
"lastVersion",
|
|
"onboarded",
|
|
"token",
|
|
"users",
|
|
],
|
|
};
|
|
expect(analyze(`const vscode = require("./vscode");${reads}`)).toEqual({
|
|
web: unproven,
|
|
application: unproven,
|
|
});
|
|
});
|
|
|
|
it("keeps loadURL spelling without Electron receiver proof as a candidate", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection('window.win.loadURL("https://embedded.test/app");'),
|
|
);
|
|
expect(result.observations.endpoints).toEqual([]);
|
|
expect(result.inferences).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "endpoint_candidate",
|
|
value: "https://embedded.test/app",
|
|
confidence: "low",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("web bundle artifact metadata", () => {
|
|
it("retains only a validated HTTP target origin", () => {
|
|
expect(
|
|
analyzeWebBundleFixture(inspection("export {};")).capture.target_origin,
|
|
).toBe(origin);
|
|
|
|
const nonHttp = inspection("export {};");
|
|
nonHttp.target.url = "ftp://host/app";
|
|
nonHttp.target.origin = "ftp://host";
|
|
expect(analyzeWebBundleFixture(nonHttp).capture.target_origin).toBeNull();
|
|
});
|
|
|
|
it("preserves long provider media types in artifact summaries", () => {
|
|
const result = analyzeWebBundleFixture(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);
|
|
});
|
|
});
|
|
|
|
describe("deep captured web bundle syntax", () => {
|
|
it("retains evidence after a parser-admitted deep property chain", () => {
|
|
const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nfetch("/after");`;
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
expect(result.completeness).toMatchObject({
|
|
status: "complete",
|
|
parsed_scripts: 1,
|
|
parse_failures: 0,
|
|
});
|
|
expect(result.observations.chunks.edges).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "static_import",
|
|
specifier: "./last.js",
|
|
resolved_url: `${origin}/assets/last.js`,
|
|
location: expect.objectContaining({ line: 2, column: 0 }),
|
|
}),
|
|
);
|
|
expect(result.observations.endpoints).toContainEqual(
|
|
expect.objectContaining({
|
|
value: "/after",
|
|
location: expect.objectContaining({ line: 3, column: 0 }),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
const inspection = (source: string) =>
|
|
createWebPageInspection([
|
|
{
|
|
source,
|
|
scriptKey: `scr_${"1".repeat(64)}`,
|
|
url: `${origin}/assets/app.js`,
|
|
origin,
|
|
cdpHash: "hash",
|
|
},
|
|
]);
|
|
|
|
describe("per-script web bundle analysis", () => {
|
|
it("keeps parser failures as analyzed outcomes with their own coverage", () => {
|
|
const captured = inspection("function broken( {");
|
|
const script = captured.scripts.items.find(isIncludedScript);
|
|
if (script === undefined)
|
|
throw new Error("Missing included script fixture");
|
|
|
|
expect(analyzeCapturedWebScript(script)).toMatchObject({
|
|
status: "analyzed",
|
|
script_key: script.script_key,
|
|
parse_status: "failed",
|
|
visited_ast_nodes: 0,
|
|
observations: {
|
|
edges: [],
|
|
routes: [],
|
|
endpoints: [],
|
|
webmcp_declarations: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("merges out-of-order worker outcomes in capture order and preserves exact unavailable keys", () => {
|
|
const firstCapture = inspection("import './first.js'; fetch('/first');");
|
|
const secondCapture = inspection("import './second.js'; fetch('/second');");
|
|
const first = firstCapture.scripts.items.find(isIncludedScript);
|
|
const second = secondCapture.scripts.items.find(isIncludedScript);
|
|
if (first === undefined || second === undefined)
|
|
throw new Error("Missing included script fixtures");
|
|
const captured = firstCapture;
|
|
captured.scripts.items.push({ ...second, script_key: "second-script" });
|
|
captured.scripts.total += 1;
|
|
|
|
const result = buildCapturedWebBundleAnalysis(
|
|
captured,
|
|
{
|
|
status: "not_requested",
|
|
requested: 0,
|
|
processed: 0,
|
|
items: [],
|
|
},
|
|
[
|
|
{
|
|
status: "unavailable",
|
|
script_key: "second-script",
|
|
reason: "deadline",
|
|
detail: "Worker deadline expired.",
|
|
},
|
|
analyzeCapturedWebScript(first),
|
|
],
|
|
);
|
|
|
|
expect(
|
|
result.observations.chunks.edges.map(({ specifier }) => specifier),
|
|
).toEqual(["./first.js"]);
|
|
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
|
|
"/first",
|
|
]);
|
|
expect(result.unknowns).toContainEqual({
|
|
dimension: "javascript_analysis",
|
|
reason: expect.stringContaining("deadline"),
|
|
affected_script_keys: ["second-script"],
|
|
});
|
|
expect(result.completeness).toMatchObject({
|
|
status: "partial",
|
|
parsed_scripts: 1,
|
|
parse_failures: 0,
|
|
});
|
|
});
|
|
|
|
it("does not count explicitly unstarted scripts as submitted and reports source budget omissions", () => {
|
|
const captured = inspection("fetch('/started');");
|
|
const script = captured.scripts.items.find(isIncludedScript);
|
|
if (script === undefined)
|
|
throw new Error("Missing included script fixture");
|
|
const later = { ...script, script_key: "not-started-script" };
|
|
captured.scripts.items.push(later);
|
|
captured.scripts.total += 1;
|
|
captured.completeness.excluded.push({
|
|
section: "script_sources",
|
|
reason: "resource_budget_exhausted",
|
|
count: 2,
|
|
});
|
|
|
|
const result = buildCapturedWebBundleAnalysis(
|
|
captured,
|
|
{
|
|
status: "not_requested",
|
|
requested: 0,
|
|
processed: 0,
|
|
items: [],
|
|
},
|
|
[
|
|
analyzeCapturedWebScript(script),
|
|
{
|
|
status: "unavailable",
|
|
script_key: "not-started-script",
|
|
reason: "not_started",
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(result.capture.scripts_analyzed).toBe(1);
|
|
expect(result.completeness.status).toBe("partial");
|
|
expect(result.unknowns).toContainEqual({
|
|
dimension: "script_source",
|
|
reason: expect.stringContaining("omitted 2 script source artifacts"),
|
|
affected_script_keys: [],
|
|
});
|
|
expect(result.unknowns).toContainEqual({
|
|
dimension: "javascript_analysis",
|
|
reason: expect.stringContaining("not_started"),
|
|
affected_script_keys: ["not-started-script"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("unavailable web bundle analysis outcomes", () => {
|
|
it("preserves distinct unavailable reasons and details with their exact script keys", () => {
|
|
const captured = inspection("fetch('/started');");
|
|
const first = captured.scripts.items.find(isIncludedScript);
|
|
if (first === undefined) throw new Error("Missing included script fixture");
|
|
const keys = ["deadline-second", "deadline-different", "resource-failure"];
|
|
for (const script_key of keys)
|
|
captured.scripts.items.push({ ...first, script_key });
|
|
captured.scripts.total += keys.length;
|
|
|
|
const result = buildCapturedWebBundleAnalysis(
|
|
captured,
|
|
{
|
|
status: "not_requested",
|
|
requested: 0,
|
|
processed: 0,
|
|
items: [],
|
|
},
|
|
[
|
|
{
|
|
status: "unavailable",
|
|
script_key: first.script_key,
|
|
reason: "not_started",
|
|
detail: "Total analysis deadline expired after 50 ms.",
|
|
},
|
|
{
|
|
status: "unavailable",
|
|
script_key: "deadline-second",
|
|
reason: "not_started",
|
|
detail: "Total analysis deadline expired after 50 ms.",
|
|
},
|
|
{
|
|
status: "unavailable",
|
|
script_key: "deadline-different",
|
|
reason: "deadline",
|
|
detail: "Per-script deadline expired after 10 ms.",
|
|
},
|
|
{
|
|
status: "unavailable",
|
|
script_key: "resource-failure",
|
|
reason: "resource_constraint",
|
|
detail: "Worker result exceeded its byte budget.",
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(
|
|
result.unknowns.filter(
|
|
({ dimension }) => dimension === "javascript_analysis",
|
|
),
|
|
).toEqual([
|
|
{
|
|
dimension: "javascript_analysis",
|
|
reason:
|
|
'Analysis was unavailable for captured scripts (not_started); findings from those scripts are unknown. Worker detail: "Total analysis deadline expired after 50 ms."',
|
|
affected_script_keys: [first.script_key, "deadline-second"],
|
|
},
|
|
{
|
|
dimension: "javascript_analysis",
|
|
reason:
|
|
'Analysis was unavailable for captured scripts (deadline); findings from those scripts are unknown. Worker detail: "Per-script deadline expired after 10 ms."',
|
|
affected_script_keys: ["deadline-different"],
|
|
},
|
|
{
|
|
dimension: "javascript_analysis",
|
|
reason:
|
|
'Analysis was unavailable for captured scripts (resource_constraint); findings from those scripts are unknown. Worker detail: "Worker result exceeded its byte budget."',
|
|
affected_script_keys: ["resource-failure"],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
it("requires every result reference to belong to its script key", () => {
|
|
const captured = inspection(
|
|
"import './module.js'; const routes = [{ path: '/known' }]; fetch('/api'); form.get('candidate'); document.modelContext.registerTool({ name: 'lookup' });",
|
|
);
|
|
const script = captured.scripts.items.find(isIncludedScript);
|
|
if (script === undefined) throw new Error("Missing included script fixture");
|
|
const outcome = analyzeCapturedWebScript(script);
|
|
if (outcome.status !== "analyzed")
|
|
throw new Error("Expected a completed script analysis fixture");
|
|
const result = outcome;
|
|
expect(webBundleScriptAnalysisSchema.parse(result)).toEqual(result);
|
|
expect(result.observations.edges.length).toBeGreaterThan(0);
|
|
expect(result.observations.routes.length).toBeGreaterThan(0);
|
|
expect(result.observations.endpoints.length).toBeGreaterThan(0);
|
|
expect(result.observations.webmcp_declarations.length).toBeGreaterThan(0);
|
|
expect(result.inferences.length).toBeGreaterThan(0);
|
|
|
|
const wrongEdgeSource = {
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
edges: result.observations.edges.map((edge, index) =>
|
|
index === 0 ? { ...edge, from_script_key: "another-script" } : edge,
|
|
),
|
|
},
|
|
};
|
|
const wrongEdgeLocation = {
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
edges: result.observations.edges.map((edge, index) =>
|
|
index === 0
|
|
? {
|
|
...edge,
|
|
location: { ...edge.location, script_key: "another-script" },
|
|
}
|
|
: edge,
|
|
),
|
|
},
|
|
};
|
|
const wrongFindingLocation = {
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
endpoints: result.observations.endpoints.map((finding, index) =>
|
|
index === 0
|
|
? {
|
|
...finding,
|
|
location: {
|
|
...finding.location,
|
|
script_key: "another-script",
|
|
},
|
|
}
|
|
: finding,
|
|
),
|
|
},
|
|
};
|
|
const wrongRouteLocation = {
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
routes: result.observations.routes.map((finding, index) =>
|
|
index === 0
|
|
? {
|
|
...finding,
|
|
location: {
|
|
...finding.location,
|
|
script_key: "another-script",
|
|
},
|
|
}
|
|
: finding,
|
|
),
|
|
},
|
|
};
|
|
const wrongWebMcpLocation = {
|
|
...result,
|
|
observations: {
|
|
...result.observations,
|
|
webmcp_declarations: result.observations.webmcp_declarations.map(
|
|
(declaration, index) =>
|
|
index === 0
|
|
? {
|
|
...declaration,
|
|
location: {
|
|
...declaration.location,
|
|
script_key: "another-script",
|
|
},
|
|
}
|
|
: declaration,
|
|
),
|
|
},
|
|
};
|
|
const wrongInferenceBasis = {
|
|
...result,
|
|
inferences: result.inferences.map((inference, inferenceIndex) =>
|
|
inferenceIndex === 0
|
|
? {
|
|
...inference,
|
|
basis: inference.basis.map((basis, basisIndex) =>
|
|
basisIndex === 0
|
|
? { ...basis, script_key: "another-script" }
|
|
: basis,
|
|
),
|
|
}
|
|
: inference,
|
|
),
|
|
};
|
|
|
|
expect(webBundleScriptAnalysisSchema.safeParse(wrongEdgeSource).success).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse(wrongEdgeLocation).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse(wrongFindingLocation).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse(wrongRouteLocation).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse(wrongWebMcpLocation).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse(wrongInferenceBasis).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects a failed parse that claims visited nodes or derived facts", () => {
|
|
const malformed =
|
|
inspection("function broken( {").scripts.items.find(isIncludedScript);
|
|
const valid =
|
|
inspection("fetch('/api');").scripts.items.find(isIncludedScript);
|
|
if (malformed === undefined || valid === undefined)
|
|
throw new Error("Missing parse result fixtures");
|
|
const failedOutcome = analyzeCapturedWebScript(malformed);
|
|
const parsedOutcome = analyzeCapturedWebScript(valid);
|
|
if (
|
|
failedOutcome.status !== "analyzed" ||
|
|
parsedOutcome.status !== "analyzed"
|
|
)
|
|
throw new Error("Expected completed script analysis fixtures");
|
|
const failed = failedOutcome;
|
|
const parsed = parsedOutcome;
|
|
|
|
expect(webBundleScriptAnalysisSchema.parse(failed)).toEqual(failed);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse({
|
|
...failed,
|
|
visited_ast_nodes: 1,
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse({
|
|
...failed,
|
|
widened_return_references: true,
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
webBundleScriptAnalysisSchema.safeParse({
|
|
...failed,
|
|
observations: {
|
|
...failed.observations,
|
|
endpoints: parsed.observations.endpoints,
|
|
},
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("recognizes expression-free template literals as route properties", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
const routes = [
|
|
{ path: \`score-board\`, component: ScoreBoard },
|
|
{ route: \`administration\`, component: Admin },
|
|
{ path: \`/user/\${name}\`, component: Dynamic },
|
|
];
|
|
`),
|
|
);
|
|
|
|
expect(result.observations.routes.map(({ value }) => value)).toEqual([
|
|
"score-board",
|
|
"administration",
|
|
]);
|
|
});
|
|
|
|
it("uses the shared parser for decorated captured scripts", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`@sealed class Widget {} fetch('/decorated');`),
|
|
);
|
|
|
|
expect(result.completeness).toMatchObject({
|
|
status: "complete",
|
|
parsed_scripts: 1,
|
|
parse_failures: 0,
|
|
});
|
|
expect(result.observations.endpoints).toEqual([
|
|
expect.objectContaining({ value: "/decorated" }),
|
|
]);
|
|
});
|
|
|
|
it("keeps bare module locations unresolved without inventing URL-relative paths", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
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 = analyzeWebBundleFixture(
|
|
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 = analyzeWebBundleFixture(
|
|
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"],
|
|
schema_property_names_coverage: "partial",
|
|
}),
|
|
expect.objectContaining({
|
|
name: null,
|
|
schema_property_names_coverage: "unknown",
|
|
}),
|
|
expect.objectContaining({
|
|
name: null,
|
|
schema_property_names_coverage: "not_present",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "restored",
|
|
schema_property_names_coverage: "unknown",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "valid",
|
|
schema_property_names_coverage: "not_present",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("projects only exact input schema property names", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
document.modelContext.registerTool({
|
|
name: "literal",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: { type: {}, properties: {}, "": {}, [dynamicKey]: {}, ...dynamicProperties }
|
|
}
|
|
});
|
|
document.modelContext.registerTool({ name: "missing properties", inputSchema: { type: "object" } });
|
|
document.modelContext.registerTool({ name: "dynamic properties", inputSchema: { properties: dynamicProperties } });
|
|
document.modelContext.registerTool({ name: "null properties", inputSchema: { properties: null } });
|
|
document.modelContext.registerTool({ name: "missing schema" });
|
|
document.modelContext.registerTool({ name: "dynamic schema", inputSchema: schema });
|
|
document.modelContext.registerTool({ name: "null schema", inputSchema: null });
|
|
document.modelContext.registerTool({ name: "spread schema", inputSchema: { ...schema } });
|
|
document.modelContext.registerTool({
|
|
name: "computed literal keys",
|
|
["inputSchema"]: { ["properties"]: { [""]: {}, ["literal"]: {}, [dynamicKey]: {} } }
|
|
});
|
|
`),
|
|
);
|
|
|
|
expect(
|
|
result.observations.webmcp_declarations.map(
|
|
({ name, schema_property_names, schema_property_names_coverage }) => ({
|
|
name,
|
|
schema_property_names,
|
|
schema_property_names_coverage,
|
|
}),
|
|
),
|
|
).toEqual([
|
|
{
|
|
name: "literal",
|
|
schema_property_names: ["", "properties", "type"],
|
|
schema_property_names_coverage: "partial",
|
|
},
|
|
{
|
|
name: "missing properties",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "not_present",
|
|
},
|
|
{
|
|
name: "dynamic properties",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "unknown",
|
|
},
|
|
{
|
|
name: "null properties",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "unknown",
|
|
},
|
|
{
|
|
name: "missing schema",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "not_present",
|
|
},
|
|
{
|
|
name: "dynamic schema",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "unknown",
|
|
},
|
|
{
|
|
name: "null schema",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "not_present",
|
|
},
|
|
{
|
|
name: "spread schema",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "unknown",
|
|
},
|
|
{
|
|
name: "computed literal keys",
|
|
schema_property_names: ["", "literal"],
|
|
schema_property_names_coverage: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps missing coverage absent when parsing historical observations", () => {
|
|
const analyzed = analyzeWebBundleFixture(
|
|
inspection(
|
|
`document.modelContext.registerTool({ name: "legacy", inputSchema: { properties: {} } });`,
|
|
),
|
|
);
|
|
const declaration = analyzed.observations.webmcp_declarations[0];
|
|
expect(declaration).toBeDefined();
|
|
if (declaration === undefined) throw new Error("Expected declaration");
|
|
const { schema_property_names_coverage: _coverage, ...legacyDeclaration } =
|
|
declaration;
|
|
const parsed = webBundleAnalysisSchema.parse({
|
|
...analyzed,
|
|
observations: {
|
|
...analyzed.observations,
|
|
webmcp_declarations: [legacyDeclaration],
|
|
},
|
|
});
|
|
expect(
|
|
Object.hasOwn(
|
|
parsed.observations.webmcp_declarations[0] ?? {},
|
|
"schema_property_names_coverage",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("retains WebMCP declarations through optional member and call chaining", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
document.modelContext?.registerTool({ name: "optional member" });
|
|
document.modelContext.registerTool?.({ name: "optional call" });
|
|
`),
|
|
);
|
|
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({
|
|
name: "optional member",
|
|
trust: "page-declared-untrusted",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "optional call",
|
|
trust: "page-declared-untrusted",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("reads expression-free template dynamic imports as chunk edges", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
import(\`./lazy.js\`);
|
|
import("./quoted.js");
|
|
import(\`./\${name}.js\`);
|
|
`),
|
|
);
|
|
|
|
expect(result.observations.chunks.edges).toEqual([
|
|
expect.objectContaining({
|
|
kind: "dynamic_import",
|
|
specifier: "./lazy.js",
|
|
resolved_url: `${origin}/assets/lazy.js`,
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "dynamic_import",
|
|
specifier: "./quoted.js",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("does not infer call names from dynamic computed identifiers", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
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 = analyzeWebBundleFixture(
|
|
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",
|
|
]);
|
|
expect(
|
|
result.observations.chunks.edges.map(({ specifier }) => specifier),
|
|
).toEqual(["./direct.js", "./literal.js"]);
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({ name: "literal-tool" }),
|
|
]);
|
|
});
|
|
|
|
it("requires WebMCP receiver and method names to occupy exact member segments", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
navigator.modelContext.registerTool({ name: "navigator" });
|
|
local.modelContext.registerTool({ name: "exact nested" });
|
|
const appmodelContext = { registerTool() {} };
|
|
appmodelContext.registerTool({ name: "receiver suffix" });
|
|
const app = { analyticsmodelContext: { registerTool() {} } };
|
|
app.analyticsmodelContext.registerTool({ name: "nested suffix" });
|
|
window["modelContext.registerTool"]({ name: "dotted property" });
|
|
window["modelContext"].registerTool({ name: "computed receiver" });
|
|
`),
|
|
);
|
|
|
|
expect(result.observations.webmcp_declarations).toEqual([
|
|
expect.objectContaining({ name: "navigator" }),
|
|
expect.objectContaining({ name: "exact nested" }),
|
|
expect.objectContaining({ name: "computed receiver" }),
|
|
]);
|
|
});
|
|
|
|
it("recognizes tRPC procedures and named GraphQL operations in captured scripts", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
const n = createTRPCClient({ links: [httpBatchLink({ url: "/api/trpc" })] });
|
|
e.invoice.list.useQuery({ competencia: "2026-09" });
|
|
n.empresa.byCnpj.query("00000000000191");
|
|
const q = "query ListarNotas($competencia: String!) { notas { id } }";
|
|
this.client.query({ query: q });
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value, mechanism }) => ({
|
|
value,
|
|
mechanism,
|
|
})),
|
|
).toEqual([
|
|
{ value: "/api/trpc", mechanism: "trpc:link:httpBatchLink" },
|
|
{ value: "invoice.list", mechanism: "trpc:procedure:useQuery" },
|
|
{ value: "empresa.byCnpj", mechanism: "trpc:procedure:query" },
|
|
{ value: "ListarNotas", mechanism: "graphql:query" },
|
|
]);
|
|
});
|
|
|
|
it("keeps RPC lexical identity and bundled factory recognition in captured scripts", () => {
|
|
const longOperation = "Operation" + "x".repeat(512);
|
|
const document =
|
|
"#" +
|
|
"comment".repeat(700) +
|
|
"\nquery " +
|
|
longOperation +
|
|
" { users { id } }";
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
function first() { const c = createTRPCClient({}); }
|
|
function unrelated(c) { c.users.query("select 1"); }
|
|
const c = (0, t.createTRPCClient)({ links: [(0, t.httpBatchLink)({ url: "/api/rpc" })] });
|
|
c.posts.query();
|
|
const doc = ${JSON.stringify(document)};
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value, mechanism }) => ({
|
|
value,
|
|
mechanism,
|
|
})),
|
|
).toEqual([
|
|
{ value: "/api/rpc", mechanism: "trpc:link:httpBatchLink" },
|
|
{ value: "posts", mechanism: "trpc:procedure:query" },
|
|
{ value: longOperation, mechanism: "graphql:query" },
|
|
]);
|
|
expect(result.completeness.status).toBe("complete");
|
|
});
|
|
|
|
it("retains distinct WebMCP descriptions and names while deduplicating equivalent properties", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
document.modelContext.registerTool({ name: "lookup", description: "first", inputSchema: { properties: { alpha: {} } } });
|
|
document.modelContext.registerTool({ name: "lookup", description: "second", inputSchema: { properties: { alpha: {} } } });
|
|
document.modelContext.registerTool({ name: "equivalent", inputSchema: { properties: { alpha: {}, beta: {} } } });
|
|
document.modelContext.registerTool({ name: "equivalent", inputSchema: { properties: { beta: {}, alpha: {}, alpha: {} } } });
|
|
document.modelContext.registerTool({ name: "empty-status", inputSchema: { properties: {} } });
|
|
document.modelContext.registerTool({ name: "empty-status", inputSchema: { properties: dynamicProperties } });
|
|
document.modelContext.registerTool({ description: "unnamed" });
|
|
document.modelContext.registerTool({ name: "", description: "unnamed" });
|
|
`),
|
|
);
|
|
|
|
expect(
|
|
result.observations.webmcp_declarations.map(
|
|
({
|
|
name,
|
|
description,
|
|
schema_property_names,
|
|
schema_property_names_coverage,
|
|
}) => ({
|
|
name,
|
|
description,
|
|
schema_property_names,
|
|
schema_property_names_coverage,
|
|
}),
|
|
),
|
|
).toEqual([
|
|
{
|
|
name: "lookup",
|
|
description: "first",
|
|
schema_property_names: ["alpha"],
|
|
schema_property_names_coverage: "complete",
|
|
},
|
|
{
|
|
name: "lookup",
|
|
description: "second",
|
|
schema_property_names: ["alpha"],
|
|
schema_property_names_coverage: "complete",
|
|
},
|
|
{
|
|
name: "equivalent",
|
|
description: null,
|
|
schema_property_names: ["alpha", "beta"],
|
|
schema_property_names_coverage: "complete",
|
|
},
|
|
{
|
|
name: "empty-status",
|
|
description: null,
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "complete",
|
|
},
|
|
{
|
|
name: "empty-status",
|
|
description: null,
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "unknown",
|
|
},
|
|
{
|
|
name: null,
|
|
description: "unnamed",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "not_present",
|
|
},
|
|
{
|
|
name: "",
|
|
description: "unnamed",
|
|
schema_property_names: [],
|
|
schema_property_names_coverage: "not_present",
|
|
},
|
|
]);
|
|
expect(result.observations.webmcp_declarations[2]?.location).toEqual({
|
|
script_key: `scr_${"1".repeat(64)}`,
|
|
line: 4,
|
|
column: 6,
|
|
});
|
|
expect(
|
|
result.observations.webmcp_declarations.every(
|
|
({ trust }) => trust === "page-declared-untrusted",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps known capture omissions in compact route summaries and queryable gap facts", () => {
|
|
const captured = inspection("createBrowserRouter([{path:'/reports'}]);");
|
|
captured.completeness.excluded.push(
|
|
{ section: "scripts", reason: "resource_budget_exhausted", count: 3 },
|
|
{
|
|
section: "script_sources",
|
|
reason: "resource_budget_exhausted",
|
|
count: null,
|
|
},
|
|
);
|
|
const report = analyzeWebBundleFixture(captured),
|
|
parent = {
|
|
evidenceId: `ev_${"a".repeat(64)}`,
|
|
operation: "analyze_web_bundle",
|
|
normalizedResult: report,
|
|
limitations: report.limitations,
|
|
subject: null,
|
|
};
|
|
const summary = projectAnalysisView(
|
|
parent,
|
|
analysisViewRequestSchema.parse({
|
|
kind: "javascript-routes",
|
|
collection: "summary",
|
|
}),
|
|
);
|
|
if (!summary.ok) throw summary.error;
|
|
if (summary.value.kind !== "javascript-routes") throw new Error("Wrong view");
|
|
expect(summary.value.source_coverage.candidate_enumeration).toBe("partial");
|
|
expect(summary.value.summary!.reported_capture_omissions).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ unit: "script-records", count: 3 }),
|
|
expect.objectContaining({ unit: "script-sources", count: null }),
|
|
]),
|
|
);
|
|
const gaps = projectAnalysisView(
|
|
parent,
|
|
analysisViewRequestSchema.parse({
|
|
kind: "javascript-routes",
|
|
collection: "unknowns",
|
|
where: { kinds: ["capture-omission"] },
|
|
fields: ["capture_omission", "reason"],
|
|
}),
|
|
);
|
|
if (!gaps.ok) throw gaps.error;
|
|
if (gaps.value.kind !== "javascript-routes") throw new Error("Wrong view");
|
|
expect(gaps.value.items).toHaveLength(2);
|
|
for (const row of gaps.value.items)
|
|
expect(row).toMatchObject({ source_id: null, provenance: null });
|
|
});
|
|
|
|
it("retains captured Next script identity while deriving decoded route patterns", () => {
|
|
const url = `${origin}/base/_next/static/chunks/app/%28shop%29/blog/%5Bid%5D/page-abc.js`;
|
|
const captured = createWebPageInspection([
|
|
{
|
|
source: "router.push('/help');",
|
|
scriptKey: `scr_${"2".repeat(64)}`,
|
|
url,
|
|
origin,
|
|
cdpHash: "hash",
|
|
},
|
|
]);
|
|
const result = analyzeWebBundleFixture(captured);
|
|
expect(result.route_inventory?.routes.map(({ path }) => path)).toEqual([
|
|
"/base/blog/:id",
|
|
"/base/help",
|
|
]);
|
|
expect(result.route_inventory?.sources).toEqual([
|
|
expect.objectContaining({
|
|
url,
|
|
sha256: captured.scripts.items[0]?.source.included
|
|
? captured.scripts.items[0].source.artifact.sha256
|
|
: null,
|
|
}),
|
|
]);
|
|
expect(result.route_inventory?.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
origin: "source-url",
|
|
unprefixed_path: "/blog/:id",
|
|
inferred_base_path: "/base",
|
|
}),
|
|
expect.objectContaining({
|
|
origin: "source-syntax",
|
|
unprefixed_path: "/help",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("qualifies local browser API spellings while preserving intrinsic requests", () => {
|
|
const result = analyzeWebBundleFixture(
|
|
inspection(`
|
|
const local = { fetch(x) { return x; }, sendBeacon(x) { return x; }, EventSource(x) { return x; } };
|
|
local.fetch('/local-fetch'); local.sendBeacon('/local-beacon'); local.EventSource('/local-events');
|
|
const other = { open(a,b) { return b; }, post(x) { return x; }, put(x) { return x; }, patch(x) { return x; }, request(x) { return x; } };
|
|
other.open('GET', '/local-open'); other.post('/local-post'); other.put('/local-put'); other.patch('/local-patch'); other.request('/local-request');
|
|
fetch('/intrinsic-fetch'); navigator.sendBeacon('/intrinsic-beacon'); new EventSource('/intrinsic-events');
|
|
`),
|
|
);
|
|
expect(
|
|
result.observations.endpoints.map(({ value }) => value).sort(),
|
|
).toEqual(["/intrinsic-beacon", "/intrinsic-events", "/intrinsic-fetch"]);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value)
|
|
.sort(),
|
|
).toEqual([
|
|
"/local-beacon",
|
|
"/local-events",
|
|
"/local-fetch",
|
|
"/local-open",
|
|
"/local-patch",
|
|
"/local-post",
|
|
"/local-put",
|
|
"/local-request",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["", true],
|
|
["xhr.open = replacement;", false],
|
|
["XMLHttpRequest.prototype.open = replacement;", false],
|
|
["const alias = xhr; alias.open = replacement;", false],
|
|
["mutate(xhr);", false],
|
|
])(
|
|
"preserves XHR receiver proof across visible mutation: %s",
|
|
(mutation, observed) => {
|
|
const source = `const xhr = new XMLHttpRequest(); ${mutation} xhr.open('GET', '/xhr');`;
|
|
const result = analyzeWebBundleFixture(inspection(source));
|
|
expect(result.observations.endpoints.map(({ value }) => value)).toEqual(
|
|
observed ? ["/xhr"] : [],
|
|
);
|
|
expect(
|
|
result.inferences
|
|
.filter(({ kind }) => kind === "endpoint_candidate")
|
|
.map(({ value }) => value),
|
|
).toEqual(observed ? [] : ["/xhr"]);
|
|
expect(analyzeJavaScriptStaticSource(source).endpoints).toContainEqual(
|
|
expect.objectContaining({
|
|
value: "/xhr",
|
|
...(observed ? {} : { interpretation: "candidate" }),
|
|
}),
|
|
);
|
|
},
|
|
);
|