fix(web): merge wide script analysis results without call spreads

Fixes #10445
Supersedes #10446

Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Vinícius DS Andradeandgensokyo-dev authored and Ysopwnable committed 2026-11-14 14:59:41 +01:00
1 parent fe80d52c9e
commit 2032b5a819
2 files changed
+207 -5

No files matched your search

+9 -5
View File
@@ -201,6 +201,10 @@ export const buildCapturedWebBundleAnalysis = (
});
};
const appendAll = <T>(target: T[], items: readonly T[]): void => {
for (const item of items) target.push(item);
};
const mergeScriptResults = (
results: readonly Extract<
WebBundleScriptAnalysis,
@@ -219,11 +223,11 @@ const mergeScriptResults = (
accumulator.recoveredScripts.push(result.script_key);
if (result.widened_return_references)
accumulator.widenedScripts.push(result.script_key);
accumulator.edges.push(...result.observations.edges);
accumulator.routes.push(...result.observations.routes);
accumulator.endpoints.push(...result.observations.endpoints);
accumulator.webMcp.push(...result.observations.webmcp_declarations);
accumulator.inferences.push(...result.inferences);
appendAll(accumulator.edges, result.observations.edges);
appendAll(accumulator.routes, result.observations.routes);
appendAll(accumulator.endpoints, result.observations.endpoints);
appendAll(accumulator.webMcp, result.observations.webmcp_declarations);
appendAll(accumulator.inferences, result.inferences);
}
return accumulator;
};
@@ -0,0 +1,198 @@
import { expect, it } from "vitest";
import type { WebBundleAnalysis } from "../../../src/domain/webBundleAnalysis.js";
import { buildCapturedWebBundleAnalysis } from "../../../src/domain/webBundleAnalyzerResult.js";
import { isIncludedScript } from "../../../src/domain/webBundleAnalyzerState.js";
import type { WebBundleScriptAnalysis } from "../../../src/domain/webBundleScriptAnalysis.js";
import { createWebPageInspection } from "../../support/browser/webPageInspection.js";
type WideCollection =
| "edges"
| "routes"
| "endpoints"
| "webmcp_declarations"
| "inferences";
const EXPECTED_BOUNDS: Record<WideCollection, readonly [string, string]> = {
edges: ["./first.js", "./last.js"],
routes: ["/first", "/last"],
endpoints: ["/api/first", "/api/last"],
webmcp_declarations: ["firstTool", "lastTool"],
inferences: ["/api/first", "/api/last"],
};
const fillWide = <T>(count: number, first: T, middle: T, last: T): T[] => {
const items = new Array<T>(count).fill(middle);
items[0] = first;
items[count - 1] = last;
return items;
};
const buildWideScriptResult = (
collection: WideCollection,
scriptKey: string,
sha256: string,
count: number,
): WebBundleScriptAnalysis => {
const location = { script_key: scriptKey, line: 1, column: 0 };
const makeEdge = (specifier: string) => ({
from_script_key: scriptKey,
kind: "static_import" as const,
specifier,
resolved_url: null,
location,
});
const makeFinding = (value: string, mechanism: string) => ({
value,
mechanism,
location,
});
const makeWebMcp = (name: string) => ({
name,
description: null,
schema_property_names: [],
trust: "page-declared-untrusted" as const,
location,
});
const makeInference = (value: string) => ({
kind: "endpoint_candidate" as const,
value,
confidence: "low" as const,
basis: [
{
script_key: scriptKey,
artifact_sha256: sha256,
line: 1,
column: 0,
detector: "test",
},
],
});
return {
status: "analyzed",
script_key: scriptKey,
parse_status: "parsed",
visited_ast_nodes: count,
widened_return_references: false,
observations: {
edges:
collection === "edges"
? fillWide(
count,
makeEdge("./first.js"),
makeEdge("./mid.js"),
makeEdge("./last.js"),
)
: [],
routes:
collection === "routes"
? fillWide(
count,
makeFinding("/first", "router"),
makeFinding("/mid", "router"),
makeFinding("/last", "router"),
)
: [],
endpoints:
collection === "endpoints"
? fillWide(
count,
makeFinding("/api/first", "fetch"),
makeFinding("/api/mid", "fetch"),
makeFinding("/api/last", "fetch"),
)
: [],
webmcp_declarations:
collection === "webmcp_declarations"
? fillWide(
count,
makeWebMcp("firstTool"),
makeWebMcp("midTool"),
makeWebMcp("lastTool"),
)
: [],
},
inferences:
collection === "inferences"
? fillWide(
count,
makeInference("/api/first"),
makeInference("/api/mid"),
makeInference("/api/last"),
)
: [],
};
};
const summarizeMerged = (
result: WebBundleAnalysis,
collection: WideCollection,
): readonly [number, string | null | undefined, string | null | undefined] => {
switch (collection) {
case "edges": {
const items = result.observations.chunks.edges;
return [items.length, items[0]?.specifier, items.at(-1)?.specifier];
}
case "routes": {
const items = result.observations.routes;
return [items.length, items[0]?.value, items.at(-1)?.value];
}
case "endpoints": {
const items = result.observations.endpoints;
return [items.length, items[0]?.value, items.at(-1)?.value];
}
case "webmcp_declarations": {
const items = result.observations.webmcp_declarations;
return [items.length, items[0]?.name, items.at(-1)?.name];
}
case "inferences": {
const items = result.inferences;
return [items.length, items[0]?.value, items.at(-1)?.value];
}
}
};
// Forked tests share the CLI/MCP main-thread stack, unlike domain threads.
it.each([
"edges",
"routes",
"endpoints",
"webmcp_declarations",
"inferences",
] as const)(
"merges wide %s script results without overflowing call arguments",
(collection) => {
const count = 130_000;
const scriptKey = "script-0";
const captured = createWebPageInspection([
{
source: "export {};\n",
scriptKey,
url: "https://app.example.test/app.js",
origin: "https://app.example.test",
cdpHash: "hash-0",
},
]);
const script = captured.scripts.items.find(isIncludedScript);
if (script === undefined)
throw new Error("Missing included script fixture");
const result = buildCapturedWebBundleAnalysis(
captured,
{ status: "not_requested", requested: 0, processed: 0, items: [] },
[
buildWideScriptResult(
collection,
scriptKey,
script.source.artifact.sha256,
count,
),
],
);
const [length, first, last] = summarizeMerged(result, collection);
const [expectedFirst, expectedLast] = EXPECTED_BOUNDS[collection];
expect(length).toBe(count);
expect(first).toBe(expectedFirst);
expect(last).toBe(expectedLast);
},
);