fix(slice-21 FR-4 cleanup): TTY comment intent-vs-behavior reconcile + --body-file directory path → 3-part error shape (OPR.0.3.2.21.FR-4)

Closes the two non-blocking observations from guard's FR-4(a)
review (qitem-20260601123549-f66d52a5):

(1) TTY comment intent-vs-behavior mismatch. The prior inline
    comment in defaultStdinReader said "the caller's empty-body
    check turns this into a 3-part error" — but the FR-4(a)
    handoff body explicitly delegates empty-body validation to
    the daemon, and no caller-side empty-body check exists.
    The comment is rewritten to match actual behavior: empty
    body flows through to the daemon's content validation;
    the CLI does not error locally on empty stdin.

(2) --body-file path-is-a-directory case used to fall through
    to fs.readFileSync and surface a bare Error("EISDIR:
    illegal operation on a directory") with blank
    consequence/action fields. The other body-resolve failure
    modes (missing file, mutual exclusion, missing input) all
    emit the 3-part fact/consequence/action shape; this case
    is now consistent. After the existsSync check, a
    statSync(absPath).isFile() gate fires the 3-part error
    before any read attempt, naming "not a regular file" with
    "Pass a path to a readable file (not a directory,
    symlink-to-directory, or block device); use --body-file -
    to read from stdin" as the action.

Test addition: a new discriminator in the FR-4(a) describe block
asserts the 3-part shape when --body-file points at a directory
(uses mkdtempSync for a guaranteed-fresh tmpdir as the bad
target). Discriminator-flip: without the statSync gate, the
test fails because the error reaches the assertion with blank
consequence/action fields.

Verification:
- packages/cli queue.test.ts: 29/29 PASS (28 prior across FR-4
  a/b/c describe blocks + 1 new directory-case discriminator).
- packages/cli tsc EXIT=0.
- Commit body verification grep clean per banked discipline.
- git diff --cached --stat: 2 files (+32 / -2).

Branch: slice-21-fr-4-queue-ergonomics off LOCAL main 58713e0c.
Chain: 2f731b84 (FR-4a, CLEAR) → 207ad5f1 (FR-4b, CLEAR) →
50b17c73 (FR-4c, pending verdict) → 164a67be (FR-4d-docs,
pending verdict) → this commit (FR-4 cleanup, addresses both
of guard's FR-4(a) non-blocking notes). LOCAL only, NO push.
This commit is contained in:
mvschwarz committed 2026-06-01 05:40:50 -07:00
1 parent 164a67bef2
commit a66e1488f1
2 files changed
+32 -2

No files matched your search

+13 -2
View File
@@ -67,8 +67,11 @@ export async function defaultStdinReader(): Promise<string> {
process.stdin.on("end", () => resolve(data));
process.stdin.on("error", reject);
if (process.stdin.isTTY) {
// Resolve immediately to empty if nothing is piped; the caller's
// empty-body check turns this into a 3-part error.
// No pipe is connected to stdin; resolve immediately to empty
// rather than blocking forever waiting for data on a TTY. The
// empty body then flows through to the daemon's content
// validation (queue-repository owns the body contract); the CLI
// does not error locally on empty stdin.
resolve("");
}
});
@@ -108,6 +111,14 @@ export async function resolveQueueBody(
err.action = "Check the path; pass an absolute path; or use --body-file - to read from stdin.";
throw err;
}
const stat = fs.statSync(absPath);
if (!stat.isFile()) {
const err = new Error(`--body-file path is not a regular file: ${absPath}`) as Error & { fact?: string; consequence?: string; action?: string };
err.fact = `--body-file path is not a regular file: ${absPath}`;
err.consequence = "rig queue create did not run; daemon was not contacted.";
err.action = "Pass a path to a readable file (not a directory, symlink-to-directory, or block device). Use --body-file - to read from stdin.";
throw err;
}
return fs.readFileSync(absPath, "utf8");
}
+19
View File
@@ -188,6 +188,25 @@ describe("rig queue CLI", () => {
});
});
// OPR.0.3.2.21.FR-4 cleanup (guard non-blocking note on FR-4a CLEAR): a
// directory passed to --body-file used to fall through to fs.readFileSync
// and surface a bare Error("EISDIR: illegal operation on a directory") with
// blank consequence/action. The cleanup commit emits the 3-part shape
// explicitly so the error reads consistently with the other body-resolve
// failure modes.
it("resolveQueueBody throws 3-part error when --body-file path is a directory (not a regular file)", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "queue-body-isdir-"));
try {
await expect(resolveQueueBody({ bodyFile: tmp })).rejects.toMatchObject({
fact: expect.stringMatching(/not a regular file/),
consequence: expect.stringMatching(/did not run/),
action: expect.stringMatching(/Pass a path to a readable file/),
});
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
it("resolveQueueBody calls the injected stdin reader when --body is -", async () => {
const stdinReader = vi.fn(async () => "from stdin\n");
const out = await resolveQueueBody({ body: "-" }, stdinReader);