Files
rea/src/javascript/recovery/RecoveryFiles.test.ts
T
Kaoru0822 baad9eebe4 fix(javascript): reject FIFO recovery inputs without blocking (#3788)
* fix(javascript): reject FIFO recovery inputs without blocking

scanRecoveryFile opened the selected path with a blocking O_RDONLY open
before checking that it is a regular file. A FIFO with no writer blocks
that open in the libuv threadpool, so cancellation and the recovery
deadline could not settle the read, and the process could not exit.

Admit the input through the shared openRegularFile owner, which opens
with O_NONBLOCK and verifies a regular file before any read. A FIFO,
directory or socket now fails admission immediately with the existing
regular-file error; symlink rejection and cancellation are unchanged.

Fixes #3332

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): keep a failed admission close owned on cancellation

Rethrow RegularFileAdmissionFailure before mapping cancellation, so a
descriptor whose close failed stays owned by its error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:34 -07:00

56 lines
2.2 KiB
TypeScript

import { execFile } from "node:child_process";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it, vi } from "vitest";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.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 } 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) };
});
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,
);
vi.mocked(openRegularFile).mockRejectedValueOnce(failure);
const controller = new AbortController();
controller.abort();
await expect(
readRecoveryFile("/selected.js", 1024, controller.signal),
).rejects.toBe(failure);
});