Require the caller artifact resource scope when reading JavaScript artifacts. Transfer failed nested ASAR cleanup to that scope so later work retries the same owner before admission. Preserve traversal failures and attempt every remaining iterator during unwind. Validation: reconstruction, integrity, content identity, cancellation and nested-reader cleanup boundary checks. BREAKING CHANGE: readJavaScriptArtifactFiles now requires ArtifactResourceScope as its third argument; the optional abort signal is fourth.
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { createPackage } from "@electron/asar";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
|
|
import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js";
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
|
|
import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
it("retains a failed nested reader through blocked work and scope cleanup", async () => {
|
|
const root = await createTestTempDirectory("rea-js-nested-reader-cleanup-");
|
|
const bundle = join(root, "bundle");
|
|
const contents = join(root, "asar-contents");
|
|
const archive = join(bundle, "app.asar");
|
|
await Promise.all([mkdir(bundle), mkdir(contents)]);
|
|
await writeFile(join(contents, "package.json"), '{"main":"index.js"}\n');
|
|
await writeFile(join(contents, "index.js"), "module.exports = 1;\n");
|
|
await createPackage(contents, archive);
|
|
|
|
const scope = new ArtifactResourceScope();
|
|
const originalClose = AsarArtifactReader.prototype.close;
|
|
const closedReaders: AsarArtifactReader[] = [];
|
|
const closeFailure = new Error("nested ASAR close failed");
|
|
let blockReadPhaseClose = true;
|
|
onTestFinished(async () => {
|
|
blockReadPhaseClose = false;
|
|
await scope.close().catch(() => undefined);
|
|
vi.restoreAllMocks();
|
|
});
|
|
vi.spyOn(AsarArtifactReader.prototype, "close").mockImplementation(
|
|
async function (this: AsarArtifactReader) {
|
|
closedReaders.push(this);
|
|
// Inventory closes its nested reader first. Fail the next, read-phase
|
|
// reader close after its native handle remains available for scope retry.
|
|
if (this === closedReaders[1] && blockReadPhaseClose) throw closeFailure;
|
|
await originalClose.call(this);
|
|
},
|
|
);
|
|
|
|
const input = { input_path: bundle };
|
|
const firstFailure = await reconstructJavaScriptArtifact(input, scope).catch(
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(firstFailure).toBeInstanceOf(ArtifactReaderFailure);
|
|
expect(firstFailure).toMatchObject({
|
|
cleanup: {
|
|
reason: closeFailure.message,
|
|
resources: [expect.stringContaining("app.asar")],
|
|
},
|
|
});
|
|
expect(closedReaders).toHaveLength(2);
|
|
const retainedReader = closedReaders[1];
|
|
expect(retainedReader).toBeDefined();
|
|
expect(retainedReader).not.toBe(closedReaders[0]);
|
|
|
|
const blockedWork = await reconstructJavaScriptArtifact(input, scope).catch(
|
|
(cause: unknown) => cause,
|
|
);
|
|
expect(blockedWork).toMatchObject({
|
|
reason: "unavailable",
|
|
cleanup: {
|
|
reason: closeFailure.message,
|
|
resources: [expect.stringContaining("app.asar")],
|
|
},
|
|
});
|
|
expect(closedReaders).toHaveLength(3);
|
|
expect(closedReaders[2]).toBe(retainedReader);
|
|
|
|
blockReadPhaseClose = false;
|
|
await scope.close();
|
|
expect(closedReaders).toHaveLength(4);
|
|
expect(closedReaders[3]).toBe(retainedReader);
|
|
});
|