From 4e28166626aec19bb9bc60e8117d42fd3af17b22 Mon Sep 17 00:00:00 2001 From: Alex Gorevski Date: Wed, 21 Oct 2026 13:11:15 -0700 Subject: [PATCH] fix(cancellation): preserve caller abort reason in cancellation errors (#6144) * fix(cancellation): preserve caller abort reason in cancellation errors Several application, provider, and MCP boundaries constructed AnalysisCancelledError, ArtifactReaderFailure, or DetachedPgpSignatureError for an already-observed or newly-observed abort without passing the caller's signal.reason as the error cause. The typed result then reported cancellation with an undefined reason even though the exact object supplied to AbortController.abort(reason) was still available. Pass signal.reason as the cause at each producing cancellation site: JavaScript artifact reconstruction and file abort checks, derived MCP operation checkpoints, detached PGP verification, ASAR listing admission, Dart AOT inspection and its provider guard, evidence-file analysis views, binary layout and recorded crash admission, WASM inspection admission, and the Kuna shared assembly-index wait. Cancellation classification and cleanup behavior are unchanged. Fixes morluto/rea#5935 Fixes morluto/rea#5944 Fixes morluto/rea#6100 Fixes morluto/rea#6101 Fixes morluto/rea#6110 Fixes morluto/rea#6113 Fixes morluto/rea#6121 Fixes morluto/rea#6131 Fixes morluto/rea#6132 * test(cancellation): assert the exact caller reason identity Copied reasons satisfied toMatchObject, so the tests would pass even if the caller's abort reason were replaced by a structurally equal copy. Capture the rejection and assert the cause is the same object. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 35b247e9-c793-4b8f-979a-807c27112cf5 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 35b247e9-c793-4b8f-979a-807c27112cf5 --- .../analysisView/AnalysisViewService.test.ts | 37 ++++++++------- .../analysisView/AnalysisViewService.ts | 16 +++++-- .../BinaryLayoutService.test.ts | 27 +++++++++++ .../binaryDiagnostics/BinaryLayoutService.ts | 6 ++- .../RecordedCrashService.test.ts | 27 +++++++++++ .../binaryDiagnostics/RecordedCrashService.ts | 6 ++- src/application/flutter/DartAotInspection.ts | 6 ++- .../JavaScriptArtifactReconstruction.ts | 1 + .../wasm/WasmArtifactService.test.ts | 18 +++++--- src/application/wasm/WasmArtifactService.ts | 6 ++- src/artifacts/AsarListing.ts | 1 + .../javascript/JavaScriptArtifactFiles.ts | 1 + src/flutter/FlutterBuildProvider.ts | 6 ++- src/kuna/KunaProjectReader.ts | 5 ++- src/process/DetachedPgpSignature.test.ts | 20 +++++++++ src/process/DetachedPgpSignature.ts | 1 + src/server/runDerivedOperation.test.ts | 45 ++++++++++++++++++- src/server/runDerivedOperation.ts | 18 ++++++-- .../filesystem/asarListingAdmission.test.ts | 7 ++- .../javascriptArtifactReconstruction.test.ts | 12 +++-- tests/boundary/mcp/dartAotSummaryMcp.test.ts | 30 +++++++++---- .../kuna/kunaReaderCancellation.test.ts | 9 +++- 22 files changed, 253 insertions(+), 52 deletions(-) diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index e40ffea2ed..5e76a87e06 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -1023,27 +1023,32 @@ describe("inspectAnalysisViewWithFiles", () => { it("returns typed cancellation for a cancelled file request", async () => { const controller = new AbortController(); - controller.abort(); + const reason = Object.freeze({ requested: "view-stop" }); + controller.abort(reason); const parent = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); - expect( - await inspectAnalysisViewWithFiles( - { - source: { - kind: "evidence-file", - path: await writeEvidenceFile(parent), - }, - view, + const result = await inspectAnalysisViewWithFiles( + { + source: { + kind: "evidence-file", + path: await writeEvidenceFile(parent), }, - undefined, - { signal: controller.signal }, - ), - ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + view, + }, + undefined, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); }); it("returns typed cancellation when a running file request is aborted", async () => { const controller = new AbortController(); + const reason = Object.freeze({ requested: "running-view-stop" }); const parent = analysisViewCaptureEvidence( environmentManifestCaptureFixture(), ); @@ -1054,11 +1059,13 @@ describe("inspectAnalysisViewWithFiles", () => { { signal: controller.signal }, ); // Queued before the request yields after its synchronous work, so it is seen even if the read already finished. - setImmediate(() => controller.abort()); - expect(await running).toMatchObject({ + setImmediate(() => controller.abort(reason)); + const result = await running; + expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" }, }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); }); it("leaves file sources to the file-aware entry point", () => { diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index 6203f1d21b..2e5cb772d7 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -97,7 +97,11 @@ export const inspectAnalysisViewValidatedWithFiles = async ( }; // A request cancelled during authentication reports cancellation, even when authentication failed. if (await cancelled()) - return err(new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION)); + return err( + new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, { + cause: options.signal?.reason, + }), + ); if (!loaded.ok) return loaded; const id = loaded.value.evidence_id; const projected = inspectAnalysisViewValidated( @@ -105,9 +109,13 @@ export const inspectAnalysisViewValidatedWithFiles = async ( (evidenceId) => (evidenceId === id ? loaded.value : lookup?.(evidenceId)), ); // A request cancelled during projection delivers and records nothing. - return (await cancelled()) - ? err(new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION)) - : projected; + if (await cancelled()) + return err( + new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, { + cause: options.signal?.reason, + }), + ); + return projected; }; /** Authenticate borrowed inline Evidence after adapter envelope validation. */ diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts index 76532d5e28..3ab12cfe26 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -64,6 +64,33 @@ it("preserves a provider's actionable unsupported reason", async () => { expect(await service.inspect({ path: "/source.elf" })).toEqual(err(failure)); }); +it("preserves the caller's exact abort reason before invocation", async () => { + const controller = new AbortController(); + const reason = Object.freeze({ requested: "layout-stop" }); + controller.abort(reason); + let calls = 0; + const service = new BinaryLayoutService({ + inspectAndClose(input, options) { + return this.inspect(input, options); + }, + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => { + calls++; + return Promise.resolve(ok(binaryLayoutFixture())); + }, + }); + const result = await service.inspect( + { path: "/artifacts/source-owned.elf" }, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); + expect(calls).toBe(0); +}); + it("cancels before invocation and after an acquired observation", async () => { const controller = new AbortController(); const service = new BinaryLayoutService({ diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts index 53078bb948..d3315309a6 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -54,7 +54,11 @@ export class BinaryLayoutService { ) => ReturnType, ): Promise> { if (options?.signal?.aborted) - return err(new AnalysisCancelledError(OPERATION)); + return err( + new AnalysisCancelledError(OPERATION, { + cause: options.signal.reason, + }), + ); const input = inspectBinaryLayoutInputSchema.safeParse(rawInput); if (!input.success) return err( diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts index 7e225b795e..9c807a2822 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.test.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts @@ -93,6 +93,33 @@ it("preserves a provider's actionable unsupported reason", async () => { expect(await service.inspect({ path: "/source.elf" })).toEqual(err(failure)); }); +it("preserves the caller's exact abort reason before invocation", async () => { + const controller = new AbortController(); + const reason = Object.freeze({ requested: "crash-stop" }); + controller.abort(reason); + let calls = 0; + const service = new RecordedCrashService({ + inspectAndClose(input, options) { + return this.inspect(input, options); + }, + identity: RECORDED_CRASH_TEST_PROVIDER, + inspect: () => { + calls++; + return Promise.resolve(ok(recordedCrashFixture())); + }, + }); + const result = await service.inspect( + { path: "/artifacts/source-owned.core" }, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); + expect(calls).toBe(0); +}); + it("cancels before invocation and after an acquired observation", async () => { const controller = new AbortController(); const service = new RecordedCrashService({ diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts index 7a0ebd0846..0aa9bbcf7a 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.ts @@ -55,7 +55,11 @@ export class RecordedCrashService { ) => ReturnType, ): Promise> { if (options?.signal?.aborted) - return err(new AnalysisCancelledError(OPERATION)); + return err( + new AnalysisCancelledError(OPERATION, { + cause: options.signal.reason, + }), + ); const input = inspectRecordedCrashInputSchema.safeParse(rawInput); if (!input.success) return err( diff --git a/src/application/flutter/DartAotInspection.ts b/src/application/flutter/DartAotInspection.ts index c11bbe1f43..df49188388 100644 --- a/src/application/flutter/DartAotInspection.ts +++ b/src/application/flutter/DartAotInspection.ts @@ -21,7 +21,11 @@ export const inspectDartAot = async ( ): Promise> => { const operation = "inspect_dart_aot"; if (options?.signal?.aborted === true) - return err(new AnalysisCancelledError(operation)); + return err( + new AnalysisCancelledError(operation, { + cause: options.signal.reason, + }), + ); const parsed = dartAotInspectionInputSchema.safeParse(rawInput); if (!parsed.success) return err( diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts index 2252e5a6b1..999bdbf57c 100644 --- a/src/application/javascript/JavaScriptArtifactReconstruction.ts +++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts @@ -426,5 +426,6 @@ const abortIfNeeded = (signal?: AbortSignal): void => { throw new ArtifactReaderFailure( "cancelled", "JavaScript artifact reconstruction cancelled", + { cause: signal.reason }, ); }; diff --git a/src/application/wasm/WasmArtifactService.test.ts b/src/application/wasm/WasmArtifactService.test.ts index 4933fa9168..e79b07b4a5 100644 --- a/src/application/wasm/WasmArtifactService.test.ts +++ b/src/application/wasm/WasmArtifactService.test.ts @@ -101,13 +101,17 @@ it("cancels before provider work and delivers observed Evidence on success", asy }, }); const controller = new AbortController(); - controller.abort(); - expect( - await service.inspect( - { path: value.artifact.path }, - { signal: controller.signal }, - ), - ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + const reason = Object.freeze({ requested: "wasm-stop" }); + controller.abort(reason); + const cancelled = await service.inspect( + { path: value.artifact.path }, + { signal: controller.signal }, + ); + expect(cancelled).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(cancelled.ok ? undefined : cancelled.error.cause).toBe(reason); expect(calls).toBe(0); expect(await service.inspect({ path: value.artifact.path })).toMatchObject({ ok: true, diff --git a/src/application/wasm/WasmArtifactService.ts b/src/application/wasm/WasmArtifactService.ts index 10927acf0e..99d3eb9482 100644 --- a/src/application/wasm/WasmArtifactService.ts +++ b/src/application/wasm/WasmArtifactService.ts @@ -36,7 +36,11 @@ export class WasmArtifactService { options?: ExecutionOptions, ): Promise> { if (options?.signal?.aborted) - return err(new AnalysisCancelledError(OPERATION)); + return err( + new AnalysisCancelledError(OPERATION, { + cause: options.signal.reason, + }), + ); const input = inspectWasmArtifactInputSchema.safeParse(rawInput); if (!input.success) return err( diff --git a/src/artifacts/AsarListing.ts b/src/artifacts/AsarListing.ts index 60875e93bc..f8973dc952 100644 --- a/src/artifacts/AsarListing.ts +++ b/src/artifacts/AsarListing.ts @@ -52,6 +52,7 @@ export const readAsarListing = async ( throw new ArtifactReaderFailure( "cancelled", "ASAR operation cancelled", + { cause: signal.reason }, ); } const frame = pending.at(-1); diff --git a/src/artifacts/javascript/JavaScriptArtifactFiles.ts b/src/artifacts/javascript/JavaScriptArtifactFiles.ts index 562235eed7..1714abaae2 100644 --- a/src/artifacts/javascript/JavaScriptArtifactFiles.ts +++ b/src/artifacts/javascript/JavaScriptArtifactFiles.ts @@ -628,5 +628,6 @@ const abortIfNeeded = (signal?: AbortSignal): void => { throw new ArtifactReaderFailure( "cancelled", "JavaScript artifact reconstruction cancelled", + { cause: signal.reason }, ); }; diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts index e67367e965..605998c5dc 100644 --- a/src/flutter/FlutterBuildProvider.ts +++ b/src/flutter/FlutterBuildProvider.ts @@ -594,7 +594,11 @@ export class FlutterBuildProvider { options?: ExecutionOptions, ): Promise> { if (options?.signal?.aborted === true) - return err(new AnalysisCancelledError(request.operation)); + return err( + new AnalysisCancelledError(request.operation, { + cause: options.signal.reason, + }), + ); const { path } = request.input; const signal = options?.signal; let snapshot: OwnedSnapshot | undefined; diff --git a/src/kuna/KunaProjectReader.ts b/src/kuna/KunaProjectReader.ts index 6b534ef6e0..605461ec17 100644 --- a/src/kuna/KunaProjectReader.ts +++ b/src/kuna/KunaProjectReader.ts @@ -182,7 +182,10 @@ export class KunaProjectReader { ), ); const index = await waitForAbortable(this.#assembly, signal); - if (index === ABORTED) return err(new AnalysisCancelledError(operation)); + if (index === ABORTED) + return err( + new AnalysisCancelledError(operation, { cause: signal?.reason }), + ); return index.ok ? index : err( diff --git a/src/process/DetachedPgpSignature.test.ts b/src/process/DetachedPgpSignature.test.ts index ee240089fa..8be5489e0e 100644 --- a/src/process/DetachedPgpSignature.test.ts +++ b/src/process/DetachedPgpSignature.test.ts @@ -110,6 +110,26 @@ describe("detached binary PGP publisher verification", () => { ).toMatchObject({ ok: false, error: { reason: "cancelled" } }); }); + it.each([null, "caller stop", { phase: "publisher-key" }])( + "preserves the caller's exact abort reason %j as the cancellation cause", + async (reason) => { + const controller = new AbortController(); + controller.abort(reason); + const result = await verifyDetachedPgpSignature( + bytes, + signatures[0]!, + publicKey, + fingerprint, + controller.signal, + ); + expect(result).toMatchObject({ + ok: false, + error: { reason: "cancelled" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); + }, + ); + it("preserves cancellation while loading the publisher key, before classifying its identity", async () => { const controller = new AbortController(); const verification = verifyDetachedPgpSignature( diff --git a/src/process/DetachedPgpSignature.ts b/src/process/DetachedPgpSignature.ts index 8d1a215cd1..19ab39615a 100644 --- a/src/process/DetachedPgpSignature.ts +++ b/src/process/DetachedPgpSignature.ts @@ -33,6 +33,7 @@ export const verifyDetachedPgpSignature = async ( new DetachedPgpSignatureError( "cancelled", "Package verification was cancelled.", + { cause: signal?.reason }, ), ); const isAborted = () => signal?.aborted === true; diff --git a/src/server/runDerivedOperation.test.ts b/src/server/runDerivedOperation.test.ts index dabec6cf14..b2ac6258b7 100644 --- a/src/server/runDerivedOperation.test.ts +++ b/src/server/runDerivedOperation.test.ts @@ -9,12 +9,16 @@ import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; const context = ( controller: AbortController, notifications: unknown[] = [], + onMessage: (message: string) => void = () => undefined, ) => ({ mcpReq: { signal: controller.signal, _meta: { progressToken: "derived-test" }, notify: (notification: unknown) => { notifications.push(notification); + const message = (notification as { params?: { message?: unknown } }) + .params?.message; + if (typeof message === "string") onMessage(message); return Promise.resolve(); }, }, @@ -23,7 +27,8 @@ const context = ( describe("derived MCP operation boundary", () => { it("cancels before computation and never evaluates the operation", async () => { const controller = new AbortController(); - controller.abort(); + const reason = Object.freeze({ requested: "pre-compute-stop" }); + controller.abort(reason); let computed = false; const result = await runDerivedOperation( context(controller), @@ -38,9 +43,47 @@ describe("derived MCP operation boundary", () => { ok: false, error: { _tag: "AnalysisCancelledError" }, }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); expect(computed).toBe(false); }); + it("preserves the request's abort reason when cancellation wins after computation", async () => { + const controller = new AbortController(); + const reason = Object.freeze({ requested: "post-compute-stop" }); + const result = await runDerivedOperation( + context(controller), + "compare", + () => { + controller.abort(reason); + return "must-not-publish"; + }, + ); + + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); + }); + + it("preserves the request's abort reason when cancellation wins at terminal delivery", async () => { + const controller = new AbortController(); + const reason = Object.freeze({ requested: "terminal-stop" }); + const result = await runDerivedOperation( + context(controller, [], (message) => { + if (message.startsWith("complete:")) controller.abort(reason); + }), + "compare", + () => "must-not-publish", + ); + + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(result.ok ? undefined : result.error.cause).toBe(reason); + }); + it("lets cancellation win immediately before result publication", async () => { const controller = new AbortController(); const result = await runDerivedOperation( diff --git a/src/server/runDerivedOperation.ts b/src/server/runDerivedOperation.ts index f34a2b21e3..6d2eaffd8e 100644 --- a/src/server/runDerivedOperation.ts +++ b/src/server/runDerivedOperation.ts @@ -31,7 +31,11 @@ export const runDerivedOperation = async ( }); await setImmediate(); if (context.mcpReq.signal.aborted) - return err(new AnalysisCancelledError(operation)); + return err( + new AnalysisCancelledError(operation, { + cause: context.mcpReq.signal.reason, + }), + ); let value: Value; try { value = compute(); @@ -62,7 +66,11 @@ export const runDerivedOperation = async ( }); await setImmediate(); if (context.mcpReq.signal.aborted) - return err(new AnalysisCancelledError(operation)); + return err( + new AnalysisCancelledError(operation, { + cause: context.mcpReq.signal.reason, + }), + ); await progress.report({ phase: "complete", completed: 2, @@ -71,6 +79,10 @@ export const runDerivedOperation = async ( terminal: true, }); if (context.mcpReq.signal.aborted) - return err(new AnalysisCancelledError(operation)); + return err( + new AnalysisCancelledError(operation, { + cause: context.mcpReq.signal.reason, + }), + ); return ok(value); }; diff --git a/tests/boundary/filesystem/asarListingAdmission.test.ts b/tests/boundary/filesystem/asarListingAdmission.test.ts index f5ebaa870d..9762adff9f 100644 --- a/tests/boundary/filesystem/asarListingAdmission.test.ts +++ b/tests/boundary/filesystem/asarListingAdmission.test.ts @@ -22,6 +22,7 @@ const archiveWith = async (bytes: Buffer) => { it("interrupts expanded listing admission between traversal batches", async () => { const controller = new AbortController(); + const reason = Object.freeze({ requested: "listing-stop" }); const header = { files: Object.fromEntries( Array.from({ length: 4000 }, (_, index) => [ @@ -32,8 +33,10 @@ it("interrupts expanded listing admission between traversal batches", async () = }; const listing = readAsarListing(header, controller.signal); await setImmediate(); - controller.abort(); - await expect(listing).rejects.toMatchObject({ reason: "cancelled" }); + controller.abort(reason); + const error: unknown = await listing.catch((caught: unknown) => caught); + expect(error).toMatchObject({ reason: "cancelled" }); + expect((error as { cause?: unknown }).cause).toBe(reason); }); it("rejects path expansion before publishing any inventory, preserving the input", async () => { diff --git a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts index d5e5c67a0b..f5db813439 100644 --- a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts @@ -404,10 +404,14 @@ it("rejects traversal from a production reader seam and malformed ASAR container it("preserves cancellation and explicit format diagnostics", async () => { const root = await fixtureDirectory(); const controller = new AbortController(); - controller.abort(); - await expect( - reconstructJavaScriptArtifact({ input_path: root }, controller.signal), - ).rejects.toMatchObject({ reason: "cancelled" }); + const reason = Object.freeze({ requested: "reconstruction-stop" }); + controller.abort(reason); + const cancelled: unknown = await reconstructJavaScriptArtifact( + { input_path: root }, + controller.signal, + ).catch((caught: unknown) => caught); + expect(cancelled).toMatchObject({ reason: "cancelled" }); + expect((cancelled as { cause?: unknown }).cause).toBe(reason); await expect( reconstructJavaScriptArtifact({ input_path: root, format: "asar" }), ).rejects.toMatchObject({ diff --git a/tests/boundary/mcp/dartAotSummaryMcp.test.ts b/tests/boundary/mcp/dartAotSummaryMcp.test.ts index be6a970d3d..9962bb74f3 100644 --- a/tests/boundary/mcp/dartAotSummaryMcp.test.ts +++ b/tests/boundary/mcp/dartAotSummaryMcp.test.ts @@ -273,13 +273,27 @@ it("rejects unavailable retention, invalid delivery and cancellation before prod retention, ), ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } }); - expect( - await inspectDartAot( - service, - { path: "/missing.apk", detail: "summary" }, - retention, - { signal: AbortSignal.abort() }, - ), - ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); + const reason = Object.freeze({ requested: "dart-stop" }); + const cancelled = await inspectDartAot( + service, + { path: "/missing.apk", detail: "summary" }, + retention, + { signal: AbortSignal.abort(reason) }, + ); + expect(cancelled).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(cancelled.ok ? undefined : cancelled.error.cause).toBe(reason); + const executed = await service.execute( + "inspect_dart_aot", + { path: "/missing.apk", abi: "x86_64" }, + { signal: AbortSignal.abort(reason) }, + ); + expect(executed).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(executed.ok ? undefined : executed.error.cause).toBe(reason); expect(execute).not.toHaveBeenCalled(); }); diff --git a/tests/boundary/providers/kuna/kunaReaderCancellation.test.ts b/tests/boundary/providers/kuna/kunaReaderCancellation.test.ts index 0b86384e6a..dce88a0bd9 100644 --- a/tests/boundary/providers/kuna/kunaReaderCancellation.test.ts +++ b/tests/boundary/providers/kuna/kunaReaderCancellation.test.ts @@ -84,12 +84,17 @@ it.each([ const other = run(client, "procedure_assembly", { procedure: "parse" }); try { await expect.poll(() => held.streams.length).toBe(1); - controller.abort(); + const reason = Object.freeze({ requested: "reader-stop", operation }); + controller.abort(reason); await expect.poll(() => finished, { timeout: 1000 }).toBe(true); - expect(await cancelled).toMatchObject({ + const cancelledResult = await cancelled; + expect(cancelledResult).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" }, }); + expect(cancelledResult.ok ? undefined : cancelledResult.error.cause).toBe( + reason, + ); expect(held.streams[0]?.destroyed).toBe(false); expect((await run(client, "health")).result).toBeDefined(); held.streams[0]?.end(bytes);