fix(cli): refuse nonregular experiment inputs without waiting (#670)

The experimental project input reader opens its file non-blocking, so a named pipe with no writer is refused immediately by the existing bounded-regular-file check instead of hanging the command. Regular files read as before.
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-03 23:55:26 -07:00
1 parent d0ae339c23
commit a60ab60d72
2 files changed
+28 -1

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@ export type JevResult = { status: "answered"; answers: Answers; model: string }
const hash = (text: string) => "sha256:" + createHash("sha256").update(text).digest("hex");
export function readBounded(file: string, limit = 1024 * 1024): string {
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
try {
const s = fs.fstatSync(fd);
if (!s.isFile() || s.size > limit) throw Error("input must be a bounded regular file");
@@ -0,0 +1,27 @@
import { afterEach, expect, it } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn, execFileSync } from "node:child_process";
import { readBounded } from "../src/commands/project-jev.js";
const dirs: string[] = [];
afterEach(() => { for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); });
function fixture() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "jev-input-native-")); dirs.push(dir); return dir; }
it.skipIf(process.platform === "win32")("rejects an owned FIFO without waiting for its writer", async () => {
const file = path.join(fixture(), "input.json"); execFileSync("mkfifo", [file]);
const source = new URL("../src/commands/project-jev.ts", import.meta.url).href;
const script = `import { readBounded } from ${JSON.stringify(source)}; try { readBounded(process.argv[1]); process.exitCode=2; } catch(e) { console.log(e.message); }`;
const child = spawn(process.execPath, ["--import", "tsx", "--input-type=module", "-e", script, file], { stdio: ["ignore", "pipe", "pipe"] });
let out = ""; child.stdout.on("data", (chunk) => { out += chunk; });
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 10_000);
const code = await new Promise<number | null>((resolve, reject) => { child.once("close", resolve); child.once("error", reject); });
clearTimeout(timer);
expect({ code, timedOut, out }).toEqual({ code: 0, timedOut: false, out: "input must be a bounded regular file\n" });
});
it("retains normal bounded-file reads and refuses an oversized regular file", () => {
const file = path.join(fixture(), "input.json"); fs.writeFileSync(file, "source bytes");
expect(readBounded(file, 12)).toBe("source bytes");
expect(() => readBounded(file, 11)).toThrow("bounded regular file");
});