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:
1 parent
662fad9072
commit
f1d18859eb
2 files changed
+39
-9
No files matched your search
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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");
|
||||
Reference in new issue
Block a user