refactor(browser): move bundle fixture composition out of runtime

This commit is contained in:
morluto committed 2026-10-24 10:49:46 +08:00
1 parent 7958e15892
commit 6021efd371
4 files changed
+75 -77

No files matched your search

@@ -17,6 +17,7 @@ import {
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { err, ok } from "../domain/result.js";
import { passiveNetworkCapture } from "../../tests/fixtures/passiveNetworkCapture.js";
import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js";
import { inspectWebPage } from "./BrowserObservationService.js";
import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
@@ -163,11 +164,9 @@ it("retains captured page Evidence when bundle analysis fails after capture", as
});
it("retains completed bundle facts as bundle Evidence after cancellation", async () => {
const { analyzeCapturedWebBundle } =
await import("../domain/webBundleAnalyzer.js");
const { AnalysisCancelledError } =
await import("../domain/analysisErrorCore.js");
const bundle = analyzeCapturedWebBundle(passiveNetworkCapture());
const bundle = analyzeWebBundleFixture(passiveNetworkCapture());
const reason = { requested: "stop after completed script" };
const failure = new AnalysisCancelledError("analyze_web_bundle", {
cause: reason,
+50 -52
View File
@@ -6,10 +6,8 @@ import { describe, expect, it } from "vitest";
import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js";
import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js";
import {
analyzeCapturedWebBundle,
analyzeCapturedWebScript,
} from "./webBundleAnalyzer.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";
@@ -19,7 +17,7 @@ import { createWebTextArtifact } from "./webContentArtifact.js";
const origin = "https://app.example.test";
it("retains standalone GraphQL Source operations in captured scripts", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const source = {
body: "query GetUser { user { id } }",
@@ -57,7 +55,7 @@ it.each([
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 = analyzeCapturedWebBundle(captured);
const result = analyzeWebBundleFixture(captured);
expect(result.completeness).toMatchObject({
status: "partial",
parsed_scripts: parsed,
@@ -98,7 +96,7 @@ it.each([
"shares Map receiver certainty with application analysis: %s",
(source, candidate) => {
const input = source + " fetch('/network');";
const result = analyzeCapturedWebBundle(inspection(input));
const result = analyzeWebBundleFixture(inspection(input));
expect(result.observations.endpoints.map(({ value }) => value)).toEqual([
"/network",
]);
@@ -140,7 +138,7 @@ it("discloses widened returned-reference effects only for the affected script",
if (independent === undefined) throw new Error("Missing independent script");
captured.scripts.items.push({ ...independent, script_key: "independent" });
captured.scripts.total += 1;
const result = analyzeCapturedWebBundle(captured);
const result = analyzeWebBundleFixture(captured);
expect(result.completeness).toMatchObject({
status: "partial",
parsed_scripts: 2,
@@ -225,7 +223,7 @@ it.each([
])(
"preserves WebMCP facts through $label",
({ source, name, description, properties, coverage }) => {
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
@@ -270,7 +268,7 @@ it.each([
])(
"preserves rightmost uncertainty for $label",
({ source, name, description, properties, coverage }) => {
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
@@ -375,7 +373,7 @@ it.each([
])(
"preserves WebMCP schema ownership for $label",
({ source, name, properties, coverage }) => {
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.observations.webmcp_declarations).toEqual([
expect.objectContaining({
@@ -408,7 +406,7 @@ describe("web bundle analyzer", () => {
});
const __webpack_require__ = () => createApp(routes, lazy);
`;
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.observations.chunks.edges).toEqual(
expect.arrayContaining([
@@ -451,14 +449,14 @@ describe("web bundle analyzer", () => {
it("reports parser gaps without imposing a finding cap", () => {
const malformed = inspection("function broken( {");
const failed = analyzeCapturedWebBundle(malformed);
const failed = analyzeWebBundleFixture(malformed);
expect(failed.completeness).toMatchObject({
status: "partial",
parse_failures: 1,
});
expect(failed.unknowns[0]?.dimension).toBe("javascript_ast");
const analyzed = analyzeCapturedWebBundle(
const analyzed = analyzeWebBundleFixture(
inspection("fetch('/one'); fetch('/two'); fetch('/three');"),
);
expect(analyzed.observations.endpoints).toHaveLength(3);
@@ -467,7 +465,7 @@ describe("web bundle analyzer", () => {
it("analyzes AST nodes after the former fixed node ceiling", () => {
const source = `${";".repeat(250_001)}fetch('/last');`;
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.observations.endpoints).toContainEqual(
expect.objectContaining({ value: "/last" }),
@@ -481,7 +479,7 @@ describe("web bundle analyzer", () => {
const name = "tool".repeat(100);
const description = "untrusted declaration ".repeat(150);
const propertyName = "property".repeat(100);
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
import "${specifier}";
document.modelContext.registerTool({
@@ -505,7 +503,7 @@ describe("web bundle analyzer", () => {
describe("web bundle source-map coverage", () => {
it("reports unavailable source maps as partial without dropping requested maps", () => {
const analysis = analyzeCapturedWebBundle(inspection("export {};"), {
const analysis = analyzeWebBundleFixture(inspection("export {};"), {
status: "unavailable",
requested: 1,
processed: 1,
@@ -534,7 +532,7 @@ describe("web bundle source-map coverage", () => {
});
it("keeps map-less captured source complete when no maps were requested", () => {
const analysis = analyzeCapturedWebBundle(inspection("export {};"), {
const analysis = analyzeWebBundleFixture(inspection("export {};"), {
status: "not_requested",
requested: 0,
processed: 0,
@@ -546,7 +544,7 @@ describe("web bundle source-map coverage", () => {
});
it("rejects inconsistent source-map coverage counts", () => {
const result = analyzeCapturedWebBundle(inspection("export {};"));
const result = analyzeWebBundleFixture(inspection("export {};"));
expect(
webBundleAnalysisSchema.safeParse({
...result,
@@ -564,7 +562,7 @@ describe("web bundle source-map coverage", () => {
describe("web bundle static-analysis parity", () => {
it("recognizes endpoints through nested dynamic receivers like static analysis", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection('window[recv].fetch("/nested-fetch");'),
);
expect(result.observations.endpoints).toContainEqual(
@@ -573,7 +571,7 @@ describe("web bundle static-analysis parity", () => {
});
it("reads open endpoints only from XMLHttpRequest method and URL pairs", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
xhr.open("GET", "/xhr-get");
xhr.open("post", "https://xhr.example.test/submit");
@@ -632,7 +630,7 @@ describe("web bundle API classification parity", () => {
indexedDB.open("GET", "/storage-named-xhr");
`;
expect(
analyzeCapturedWebBundle(inspection(source)).observations.endpoints.map(
analyzeWebBundleFixture(inspection(source)).observations.endpoints.map(
({ value }) => value,
),
).toEqual(["/storage-named-xhr"]);
@@ -649,7 +647,7 @@ describe("web bundle API classification parity", () => {
});
it("uses lexical receiver facts for overloaded open calls", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const popup = window;
popup.open(url, "/preview");
@@ -676,7 +674,7 @@ describe("web bundle API classification parity", () => {
const xhr = new XMLHttpRequest();
xhr?.open(method, "/api");
`;
const bundle = analyzeCapturedWebBundle(inspection(source));
const bundle = analyzeWebBundleFixture(inspection(source));
expect(bundle.observations.endpoints.map(({ value }) => value)).toEqual([
"/api",
]);
@@ -690,7 +688,7 @@ describe("web bundle API classification parity", () => {
});
it("keeps HTTP-method-named reads of provable keyed collections out of endpoints", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
axios.get("https://api.example.test/users");
api.get("users");
@@ -728,7 +726,7 @@ describe("web bundle API classification parity", () => {
});
it("keeps undici header-store reads out of endpoint observations", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
response.headersList.get("location", true);
request.headersList.delete("authorization");
@@ -829,7 +827,7 @@ describe("web bundle explicit request reference forms", () => {
"POST",
"X",
].sort();
const web = analyzeCapturedWebBundle(inspection(source));
const web = analyzeWebBundleFixture(inspection(source));
expect(web.observations.endpoints.map(({ value }) => value).sort()).toEqual(
expected,
);
@@ -869,7 +867,7 @@ describe("web bundle endpoint candidate classification", () => {
xhr.open(method, "login");
xhr.open("GET", "ping");
`;
const result = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(
result.observations.endpoints.map(({ value }) => value).sort(),
).toEqual(["login", "ping"]);
@@ -919,7 +917,7 @@ describe("web bundle endpoint candidate classification", () => {
const first = captured.scripts.items[0];
if (first === undefined) throw new Error("Missing captured script fixture");
const secondKey = `scr_${"2".repeat(64)}`;
const result = analyzeCapturedWebBundle({
const result = analyzeWebBundleFixture({
...captured,
scripts: {
total: 2,
@@ -1011,7 +1009,7 @@ describe("web bundle configuration and navigation parity", () => {
"shadowed",
"users",
];
const web = analyzeCapturedWebBundle(inspection(source));
const web = analyzeWebBundleFixture(inspection(source));
expect(web.observations.endpoints).toEqual([]);
expect(
web.inferences
@@ -1036,7 +1034,7 @@ describe("web bundle configuration and navigation parity", () => {
api.get("users");
`;
const analyze = (source: string) => {
const web = analyzeCapturedWebBundle(inspection(source));
const web = analyzeWebBundleFixture(inspection(source));
const staticAnalysis = analyzeJavaScriptStaticSource(source);
return {
web: {
@@ -1090,7 +1088,7 @@ describe("web bundle configuration and navigation parity", () => {
});
it("recognizes loadURL endpoints like static analysis", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection('window.win.loadURL("https://embedded.test/app");'),
);
expect(result.observations.endpoints).toContainEqual(
@@ -1102,17 +1100,17 @@ describe("web bundle configuration and navigation parity", () => {
describe("web bundle artifact metadata", () => {
it("retains only a validated HTTP target origin", () => {
expect(
analyzeCapturedWebBundle(inspection("export {};")).capture.target_origin,
analyzeWebBundleFixture(inspection("export {};")).capture.target_origin,
).toBe(origin);
const nonHttp = inspection("export {};");
nonHttp.target.url = "ftp://host/app";
nonHttp.target.origin = "ftp://host";
expect(analyzeCapturedWebBundle(nonHttp).capture.target_origin).toBeNull();
expect(analyzeWebBundleFixture(nonHttp).capture.target_origin).toBeNull();
});
it("preserves long provider media types in artifact summaries", () => {
const result = analyzeCapturedWebBundle(inspection("export {};"));
const result = analyzeWebBundleFixture(inspection("export {};"));
const mediaType = `application/${"x".repeat(300)}`;
const artifact = result.capture.source_artifacts[0] ?? {
sha256: "0".repeat(64),
@@ -1137,7 +1135,7 @@ describe("web bundle artifact metadata", () => {
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 = analyzeCapturedWebBundle(inspection(source));
const result = analyzeWebBundleFixture(inspection(source));
expect(result.completeness).toMatchObject({
status: "complete",
parsed_scripts: 1,
@@ -1530,7 +1528,7 @@ it("rejects a failed parse that claims visited nodes or derived facts", () => {
});
it("recognizes expression-free template literals as route properties", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const routes = [
{ path: \`score-board\`, component: ScoreBoard },
@@ -1547,7 +1545,7 @@ it("recognizes expression-free template literals as route properties", () => {
});
it("uses the shared parser for decorated captured scripts", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`@sealed class Widget {} fetch('/decorated');`),
);
@@ -1562,7 +1560,7 @@ it("uses the shared parser for decorated captured scripts", () => {
});
it("keeps bare module locations unresolved without inventing URL-relative paths", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
import "package-name";
import("@scope/package");
@@ -1590,7 +1588,7 @@ it("keeps bare module locations unresolved without inventing URL-relative paths"
});
it("retains each literal importScripts argument in source order", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(
'importScripts("first.js", dynamicValue, "second.js", "first.js");',
),
@@ -1604,7 +1602,7 @@ it("retains each literal importScripts argument in source order", () => {
describe("web metadata key semantics", () => {
it("keeps computed metadata keys and overwrite order conservative", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const path = "description";
const routes = [{ [path]: "/not-a-route" }, { ["path"]: "/real" }];
@@ -1649,7 +1647,7 @@ describe("web metadata key semantics", () => {
});
it("projects only exact input schema property names", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
document.modelContext.registerTool({
name: "literal",
@@ -1730,7 +1728,7 @@ describe("web metadata key semantics", () => {
});
it("keeps missing coverage absent when parsing historical observations", () => {
const analyzed = analyzeCapturedWebBundle(
const analyzed = analyzeWebBundleFixture(
inspection(
`document.modelContext.registerTool({ name: "legacy", inputSchema: { properties: {} } });`,
),
@@ -1757,7 +1755,7 @@ describe("web metadata key semantics", () => {
});
it("retains WebMCP declarations through optional member and call chaining", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
document.modelContext?.registerTool({ name: "optional member" });
document.modelContext.registerTool?.({ name: "optional call" });
@@ -1777,7 +1775,7 @@ it("retains WebMCP declarations through optional member and call chaining", () =
});
it("reads expression-free template dynamic imports as chunk edges", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
import(\`./lazy.js\`);
import("./quoted.js");
@@ -1799,7 +1797,7 @@ it("reads expression-free template dynamic imports as chunk edges", () => {
});
it("does not infer call names from dynamic computed identifiers", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const fetch = "render";
const require = "render";
@@ -1816,7 +1814,7 @@ it("does not infer call names from dynamic computed identifiers", () => {
});
it("retains direct and literal computed call names", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
window.fetch("/direct");
window["fetch"]("/literal");
@@ -1841,7 +1839,7 @@ it("retains direct and literal computed call names", () => {
});
it("requires WebMCP receiver and method names to occupy exact member segments", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
navigator.modelContext.registerTool({ name: "navigator" });
local.modelContext.registerTool({ name: "exact nested" });
@@ -1862,7 +1860,7 @@ it("requires WebMCP receiver and method names to occupy exact member segments",
});
it("recognizes tRPC procedures and named GraphQL operations in captured scripts", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
const n = createTRPCClient({ links: [httpBatchLink({ url: "/api/trpc" })] });
e.invoice.list.useQuery({ competencia: "2026-09" });
@@ -1892,7 +1890,7 @@ it("keeps RPC lexical identity and bundled factory recognition in captured scrip
"\nquery " +
longOperation +
" { users { id } }";
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
function first() { const c = createTRPCClient({}); }
function unrelated(c) { c.users.query("select 1"); }
@@ -1915,7 +1913,7 @@ it("keeps RPC lexical identity and bundled factory recognition in captured scrip
});
it("retains distinct WebMCP descriptions and names while deduplicating equivalent properties", () => {
const result = analyzeCapturedWebBundle(
const result = analyzeWebBundleFixture(
inspection(`
document.modelContext.registerTool({ name: "lookup", description: "first", inputSchema: { properties: { alpha: {} } } });
document.modelContext.registerTool({ name: "lookup", description: "second", inputSchema: { properties: { alpha: {} } } });
@@ -2008,7 +2006,7 @@ it("keeps known capture omissions in compact route summaries and queryable gap f
count: null,
},
);
const report = analyzeCapturedWebBundle(captured),
const report = analyzeWebBundleFixture(captured),
parent = {
evidenceId: `ev_${"a".repeat(64)}`,
operation: "analyze_web_bundle",
-22
View File
@@ -1,12 +1,8 @@
import type { WebPageInspection } from "./browserObservationSchemas.js";
import { analyzeScript } from "./webBundleAnalyzerInspection.js";
import {
createWebBundleAnalysisAccumulator,
isIncludedScript,
type IncludedScript,
} from "./webBundleAnalyzerState.js";
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
import {
webBundleScriptAnalysisSchema,
type WebBundleScriptAnalysis,
@@ -36,21 +32,3 @@ export const analyzeCapturedWebScript = (
inferences: accumulator.inferences,
});
};
/** Analyze selected captured JavaScript source without execution. */
export const analyzeCapturedWebBundle = (
inspection: WebPageInspection,
sourceMaps: WebBundleAnalysis["observations"]["source_maps"] = {
status: "not_requested",
requested: 0,
processed: 0,
items: [],
},
): WebBundleAnalysis => {
const sourceScripts = inspection.scripts.items.filter(isIncludedScript);
return buildCapturedWebBundleAnalysis(
inspection,
sourceMaps,
sourceScripts.map(analyzeCapturedWebScript),
);
};
@@ -0,0 +1,23 @@
import type { WebPageInspection } from "../../../src/domain/browserObservationSchemas.js";
import type { WebBundleAnalysis } from "../../../src/domain/webBundleAnalysis.js";
import { analyzeCapturedWebScript } from "../../../src/domain/webBundleAnalyzer.js";
import { buildCapturedWebBundleAnalysis } from "../../../src/domain/webBundleAnalyzerResult.js";
import { isIncludedScript } from "../../../src/domain/webBundleAnalyzerState.js";
/** Compose the production script parser and result merger for semantic fixtures. */
export const analyzeWebBundleFixture = (
inspection: WebPageInspection,
sourceMaps: WebBundleAnalysis["observations"]["source_maps"] = {
status: "not_requested",
requested: 0,
processed: 0,
items: [],
},
): WebBundleAnalysis => {
const sourceScripts = inspection.scripts.items.filter(isIncludedScript);
return buildCapturedWebBundleAnalysis(
inspection,
sourceMaps,
sourceScripts.map(analyzeCapturedWebScript),
);
};