import { expect, it } from "vitest"; import type { BrowserObservationPort } from "./BrowserObservationPort.js"; import { listBrowserTargets } from "./BrowserObservationService.js"; import type { BrowserTargetList } from "../domain/browserObservationSchemas.js"; import { parseEvidence } from "../domain/evidence.js"; import { AnalysisCancelledError, AnalysisOutputError, } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { expectDroppedPartial, expectKeptPartial, expectOutputError, expectOwnerError, } from "./ownerObservationExpectation.fixture.js"; 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"; import { analyzeWebBundle } from "./BrowserObservationService.js"; const input = { cdp_endpoint: { transport: "http_discovery" as const, url: "http://127.0.0.1:9222", recorded: "http://127.0.0.1:9222", }, allowed_origins: [], }; const report: BrowserTargetList = { browser: { product: "Chrome", protocol_version: "1.3", revision: "1", user_agent: "Chrome fixture", js_version: "V8 fixture", }, targets: [], excluded: { disallowed_origin: 0, unsupported_url: 0, non_page: 0 }, limitations: [], }; const provider = ( listTargets: BrowserObservationPort["listTargets"], ): BrowserObservationPort => ({ identity: () => ({ id: "browser-fixture", name: "Fixture", version: "1" }), listTargets, }) as unknown as BrowserObservationPort; it("retains completed browser inventory Evidence when the provider completes after abort", async () => { const controller = new AbortController(); const result = await listBrowserTargets( provider(async () => { controller.abort(); return ok(report); }), input, { signal: controller.signal }, ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error._tag).toBe("AnalysisCancelledError"); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "list_browser_targets", normalized_result: report, }); } }); it("does not let late cancellation hide invalid completed browser output", async () => { const malformed = { ...report, limitations: [() => undefined], } as unknown as BrowserTargetList; const output = await listBrowserTargets( provider(async () => ok(malformed)), input, ); expectOutputError(output); const controller = new AbortController(); const late = await listBrowserTargets( provider(async () => { controller.abort(); return ok(malformed); }), input, { signal: controller.signal }, ); expectOutputError(late); }); it("converts a raw page inspection retained after browser cleanup failure", async () => { const inspection = passiveNetworkCapture(); const failure = new BrowserObservationError( "inspect_web_page", "cleanup_failed", { cleanup: { reason: "target detach failed", resources: ["browser target"], }, partialObservation: inspection, }, ); const browser: BrowserObservationPort = { identity: () => ({ id: "browser-fixture", name: "Fixture", version: "1" }), listTargets: async () => ok(report), inspectPage: async () => ({ ok: false, error: failure }), } as unknown as BrowserObservationPort; const pageInput = inspectWebPageInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, allowed_origins: [], target_id: "page", observation_ms: 0, include_script_sources: false, }); const result = await inspectWebPage(browser, pageInput); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBe(failure); expect(result.error.cleanupIncomplete).toBe(true); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "inspect_web_page", normalized_result: inspection, }); } }); it("retains captured page Evidence when bundle analysis fails after capture", async () => { const inspection = passiveNetworkCapture(); const failure = new BrowserObservationError( "analyze_web_bundle", "cancelled", { partialObservation: inspection }, ); const browser: BrowserObservationPort = { ...provider(async () => ok(report)), analyzeBundle: async () => ({ ok: false as const, error: failure }), } as unknown as BrowserObservationPort; const bundleInput = analyzeWebBundleInputSchema.parse({ ...input, cdp_endpoint: input.cdp_endpoint.recorded, target_id: "page", observation_ms: 0, include_script_sources: true, }); const result = await analyzeWebBundle(browser, bundleInput); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBe(failure); expect(failure.operation).toBe("analyze_web_bundle"); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "inspect_web_page", normalized_result: inspection, }); } }); it("retains completed bundle facts as bundle Evidence after cancellation", async () => { const { AnalysisCancelledError } = await import("../domain/analysisErrorCore.js"); const bundle = analyzeWebBundleFixture(passiveNetworkCapture()); const reason = { requested: "stop after completed script" }; const failure = new AnalysisCancelledError("analyze_web_bundle", { cause: reason, partialObservation: bundle, }); const browser: BrowserObservationPort = { ...provider(async () => ok(report)), analyzeBundle: async () => ({ ok: false as const, error: failure }), }; const selected = analyzeWebBundleInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, target_id: "page", max_heap_mb: 256, analysis_timeout_ms: 100, total_analysis_timeout_ms: 1000, }); const result = await analyzeWebBundle(browser, selected); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation"); expect(result.error).toBe(failure); expect(result.error.cause).toBe(reason); expect(parseEvidence(result.error.partialObservation)).toMatchObject({ operation: "analyze_web_bundle", parameters: expect.objectContaining({ max_heap_mb: 256, analysis_timeout_ms: 100, total_analysis_timeout_ms: 1000, }), normalized_result: bundle, }); }); it("preserves a prior caller reason without admitting browser work", async () => { const reason = new AnalysisOutputError( "inspect_web_page", "caller supplied reason", ); const result = await listBrowserTargets( provider(async () => { throw new Error("Cancelled inventory must not start"); }), input, { signal: AbortSignal.abort(reason) }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancelled inventory"); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(result.error.cause).toBe(reason); }); it("keeps page cancellation when the retained inspection is incomplete", async () => { const reason = { requested: "stop during page inspection" }; const partial = { target: { target_id: "page" }, network: {}, capture_window: {}, }; const failure = new AnalysisCancelledError("inspect_web_page", { cause: reason, partialObservation: partial as never, }); const browser = { ...provider(async () => ok(report)), inspectPage: async () => err(failure), } as unknown as BrowserObservationPort; const pageInput = inspectWebPageInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, allowed_origins: [], target_id: "page", observation_ms: 0, include_script_sources: false, }); const result = await inspectWebPage(browser, pageInput); expectKeptPartial(expectOwnerError(result, failure, reason), partial); }); it("keeps bundle cancellation and an earlier failure when retained facts are incomplete", async () => { const reason = { requested: "stop during bundle analysis" }; const partial = { observations: [], capture: { status: "partial" } }; const cancelled = new AnalysisCancelledError("analyze_web_bundle", { cause: reason, partialObservation: partial as never, }); const timedOut = new BrowserObservationError( "analyze_web_bundle", "timeout", { partialObservation: partial as never }, ); const selected = analyzeWebBundleInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, target_id: "page", observation_ms: 0, }); for (const failure of [cancelled, timedOut]) { const browser = { ...provider(async () => ok(report)), analyzeBundle: async () => err(failure), } as unknown as BrowserObservationPort; const result = await analyzeWebBundle(browser, selected); expectKeptPartial( expectOwnerError(result, failure, failure.cause), partial, failure === timedOut ? "provider_timeout" : "cancelled", ); } expect(projectAnalysisError(cancelled).code).toBe("cancelled"); }); const pageInput = () => inspectWebPageInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, allowed_origins: [], target_id: "page", observation_ms: 0, include_script_sources: false, }); const bundleInput = () => analyzeWebBundleInputSchema.parse({ cdp_endpoint: input.cdp_endpoint.recorded, target_id: "page", observation_ms: 0, }); const browserReturning = ( failure: BrowserObservationError | AnalysisCancelledError, ) => ({ ...provider(async () => ok(report)), inspectPage: async () => err(failure), analyzeBundle: async () => err(failure), }) as unknown as BrowserObservationPort; it.each([ { name: "bundle bytes", operation: "analyze_web_bundle" as const, partial: { observations: [], capture: { bytes: Number.NaN } }, }, { name: "bundle observations", operation: "analyze_web_bundle" as const, partial: { observations: [undefined], capture: {} }, }, { name: "page requests", operation: "inspect_web_page" as const, partial: { target: { target_id: "page" }, network: { requests: Number.NaN }, capture_window: {}, }, }, ])( "drops a non-JSON $name partial and keeps cancellation primary", async ({ operation, partial }) => { const reason = { requested: `stop ${operation}` }; const failure = new AnalysisCancelledError(operation, { cause: reason, partialObservation: partial as never, }); const result = operation === "inspect_web_page" ? await inspectWebPage(browserReturning(failure), pageInput()) : await analyzeWebBundle(browserReturning(failure), bundleInput()); const error = expectOwnerError(result, failure, reason); expectDroppedPartial( error, "Browser provider retained a non-JSON incomplete observation.", ); expect(projectAnalysisError(error).details).not.toHaveProperty( "partial_observation", ); }, ); it.each([ { name: "schema-shaped page", partial: { target: { target_id: "page" }, network: { requests: Number.NaN }, capture_window: {}, }, }, { name: "shapeless page", partial: { requests: Number.NaN }, }, ])( "drops a non-JSON $name partial and keeps timeout primary", async ({ partial }) => { const failure = new BrowserObservationError("inspect_web_page", "timeout", { partialObservation: partial as never, }); const error = expectOwnerError( await inspectWebPage(browserReturning(failure), pageInput()), failure, undefined, ); expectDroppedPartial( error, "Browser provider retained a non-JSON incomplete observation.", "provider_timeout", ); expect(projectAnalysisError(error).category).toBe("timeout"); }, );