import { expect, it } from "vitest"; import { webPageInspectionSchema } from "../domain/browserObservationSchemas.js"; import { AnalysisCancelledError, AnalysisOutputError, AnalysisResourceConstraintError, AnalysisTimeoutError, } from "../domain/analysisErrorCore.js"; import type { AnalyzeWebBundleInput, WebBundleAnalysis, } from "../domain/webBundleAnalysis.js"; import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; import { analyzeCapturedWebScript } from "../domain/webBundleAnalyzer.js"; import { createWebTextArtifact } from "../domain/webContentArtifact.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { err, ok } from "../domain/result.js"; import { IsolatedWebBundleAnalyzer, type WebBundleAnalysisWorkerPort, } from "./IsolatedWebBundleAnalyzer.js"; const origin = "https://app.example.test"; const noSourceMaps: WebBundleAnalysis["observations"]["source_maps"] = { status: "not_requested", requested: 0, processed: 0, items: [], }; const input: AnalyzeWebBundleInput = analyzeWebBundleInputSchema.parse({ cdp_endpoint: "http://127.0.0.1:9222", target_id: "page-1", include_script_sources: true, }); it("preserves completed prefix and suffix facts when one script times out", async () => { const captured = inspection([ "import './prefix.js'; fetch('/prefix');", "function stalled() {}", "import './suffix.js'; fetch('/suffix');", ]); const calls: string[] = []; const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async (script) => { calls.push(script.script_key); if (calls.length === 2) return err({ error: new AnalysisTimeoutError("analyze_web_bundle", 5), completed: null, }); return ok(analyzeCapturedWebScript(script)); }, close: async () => {}, })); expect((await analyzer.reserve()).ok).toBe(true); const result = await analyzer.analyze(captured, noSourceMaps, input); analyzer.releaseReservation(); expect(calls).toEqual( captured.scripts.items.map(({ script_key }) => script_key), ); expect(result.ok).toBe(true); if (!result.ok) throw result.error; expect( result.value.observations.chunks.edges.map(({ specifier }) => specifier), ).toEqual(["./prefix.js", "./suffix.js"]); expect(result.value.observations.endpoints.map(({ value }) => value)).toEqual( ["/prefix", "/suffix"], ); expect(result.value.completeness).toMatchObject({ status: "partial", parsed_scripts: 2, parse_failures: 0, }); expect(result.value.unknowns).toContainEqual({ dimension: "javascript_analysis", reason: expect.stringContaining("deadline"), affected_script_keys: [captured.scripts.items[1]?.script_key], }); }); it("marks the unstarted suffix when the aggregate deadline expires", async () => { const captured = inspection([ "fetch('/prefix');", "fetch('/stalled');", "fetch('/not-started');", ]); const calls: string[] = []; const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async (script, _timeout, signal) => { calls.push(script.script_key); if (calls.length === 1) return ok(analyzeCapturedWebScript(script)); await waitForAbort(signal); return err({ error: new AnalysisTimeoutError("analyze_web_bundle", 50), completed: null, }); }, close: async () => {}, })); expect((await analyzer.reserve()).ok).toBe(true); const result = await analyzer.analyze(captured, noSourceMaps, { ...input, total_analysis_timeout_ms: 50, }); analyzer.releaseReservation(); expect(calls).toEqual( captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key), ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected total deadline failure"); expect(result.error._tag).toBe("AnalysisTimeoutError"); expect(result.error.partialObservation).toMatchObject({ completeness: { status: "partial", parsed_scripts: 1 }, observations: { endpoints: [{ value: "/prefix" }] }, }); expect(result.error.partialObservation).toMatchObject({ unknowns: expect.arrayContaining([ expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[1]?.script_key], }), expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[2]?.script_key], }), ]), }); }); it("stops after a middle-script result-capacity failure and retains prefix facts", async () => { const captured = inspection([ "import './prefix.js'; fetch('/prefix');", "fetch('/capacity-blocked');", "fetch('/must-not-start');", ]); const calls: string[] = []; const caller = new AbortController(); const primary = new AnalysisResourceConstraintError( "analyze_web_bundle", "memory", "No result capacity remains", { result_bytes: 0 }, ); const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async (script) => { calls.push(script.script_key); if (calls.length === 1) return ok(analyzeCapturedWebScript(script)); return err({ error: primary, completed: null, continuation: "result_capacity_exhausted", }); }, close: async () => { caller.abort(new Error("later cancellation during final close")); }, })); expect((await analyzer.reserve()).ok).toBe(true); const result = await analyzer.analyze( captured, noSourceMaps, input, caller.signal, ); analyzer.releaseReservation(); expect(calls).toEqual( captured.scripts.items.slice(0, 2).map(({ script_key }) => script_key), ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected result-capacity failure"); expect(result.error).toBe(primary); expect(result.error.partialObservation).toMatchObject({ completeness: { status: "partial", parsed_scripts: 1 }, observations: { chunks: { edges: [{ specifier: "./prefix.js" }] }, endpoints: [{ value: "/prefix" }], }, unknowns: expect.arrayContaining([ expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[1]?.script_key], }), expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[2]?.script_key], }), ]), }); }); it("carries the exact caller cancellation reason and completed prefix facts", async () => { const captured = inspection([ "fetch('/prefix');", "fetch('/cancelled');", "fetch('/cancelled-suffix');", ]); const started = deferred(); const controller = new AbortController(); const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async (script, _timeout, signal) => { if (script.script_key === captured.scripts.items[0]?.script_key) return ok(analyzeCapturedWebScript(script)); started.resolve(); await waitForAbort(signal); return err({ error: new AnalysisCancelledError("analyze_web_bundle", { cause: signal?.reason, }), completed: null, }); }, close: async () => {}, })); expect((await analyzer.reserve()).ok).toBe(true); const exactReason = new Error("caller stopped analysis"); const pending = analyzer.analyze( captured, noSourceMaps, input, controller.signal, ); await started.promise; controller.abort(exactReason); const result = await pending; analyzer.releaseReservation(); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation"); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(result.error.cause).toBe(exactReason); expect(result.error.partialObservation).toMatchObject({ completeness: { status: "partial", parsed_scripts: 1 }, observations: { endpoints: [{ value: "/prefix" }] }, unknowns: expect.arrayContaining([ expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[1]?.script_key], }), expect.objectContaining({ dimension: "javascript_analysis", affected_script_keys: [captured.scripts.items[2]?.script_key], }), ]), }); }); it("retains all completed facts when cancellation arrives during worker close", async () => { const captured = inspection(["fetch('/one');", "fetch('/two');"]); const closeStarted = deferred(); let closeWorker: (() => void) | undefined; const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async (script) => ok(analyzeCapturedWebScript(script)), close: async () => { closeStarted.resolve(); await new Promise((resolve) => { closeWorker = resolve; }); }, })); expect((await analyzer.reserve()).ok).toBe(true); const pending = analyzer.analyze(captured, noSourceMaps, input); await closeStarted.promise; const closing = analyzer.close(); closeWorker?.(); await closing; const result = await pending; analyzer.releaseReservation(); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation during finalization"); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(result.error.partialObservation).toMatchObject({ completeness: { status: "complete", parsed_scripts: 2 }, observations: { endpoints: [{ value: "/one" }, { value: "/two" }] }, }); }); it("retains cleanup ownership after mixed worker/close failure and resolves it on retry", async () => { const captured = inspection(["fetch('/partial');"]); const primary = new ProviderCleanupError( "web-bundle-static-analysis", ["worker-job"], { reason: "analysis cleanup pending" }, { operation: "analyze_web_bundle" }, ); const closeFailure = new ProviderCleanupError( "web-bundle-static-analysis", ["worker-root"], { reason: "worker close pending" }, { operation: "analyze_web_bundle" }, ); let closeAttempts = 0; const analyzer = new IsolatedWebBundleAnalyzer(() => ({ analyze: async () => err({ error: primary, completed: null }), close: async () => { closeAttempts += 1; if (closeAttempts <= 2) throw closeFailure; closeFailure.resolveCleanup(); primary.resolveCleanup(); }, })); expect((await analyzer.reserve()).ok).toBe(true); const result = await analyzer.analyze(captured, noSourceMaps, input); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected mixed failure"); expect(result.error.cleanupIncomplete).toBe(true); expect(result.error.cleanupPrimary).toBe(primary); expect(result.error.cleanupResources).toEqual(["worker-job", "worker-root"]); expect(result.error.partialObservation).toMatchObject({ completeness: { status: "partial", parsed_scripts: 0 }, }); const replacement = new IsolatedWebBundleAnalyzer(() => inertWorker()); analyzer.releaseReservation(); const blocked = await replacement.reserve(); expect(blocked.ok).toBe(false); await analyzer.close(); expect(closeAttempts).toBe(3); expect(result.error.cleanupIncomplete).toBe(false); expect(result.error.cleanupResources).toEqual([]); expect((await replacement.reserve()).ok).toBe(true); replacement.releaseReservation(); }); it("enforces one process-wide capture reservation across controller instances", async () => { const first = new IsolatedWebBundleAnalyzer(() => inertWorker()); const second = new IsolatedWebBundleAnalyzer(() => inertWorker()); expect((await first.reserve()).ok).toBe(true); const blocked = await second.reserve(); expect(blocked.ok).toBe(false); if (blocked.ok) throw new Error("Expected process-wide admission refusal"); expect(blocked.error._tag).toBe("AnalysisResourceConstraintError"); first.releaseReservation(); expect((await second.reserve()).ok).toBe(true); second.releaseReservation(); }); const inertWorker = (): WebBundleAnalysisWorkerPort => ({ analyze: async () => err({ error: new AnalysisOutputError("analyze_web_bundle", "unused"), completed: null, }), close: async () => {}, }); const waitForAbort = (signal: AbortSignal | undefined): Promise => { if (signal?.aborted) return Promise.resolve(); return new Promise((resolve) => { signal?.addEventListener("abort", () => resolve(), { once: true }); }); }; const deferred = (): { promise: Promise; resolve: (value: T) => void; } => { let resolve!: (value: T) => void; const promise = new Promise((done) => { resolve = done; }); return { promise, resolve }; }; const inspection = (sources: readonly string[]) => webPageInspectionSchema.parse({ browser: { product: "Fake Chrome", protocol_version: "1.3", revision: "1", user_agent: "fake", js_version: "1", }, target: { target_id: "page-1", type: "page", title: "App", url: `${origin}/app`, origin, attached: false, }, capture_window: { started_at: "2026-07-14T00:00:00.000Z", ended_at: "2026-07-14T00:00:01.000Z", observation_ms: 1_000, }, completeness: { status: "attach_limited", conditions: ["attach_limited"], policy_filtered_sections: [], attach_limited_sections: ["network_requests"], truncated_sections: [], unavailable_sections: [], excluded: [], dropped_events: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0, total: 0, }, }, frames: [], dom: { total_nodes: 0, nodes: [] }, accessibility: { total_nodes: 0, text_capture: { status: "not_approved", retained_bytes: 0, excluded_fields: 0, }, nodes: [], }, scripts: { total: sources.length, items: sources.map((source, index) => ({ script_key: `scr_${String(index + 1).repeat(64)}`, url: `${origin}/assets/${String(index)}.js`, origin, cdp_hash: `hash-${String(index)}`, length: Buffer.byteLength(source), is_module: true, language: "JavaScript", source_map_url: null, resource_reconciliation: { status: "unmatched", reason: "no_exact_sanitized_url", }, source: { included: true, artifact: createWebTextArtifact(source, "text/javascript"), }, })), }, resources: [], network: { requests: [], websocket_connections: [], coverage_started_at: "2026-07-14T00:00:00.000Z", prior_activity_available: false, }, console: { events: [], coverage_started_at: "2026-07-14T00:00:00.000Z", prior_activity_available: false, }, workers: [], metadata: { responses: [], dom_urls: [], agent_hints: [], excluded_dom_urls: 0, headers_allowlisted: true, }, storage: { origin, usage_bytes: null, quota_bytes: null, local_storage_keys: [], session_storage_keys: [], indexed_db_names: [], cache_names: [], values_redacted: true, }, limitations: [], });