Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/lib/cli/oclif-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,97 @@ describe("runOclifCommandById", () => {
expect(exit).toHaveBeenCalledWith(2);
});

it("formats an invalid enum flag value instead of rethrowing it (#8123)", async () => {
class FlagInvalidOptionError extends Error {
oclif = { exit: 2 };
parse = {};
showHelp = false;
}
runCommandMock.mockRejectedValue(
new FlagInvalidOptionError(
"Expected --reasoning-effort=ultra to be one of: low, medium, high, default\nSee more help with --help",
),
);
const errorLine = vi.fn();
const exit = vi.fn((code: number): never => {
throw new Error(`exit:${code}`);
});

await expect(
runOclifCommandById("inference:set", ["--reasoning-effort", "ultra"], {
rootDir: "/repo",
error: errorLine,
exit,
}),
).rejects.toThrow("exit:2");

expect(errorLine).toHaveBeenCalledWith(
" Expected --reasoning-effort=ultra to be one of: low, medium, high, default\nSee more help with --help",
);
expect(exit).toHaveBeenCalledWith(2);
});

it("formats an invalid enum argument value instead of rethrowing it (#8123)", async () => {
class ArgInvalidOptionError extends Error {
oclif = { exit: 2 };
parse = {};
showHelp = false;
}
runCommandMock.mockRejectedValue(
new ArgInvalidOptionError("Expected powershell to be one of: bash, zsh, fish"),
);
const errorLine = vi.fn();
const exit = vi.fn((code: number): never => {
throw new Error(`exit:${code}`);
});

await expect(
runOclifCommandById("completion", ["powershell"], {
rootDir: "/repo",
error: errorLine,
exit,
}),
).rejects.toThrow("exit:2");

expect(errorLine).toHaveBeenCalledWith(" Expected powershell to be one of: bash, zsh, fish");
expect(exit).toHaveBeenCalledWith(2);
});

it("recognizes a parse error by shape when its class name is unknown (#8123)", async () => {
class SomeLaterParserError extends Error {
oclif = { exit: 2 };
parse = {};
showHelp = true;
}
runCommandMock.mockRejectedValue(new SomeLaterParserError("A parser rule rejected the input"));
const errorLine = vi.fn();
const exit = vi.fn((code: number): never => {
throw new Error(`exit:${code}`);
});

await expect(
runOclifCommandById("list", ["--json"], { rootDir: "/repo", error: errorLine, exit }),
).rejects.toThrow("exit:2");

expect(errorLine).toHaveBeenCalledWith(" A parser rule rejected the input");
expect(exit).toHaveBeenCalledWith(2);
});

it("keeps rethrowing a command failure that carries an unrelated parse property (#8123)", async () => {
class CommandFailure extends Error {
oclif = { exit: 2 };
parse = { source: "sandboxes.json" };
}
const error = new CommandFailure("Could not read the sandbox registry");
runCommandMock.mockRejectedValue(error);
const errorLine = vi.fn();

await expect(
runOclifCommandById("list", [], { rootDir: "/repo", error: errorLine }),
).rejects.toBe(error);
expect(errorLine).not.toHaveBeenCalled();
});

it("treats oclif graceful ExitError(0) as silent success", async () => {
// Mirrors what `Command.exit(0)` and `--help` actually throw in oclif: an
// ExitError instance whose synthetic `EEXIT: 0` message must NOT leak to
Expand Down
9 changes: 9 additions & 0 deletions src/lib/cli/oclif-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@ function getOclifExitCode(error: unknown): number | null {
return typeof oclif?.exit === "number" ? oclif.exit : null;
}

function hasOclifParseErrorShape(error: unknown): boolean {
if (!error || typeof error !== "object") return false;
if (typeof getOclifExitCode(error) !== "number") return false;
return (
Object.hasOwn(error, "parse") && typeof (error as { showHelp?: unknown }).showHelp === "boolean"
);
}

function isOclifParseError(error: unknown): boolean {
const name =
error && typeof error === "object"
? (error as { constructor?: { name?: string } }).constructor?.name
: "";
const message = error instanceof Error ? error.message : "";
return (
hasOclifParseErrorShape(error) ||
name === "NonExistentFlagsError" ||
name === "RequiredArgsError" ||
name === "UnexpectedArgsError" ||
Expand Down
36 changes: 36 additions & 0 deletions test/cli/oclif-parse-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { PARSER_EXIT_CODE, run } from "./helpers";

function expectCleanParseFailure(out: string): void {
expect(out).not.toMatch(/^\s+at /m);
expect(out).not.toContain("Node.js v");
expect(out).not.toContain("@oclif/core/lib/parser");
expect(out).not.toContain("InvalidOptionError");
expect(out).not.toContain("showHelp:");
}

describe("oclif parse errors", () => {
it("reports an invalid enum flag value without a stack trace (#8123)", () => {
const r = run(
"inference set --provider compatible-endpoint --model gpt-4o-mini --reasoning-effort ultra --no-verify",
);

expect(r.code).toBe(PARSER_EXIT_CODE);
expect(r.out).toContain(
"Expected --reasoning-effort=ultra to be one of: low, medium, high, default",
);
expectCleanParseFailure(r.out);
Comment on lines +22 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the help hint in both CLI tests.

Issue #8123 requires the validation message and the help hint. These assertions pass if output omits the help hint.

Proposed test change
     expect(r.out).toContain(
       "Expected --reasoning-effort=ultra to be one of: low, medium, high, default",
     );
+    expect(r.out).toContain("See more help with --help");
     expectCleanParseFailure(r.out);
@@
     expect(r.code).toBe(PARSER_EXIT_CODE);
     expect(r.out).toContain("Expected powershell to be one of: bash, zsh, fish");
+    expect(r.out).toContain("See more help with --help");
     expectCleanParseFailure(r.out);

As per path instructions, “Prefer observable outcomes through the public boundary.”

Also applies to: 32-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/oclif-parse-errors.test.ts` around lines 22 - 26, Update both CLI
parse-error tests around the reasoning-effort validation assertions to
explicitly verify the required help hint is present in r.out, in addition to the
validation message and clean parse-failure checks. Use the observable command
output and preserve the existing exit-code assertions.

Source: Path instructions

});

it("reports an invalid enum argument value without a stack trace (#8123)", () => {
const r = run("completion powershell");

expect(r.code).toBe(PARSER_EXIT_CODE);
expect(r.out).toContain("Expected powershell to be one of: bash, zsh, fish");
expectCleanParseFailure(r.out);
});
});
Loading