diff --git a/src/artifacts/ZipArtifactReader.test.ts b/src/artifacts/ZipArtifactReader.test.ts index 679882ec70..98fef888e5 100644 --- a/src/artifacts/ZipArtifactReader.test.ts +++ b/src/artifacts/ZipArtifactReader.test.ts @@ -1,3 +1,5 @@ +import { writeOrderedZip } from "../../tests/fixtures/artifactEntryOrder.js"; +import { ArtifactReaderFailure } from "./ArtifactReader.js"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -189,3 +191,35 @@ describe("artifactDecompressionBudgetBytes", () => { ); }); }); + +it.each([null, { caller: "opaque ZIP interruption" }])( + "preserves the exact ZIP cancellation reason at enumeration and open: %j", + async (reason) => { + const root = await createTestTempDirectory("rea-zip-cancellation-"); + const path = join(root, "fixture.zip"); + await writeOrderedZip(path, ["member.txt"]); + const reader = new ZipArtifactReader(path, "zip"); + const caller = new AbortController(); + try { + const entries = []; + for await (const entry of reader.entries()) entries.push(entry); + const entry = entries[0]; + if (entry === undefined) throw new Error("Expected real ZIP member"); + caller.abort(reason); + for (const run of [ + () => reader.entries(caller.signal)[Symbol.asyncIterator]().next(), + async () => reader.open(entry, caller.signal), + ]) { + const error: unknown = await run().catch((cause: unknown) => cause); + expect(error).toBeInstanceOf(ArtifactReaderFailure); + if (!(error instanceof ArtifactReaderFailure)) + throw new Error("Expected ZIP cancellation"); + expect(error.reason).toBe("cancelled"); + expect(error.cancellation?.reason).toBe(reason); + expect(error.cause).toBe(reason); + } + } finally { + await reader.close(); + } + }, +); diff --git a/src/artifacts/ZipArtifactReader.ts b/src/artifacts/ZipArtifactReader.ts index 7578542260..1fe4123fe7 100644 --- a/src/artifacts/ZipArtifactReader.ts +++ b/src/artifacts/ZipArtifactReader.ts @@ -1,3 +1,4 @@ +import { assertArtifactReadActive } from "./ArtifactFileOperation.js"; import type { FileHandle } from "node:fs/promises"; import { PassThrough, type Readable } from "node:stream"; import { once } from "node:events"; @@ -194,7 +195,7 @@ export class ZipArtifactReader implements ArtifactReader { for await (const entry of typedZipFailures( this.#reader.getEntriesGenerator({ decodeText: decodeZipEntryText }), )) { - abortIfNeeded(signal); + assertArtifactReadActive(signal); // ZIP member identity is its directory occurrence, not its filename. // Logical path uniqueness belongs to the artifact inventory boundary. const metadata = projectZipEntry(entry, index++); @@ -231,7 +232,7 @@ export class ZipArtifactReader implements ArtifactReader { } open(entry: ArtifactEntry, signal?: AbortSignal): Promise { - abortIfNeeded(signal); + assertArtifactReadActive(signal); const stored = this.#entries.get(entry.adapterKey); if (stored === undefined || !sameArtifactEntry(entry, stored.metadata)) return Promise.reject( @@ -371,7 +372,7 @@ const extractStream = (entry: FileEntry, signal?: AbortSignal): Readable => { }); const writable = new WritableStream({ write: async (chunk) => { - abortIfNeeded(signal); + assertArtifactReadActive(signal); if (!output.write(Buffer.from(chunk))) await once(output, "drain", { signal: controller.signal }); }, @@ -387,7 +388,7 @@ const extractStream = (entry: FileEntry, signal?: AbortSignal): Readable => { signal: controller.signal, checkSignature: true, checkOverlappingEntry: true, - onprogress: () => abortIfNeeded(signal), + onprogress: () => assertArtifactReadActive(signal), }) .catch((cause: unknown) => output.destroy(toError(typedZipFailure(cause)))); return output; @@ -447,10 +448,5 @@ async function* typedZipFailures( const isSymlink = (entry: Entry): boolean => entry.unixMode !== undefined && (entry.unixMode & 0o170000) === 0o120000; -const abortIfNeeded = (signal?: AbortSignal): void => { - if (signal?.aborted === true) - throw new ArtifactReaderFailure("cancelled", "ZIP operation cancelled"); -}; - const toError = (cause: unknown): Error => cause instanceof Error ? cause : new Error("ZIP extraction failed");