mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -54,7 +54,11 @@ export class BinaryLayoutService {
|
||||
) => ReturnType<BinaryLayoutPort["inspect"]>,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -55,7 +55,11 @@ export class RecordedCrashService {
|
||||
) => ReturnType<RecordedCrashPort["inspect"]>,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
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(
|
||||
|
||||
@@ -21,7 +21,11 @@ export const inspectDartAot = async (
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
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(
|
||||
|
||||
@@ -426,5 +426,6 @@ const abortIfNeeded = (signal?: AbortSignal): void => {
|
||||
throw new ArtifactReaderFailure(
|
||||
"cancelled",
|
||||
"JavaScript artifact reconstruction cancelled",
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,7 +36,11 @@ export class WasmArtifactService {
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
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(
|
||||
|
||||
@@ -52,6 +52,7 @@ export const readAsarListing = async (
|
||||
throw new ArtifactReaderFailure(
|
||||
"cancelled",
|
||||
"ASAR operation cancelled",
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
}
|
||||
const frame = pending.at(-1);
|
||||
|
||||
@@ -628,5 +628,6 @@ const abortIfNeeded = (signal?: AbortSignal): void => {
|
||||
throw new ArtifactReaderFailure(
|
||||
"cancelled",
|
||||
"JavaScript artifact reconstruction cancelled",
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -594,7 +594,11 @@ export class FlutterBuildProvider {
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -33,6 +33,7 @@ export const verifyDetachedPgpSignature = async (
|
||||
new DetachedPgpSignatureError(
|
||||
"cancelled",
|
||||
"Package verification was cancelled.",
|
||||
{ cause: signal?.reason },
|
||||
),
|
||||
);
|
||||
const isAborted = () => signal?.aborted === true;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -31,7 +31,11 @@ export const runDerivedOperation = async <Value>(
|
||||
});
|
||||
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 <Value>(
|
||||
});
|
||||
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 <Value>(
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user