diff --git a/src/application/pcap/PcapAnalysisService.test.ts b/src/application/pcap/PcapAnalysisService.test.ts index ec7e64763f..b4ab7950f2 100644 --- a/src/application/pcap/PcapAnalysisService.test.ts +++ b/src/application/pcap/PcapAnalysisService.test.ts @@ -1,5 +1,11 @@ import { expect, it } from "vitest"; +import { createAnalysisExecution } from "../AnalysisProvider.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; +import { parseEvidence } from "../../domain/evidence.js"; +import { pcapResultSchemas } from "../../domain/pcap/pcapAnalysis.js"; +import { err, ok } from "../../domain/result.js"; + import { PcapAnalysisService } from "./PcapAnalysisService.js"; it("preserves the exact caller reason when pcap admission is already aborted", async () => { @@ -26,3 +32,148 @@ it("preserves the exact caller reason when pcap admission is already aborted", a expect(result.error.cause).toBe(reason); expect(calls).toBe(0); }); + +const identity = { + id: "pcap", + name: "Packet capture analysis", + version: "test", +}; +const path = "/captures/selected.pcap"; +const target = { path, bytes: 24, sha256: "c".repeat(64) }; +const client = { + tshark_command: "/selected/tshark", + tshark_source: "environment", + tshark_version: "4.6.6", + capinfos_command: "/selected/capinfos", + capinfos_source: "environment", + capinfos_version: "4.6.6", +}; +const capture = { + client, + target, + file_type: "pcap", + encapsulation: "Ethernet", + packet_count: 0, + capture_start: null, + capture_end: null, + capture_duration_seconds: null, + snapshot_length: null, + endpoints: [], + endpoint_count: 0, + dns_queries: [], + dns_query_count: 0, + tls_sni_values: [], + tls_sni_count: 0, + protocol_distribution: [], + protocol_count: 0, + http_request_uris: [], + http_request_uri_count: 0, + http_uri_redaction_count: 0, + unattributed_dns_addresses: [], + unattributed_dns_address_count: 0, + unattributed_dns_packet_count: 0, + coverage: "complete", +}; + +it.each(["inspect_pcap_client", "inspect_pcap_capture"] as const)( + "retains completed %s Evidence if cancellation arrives at application handoff", + async (operation) => { + const reason = Object.freeze({ + requested: "stop after completed observation", + }); + for (const aborted of [false, true]) { + const controller = new AbortController(); + const execution = createAnalysisExecution( + operation === "inspect_pcap_client" ? { client } : capture, + identity, + { + ...(operation === "inspect_pcap_capture" + ? { + subject: { + path, + sha256: target.sha256, + format: "file" as const, + }, + } + : {}), + limitations: ["Fixture observation; no live tool verification"], + rawResult: { selected_output: "preserved" }, + }, + ); + const service = new PcapAnalysisService({ + inspectAvailability: () => Promise.reject(new Error("unused")), + close: async () => {}, + execute: async () => { + if (aborted) controller.abort(reason); + return ok(execution); + }, + }); + const result = await service.execute( + operation, + operation === "inspect_pcap_client" ? {} : { path }, + { signal: controller.signal }, + ); + const observation = result.ok + ? result.value + : result.error.partialObservation; + if (aborted) { + if (result.ok) + throw new Error("Expected cancellation after provider completion"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + } else if (!result.ok) throw result.error; + const evidence = parseEvidence(observation); + expect(evidence.operation).toBe(operation); + expect(evidence.provider).toEqual(identity); + expect(evidence.raw_result).toEqual({ selected_output: "preserved" }); + expect(evidence.limitations).toContain( + "Fixture observation; no live tool verification", + ); + expect(evidence.parameters).toEqual( + operation === "inspect_pcap_client" ? {} : { path }, + ); + expect(evidence.normalized_result).toEqual( + operation === "inspect_pcap_client" ? { client } : capture, + ); + expect(evidence.confidence).toBe( + operation === "inspect_pcap_capture" ? "derived" : "observed", + ); + if (operation === "inspect_pcap_capture") { + expect(evidence.authority).toBe("historical-reference"); + expect(evidence.subject).toMatchObject({ + local_path: path, + digest: { sha256: target.sha256 }, + }); + } + pcapResultSchemas[operation].parse(evidence.normalized_result); + } + }, +); + +it.each(["inspect_pcap_client", "inspect_pcap_capture"] as const)( + "preserves the provider's %s failure when cancellation arrives before consumption", + async (operation) => { + const controller = new AbortController(); + const failure = new AnalysisOutputError( + operation, + "The provider could not decode its output", + { diagnostics: { provider_detail: "retained" } }, + ); + const service = new PcapAnalysisService({ + inspectAvailability: () => Promise.reject(new Error("unused")), + close: async () => {}, + execute: async () => { + controller.abort({ requested: "late cancellation" }); + return err(failure); + }, + }); + const result = await service.execute( + operation, + operation === "inspect_pcap_client" ? {} : { path }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Expected the provider failure"); + expect(result.error).toBe(failure); + expect(result.error.diagnostics).toEqual({ provider_detail: "retained" }); + }, +); diff --git a/src/application/pcap/PcapAnalysisService.ts b/src/application/pcap/PcapAnalysisService.ts index cf2c8a94c3..14e1536823 100644 --- a/src/application/pcap/PcapAnalysisService.ts +++ b/src/application/pcap/PcapAnalysisService.ts @@ -11,7 +11,8 @@ import { import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { Evidence } from "../../domain/evidence.js"; import { createPcapEvidence } from "../../domain/pcap/pcapEvidence.js"; -import { err, ok, type Result } from "../../domain/result.js"; +import { err, type Result } from "../../domain/result.js"; +import { completeProviderEvidence } from "../ProviderOperationExecution.js"; /** Shared CLI/MCP admission, execution, and Evidence composition for pcap. */ export class PcapAnalysisService { @@ -34,7 +35,9 @@ export class PcapAnalysisService { return err(new AnalysisInputError(operation, { cause: request.error })); const executed = await this.provider.execute(request.data, options); if (!executed.ok) return executed; - const execution = executed.value; - return ok(createPcapEvidence(request.data, execution)); + return completeProviderEvidence( + createPcapEvidence(request.data, executed.value), + options?.signal, + ); } }