Files
reverse-engineering-anything/src/javascript/recovery/RecoveryFiles.test.ts
T
morluto 0473a3091c fix(recovery): retain file outcomes and cleanup ownership
Use shared owned-file and directory primitives throughout Wakaru discovery,
snapshots, report parsing and publication. Preserve primary read failures and
completed fingerprints when final close fails or receives cancellation.

Retain exact cleanup capabilities before retiring workspaces, block new work
while cleanup is unresolved, and bind error status to the aggregate owner so
partial cleanup and later retry keep resource attribution current.
2026-10-23 18:21:31 +08:00

190 lines
6.7 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it, vi } from "vitest";
import {
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { ArtifactFileCleanupFailure } from "../../artifacts/ArtifactFileOperation.js";
import { OwnedFileHandle } from "../../filesystem/OwnedFileHandle.js";
import {
openRegularFile,
RegularFileAdmissionFailure,
} from "../../filesystem/RegularFile.js";
import { readWithoutFifoWriter } from "../../../tests/fixtures/fifoInput.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { readRecoveryFile, snapshotRecoveryInputs } from "./RecoveryFiles.js";
vi.mock("../../filesystem/RegularFile.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../filesystem/RegularFile.js")>();
return { ...actual, openRegularFile: vi.fn(actual.openRegularFile) };
});
// Fault injection for the snapshot write; other calls use the real filesystem.
const snapshotFault = vi.hoisted(() => ({
mkdir: undefined as Error | undefined,
writeFile: undefined as Error | undefined,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs/promises")>();
return {
...actual,
mkdir: async (...args: Parameters<typeof actual.mkdir>) => {
if (snapshotFault.mkdir !== undefined) throw snapshotFault.mkdir;
return actual.mkdir(...args);
},
writeFile: async (...args: Parameters<typeof actual.writeFile>) => {
if (snapshotFault.writeFile !== undefined) throw snapshotFault.writeFile;
return actual.writeFile(...args);
},
};
});
const filesystemError = (code: string): NodeJS.ErrnoException =>
Object.assign(new Error(`${code}: injected snapshot failure`), { code });
const snapshotWithFault = async (
fault: Partial<typeof snapshotFault>,
): Promise<unknown> => {
const root = await createTestTempDirectory("rea-recovery-fault-");
const selected = join(root, "bundle.js");
await writeFile(selected, "module.exports = 1;\n");
const snapshots = join(root, "snapshots");
await mkdir(snapshots);
Object.assign(snapshotFault, fault);
try {
return await snapshotRecoveryInputs([selected], snapshots).catch(
(cause: unknown) => cause,
);
} finally {
snapshotFault.mkdir = undefined;
snapshotFault.writeFile = undefined;
}
};
it.skipIf(process.platform === "win32")(
"rejects a named pipe without waiting for a writer",
async () => {
const root = await createTestTempDirectory("rea-recovery-fifo-");
const regular = join(root, "regular.js");
await writeFile(regular, "module.exports = 1;\n");
await expect(readRecoveryFile(regular, 1024)).resolves.toHaveLength(20);
const fifoPath = join(root, "sample.js");
await promisify(execFile)("mkfifo", [fifoPath]);
const outcome = await readWithoutFifoWriter(fifoPath, () =>
readRecoveryFile(fifoPath, 1024).catch((cause: unknown) => cause),
);
if (outcome.state !== "completed")
throw new Error("Recovery input read waited for a FIFO writer");
expect(outcome.result).toBeInstanceOf(AnalysisOutputError);
},
);
it("keeps a failed admission close owned when cancellation is also requested", async () => {
const owner = new OwnedFileHandle({ fd: -1 } as never);
const failure = new RegularFileAdmissionFailure(
"/selected.js",
new Error("cancelled during admission"),
new Error("close failed"),
owner,
);
const controller = new AbortController();
vi.mocked(openRegularFile).mockImplementationOnce(async () => {
controller.abort();
throw failure;
});
const observed = await readRecoveryFile(
"/selected.js",
1024,
controller.signal,
).catch((cause: unknown) => cause);
expect(observed).toBeInstanceOf(ArtifactFileCleanupFailure);
if (!(observed instanceof ArtifactFileCleanupFailure)) return;
expect(observed.fileCleanupFailure.owner).toBe(owner);
expect(observed.fileCleanupFailure.outcome).toEqual({
kind: "failed",
cause: failure.cause,
});
expect(observed.fileCleanupFailure.cleanupCause).toBe(failure.cleanupCause);
});
it("reports one file selected under two letter cases as an input error", async (context) => {
const root = await createTestTempDirectory("rea-recovery-case-");
const lower = join(root, "bundle.js");
const upper = join(root, "BUNDLE.js");
await writeFile(lower, "module.exports = 1;\n");
// Only a case-insensitive filesystem, such as the default Windows and macOS
// ones, resolves both spellings to the selected file.
if (
!(await stat(upper).then(
() => true,
() => false,
))
)
context.skip();
const snapshots = join(root, "snapshots");
await mkdir(snapshots);
const failure = await snapshotRecoveryInputs([lower, upper], snapshots).catch(
(cause: unknown) => cause,
);
expect(failure).toBeInstanceOf(AnalysisInputError);
expect((failure as AnalysisInputError).issues).toEqual([
expect.objectContaining({
path: ["additional_paths"],
message: expect.stringContaining(upper),
}),
]);
});
it("reports one file selected twice as an input error", async () => {
const root = await createTestTempDirectory("rea-recovery-twice-");
const selected = join(root, "bundle.js");
await writeFile(selected, "module.exports = 1;\n");
const snapshots = join(root, "snapshots");
await mkdir(snapshots);
const failure = await snapshotRecoveryInputs(
[selected, selected],
snapshots,
).catch((cause: unknown) => cause);
expect(failure).toBeInstanceOf(AnalysisInputError);
expect((failure as AnalysisInputError).issues).toEqual([
expect.objectContaining({
path: ["additional_paths"],
message: expect.stringContaining("collides with another selected input"),
}),
]);
// The filesystem's own report stays attached for diagnosis.
expect((failure as AnalysisInputError).cause).toMatchObject({
code: "EEXIST",
});
});
it("reports a file and directory name clash as an input error", async () => {
const clash = filesystemError("ENOTDIR");
const failure = await snapshotWithFault({ mkdir: clash });
expect(failure).toBeInstanceOf(AnalysisInputError);
expect((failure as AnalysisInputError).issues).toEqual([
expect.objectContaining({ path: ["additional_paths"] }),
]);
expect((failure as AnalysisInputError).cause).toBe(clash);
});
it("keeps an unrelated snapshot write failure unchanged", async () => {
const fullDisk = filesystemError("ENOSPC");
await expect(snapshotWithFault({ writeFile: fullDisk })).resolves.toBe(
fullDisk,
);
});