fix(artifacts): preserve opaque ZIP cancellation reasons

Use the canonical artifact cancellation checkpoint for enumeration, open and extraction instead of dropping the caller reason in a ZIP-specific helper.

Validation: both new ZIP reason controls fail on the unchanged baseline and pass on the candidate; the focused seven-file run passes 65 tests. Native Windows/Linux were not run.
This commit is contained in:
morluto committed 2026-10-18 02:18:55 +08:00
1 parent 662fad9072
commit f1d18859eb
2 files changed
+39 -9

No files matched your search

+34
View File
@@ -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();
}
},
);
+5 -9
View File
@@ -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<Readable> {
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<Uint8Array>({
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<T>(
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");