From 2032b5a819a90d37836822afaece0eaabfc702b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20DS=20Andrade?= <95101505+viniciusdsandrade@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:54:19 -0300 Subject: [PATCH] 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> --- src/domain/webBundleAnalyzerResult.ts | 14 +- .../browser/webBundleResultCapacity.test.ts | 198 ++++++++++++++++++ 2 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 tests/boundary/browser/webBundleResultCapacity.test.ts diff --git a/src/domain/webBundleAnalyzerResult.ts b/src/domain/webBundleAnalyzerResult.ts index b27f8cf0d0..65cb116488 100644 --- a/src/domain/webBundleAnalyzerResult.ts +++ b/src/domain/webBundleAnalyzerResult.ts @@ -201,6 +201,10 @@ export const buildCapturedWebBundleAnalysis = ( }); }; +const appendAll = (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; }; diff --git a/tests/boundary/browser/webBundleResultCapacity.test.ts b/tests/boundary/browser/webBundleResultCapacity.test.ts new file mode 100644 index 0000000000..9d41460fd4 --- /dev/null +++ b/tests/boundary/browser/webBundleResultCapacity.test.ts @@ -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 = { + edges: ["./first.js", "./last.js"], + routes: ["/first", "/last"], + endpoints: ["/api/first", "/api/last"], + webmcp_declarations: ["firstTool", "lastTool"], + inferences: ["/api/first", "/api/last"], +}; + +const fillWide = (count: number, first: T, middle: T, last: T): T[] => { + const items = new Array(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); + }, +);