mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
fix(javascript): retain completed recovery hashes on cancellation
This commit is contained in:
@@ -7,11 +7,16 @@ import { afterEach, expect, it, onTestFinished, vi } from "vitest";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { ArtifactFileCleanupFailure } from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { inventoryRecoveryFiles, readRecoveryFile } from "./RecoveryFiles.js";
|
||||
import {
|
||||
fingerprintRecoveryFile,
|
||||
inventoryRecoveryFiles,
|
||||
readRecoveryFile,
|
||||
} from "./RecoveryFiles.js";
|
||||
|
||||
const closeFault = vi.hoisted(() => ({
|
||||
path: undefined as string | undefined,
|
||||
denied: false,
|
||||
cancel: undefined as AbortController | undefined,
|
||||
handles: [] as FileHandle[],
|
||||
failure: new Error("controlled recovery descriptor close failure"),
|
||||
}));
|
||||
@@ -57,6 +62,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
if (handle.fd >= 0) await close();
|
||||
});
|
||||
handle.close = async () => {
|
||||
closeFault.cancel?.abort();
|
||||
if (closeFault.denied) throw closeFault.failure;
|
||||
await close();
|
||||
};
|
||||
@@ -73,6 +79,7 @@ afterEach(() => {
|
||||
directoryFault.closeAttempts = 0;
|
||||
closeFault.path = undefined;
|
||||
closeFault.denied = false;
|
||||
closeFault.cancel = undefined;
|
||||
closeFault.handles = [];
|
||||
});
|
||||
|
||||
@@ -165,3 +172,29 @@ it.each([
|
||||
expect(resources.hasPending).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains completed hash facts and the exact reason when cancellation arrives during file close", async () => {
|
||||
const { path, bytes } = await selectedFile();
|
||||
const controller = new AbortController();
|
||||
const resources = new ArtifactResourceScope();
|
||||
closeFault.cancel = controller;
|
||||
const failure: unknown = await fingerprintRecoveryFile(
|
||||
path,
|
||||
1024,
|
||||
controller.signal,
|
||||
resources,
|
||||
).catch((cause: unknown) => cause);
|
||||
expect(failure).toMatchObject({
|
||||
_tag: "AnalysisCancelledError",
|
||||
cause: controller.signal.reason,
|
||||
cleanupIncomplete: false,
|
||||
partialObservation: {
|
||||
kind: "file-snapshot-hash",
|
||||
path,
|
||||
bytes: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
},
|
||||
});
|
||||
expect(closeFault.handles[0]?.fd).toBe(-1);
|
||||
await resources.close();
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ArtifactFileCleanupFailure,
|
||||
withArtifactRegularFile,
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { fileSnapshotPartialObservationSchema } from "../../domain/fileSnapshotObservation.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { OwnedDirectoryHandle } from "../../filesystem/OwnedDirectoryHandle.js";
|
||||
@@ -100,11 +101,17 @@ const scanRecoveryFile = async (
|
||||
},
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
cause instanceof ArtifactFileCleanupFailure ||
|
||||
cause instanceof ArtifactFileCancelled
|
||||
)
|
||||
throw cause;
|
||||
if (cause instanceof ArtifactFileCleanupFailure) throw cause;
|
||||
if (cause instanceof ArtifactFileCancelled)
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: cause.fileCancellation.reason,
|
||||
partialObservation: fileSnapshotPartialObservationSchema.parse(
|
||||
Object.assign({}, cause.fileCancellation.outcome.value, {
|
||||
kind: "file-snapshot-hash",
|
||||
path,
|
||||
}),
|
||||
),
|
||||
});
|
||||
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
|
||||
throw new AnalysisCancelledError(OPERATION, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
@@ -182,8 +189,7 @@ const snapshotRecoveryInput = async (
|
||||
cause instanceof AnalysisCancelledError ||
|
||||
cause instanceof AnalysisAccessDeniedError ||
|
||||
cause instanceof AnalysisArtifactChangedError ||
|
||||
cause instanceof ArtifactFileCleanupFailure ||
|
||||
cause instanceof ArtifactFileCancelled
|
||||
cause instanceof ArtifactFileCleanupFailure
|
||||
)
|
||||
throw cause;
|
||||
// The operation-wide input allowance is a deterministic byte budget, not
|
||||
|
||||
Reference in New Issue
Block a user