Skip to content

Commit 4df1461

Browse files
committed
fix+test(#756): missing/invalid flag-value errors now emit typed error_kind and non-null hint
1 parent 0e8a449 commit 4df1461

3 files changed

Lines changed: 77 additions & 7 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7677,3 +7677,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
76777677
754. **`missing_credentials` JSON envelope always had `hint: null` even when a contextual hint was available** — dogfooded 2026-05-26 on `e9327135`. `ApiError::Display` for `MissingCredentials` appended the hint via ` — hint: {hint}` (inline, no `\n`), so `split_error_hint()` could not extract it and left the JSON `hint` field null. Fix: change delimiter from ` — hint: ` to `\n` in `api/src/error.rs` Display impl; update two tests in `api/src/error.rs` and `api/src/providers/mod.rs` to assert newline separator. Source: Jobdori dogfood on `e9327135`, 2026-05-26.
76787678

76797679
755. **`claw -p hello --model sonnet` swallowed `--model sonnet` into the prompt string** — gaebal-gajae pinpoint on `e9327135` (#117 revival). `-p` used `args[index+1..].join(" ")`, consuming all remaining tokens as prompt. Fix: capture exactly one token via `args.get(index+1)`, reject flag-like tokens (`starts_with('-')`) as `missing_prompt`, support `--` sentinel for literal flag-text, then `continue` the flag loop so `--model`/`--output-format`/etc. parse normally. Dispatch via `short_p_prompt` after full flag scan. Regression guard: `short_p_flag_swallows_no_flags_755` asserts `--output-format json` is parsed (not swallowed) and `--model` as prompt-arg is rejected. Source: gaebal-gajae dogfood on `e9327135`, 2026-05-26.
7680+
7681+
756. **`--reasoning-effort bogus`, `--model` (no value), and sibling missing/invalid flag-value errors all returned `error_kind:"unknown"` + `hint:null`** — gaebal-gajae pinpoint on `0e8a449e`. All `missing value for --X` and `invalid value for --reasoning-effort` error strings were single-line with no classifier arm. Fix: (a) prefix all with `missing_flag_value:` / `invalid_flag_value:` + `\n` usage hint; (b) add `message.starts_with("missing_flag_value:")` → `"missing_flag_value"` and `message.starts_with("invalid_flag_value:")` → `"invalid_flag_value"` classifier arms. Covers `--model`, `--output-format`, `--permission-mode`, `--base-commit`, `--reasoning-effort`. Regression guard: `flag_value_errors_have_error_kind_and_hint_756` — invalid `--reasoning-effort HIGH` → `invalid_flag_value` + hint with valid values; missing `--model` → `missing_flag_value` + non-null hint. Source: gaebal-gajae dogfood on `0e8a449e`, 2026-05-26.

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ fn classify_error_kind(message: &str) -> &'static str {
286286
"unsupported_skills_action"
287287
} else if message.contains("unrecognized argument") || message.contains("unknown option") {
288288
"cli_parse"
289+
} else if message.starts_with("missing_flag_value:") {
290+
"missing_flag_value"
291+
} else if message.starts_with("invalid_flag_value:") {
292+
"invalid_flag_value"
289293
} else if message.contains("invalid model syntax") {
290294
"invalid_model_syntax"
291295
} else if message.contains("is not yet implemented") {
@@ -776,7 +780,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
776780
"--model" => {
777781
let value = args
778782
.get(index + 1)
779-
.ok_or_else(|| "missing value for --model".to_string())?;
783+
.ok_or_else(|| "missing_flag_value: missing value for --model.\nUsage: --model <provider/model> e.g. --model anthropic/claude-opus-4-7".to_string())?;
780784
let resolved = resolve_model_alias_with_config(value);
781785
debug!("Resolved --model '{}' -> '{}'", value, resolved);
782786
validate_model_syntax(&resolved)?;
@@ -796,14 +800,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
796800
"--output-format" => {
797801
let value = args
798802
.get(index + 1)
799-
.ok_or_else(|| "missing value for --output-format".to_string())?;
803+
.ok_or_else(|| "missing_flag_value: missing value for --output-format.\nUsage: --output-format text or --output-format json".to_string())?;
800804
output_format = CliOutputFormat::parse(value)?;
801805
index += 2;
802806
}
803807
"--permission-mode" => {
804808
let value = args
805809
.get(index + 1)
806-
.ok_or_else(|| "missing value for --permission-mode".to_string())?;
810+
.ok_or_else(|| "missing_flag_value: missing value for --permission-mode.\nUsage: --permission-mode default|acceptEdits|bypassPermissions|dangerFullAccess".to_string())?;
807811
permission_mode_override = Some(parse_permission_mode_arg(value)?);
808812
index += 2;
809813
}
@@ -826,7 +830,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
826830
"--base-commit" => {
827831
let value = args
828832
.get(index + 1)
829-
.ok_or_else(|| "missing value for --base-commit".to_string())?;
833+
.ok_or_else(|| "missing_flag_value: missing value for --base-commit.\nUsage: --base-commit <git-sha>".to_string())?;
830834
base_commit = Some(value.clone());
831835
index += 2;
832836
}
@@ -837,10 +841,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
837841
"--reasoning-effort" => {
838842
let value = args
839843
.get(index + 1)
840-
.ok_or_else(|| "missing value for --reasoning-effort".to_string())?;
844+
.ok_or_else(|| "missing_flag_value: missing value for --reasoning-effort.\nUsage: --reasoning-effort low|medium|high".to_string())?;
841845
if !matches!(value.as_str(), "low" | "medium" | "high") {
842846
return Err(format!(
843-
"invalid value for --reasoning-effort: '{value}'; must be low, medium, or high"
847+
"invalid_flag_value: invalid value for --reasoning-effort: '{value}'.\nUsage: --reasoning-effort low|medium|high"
844848
));
845849
}
846850
reasoning_effort = Some(value.clone());
@@ -850,7 +854,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
850854
let value = &flag[19..];
851855
if !matches!(value, "low" | "medium" | "high") {
852856
return Err(format!(
853-
"invalid value for --reasoning-effort: '{value}'; must be low, medium, or high"
857+
"invalid_flag_value: invalid value for --reasoning-effort: '{value}'.\nUsage: --reasoning-effort low|medium|high"
854858
));
855859
}
856860
reasoning_effort = Some(value.to_string());

rust/crates/rusty-claude-cli/tests/output_format_contract.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,70 @@ fn prompt_no_arg_json_error_kind_750() {
15041504
);
15051505
}
15061506

1507+
#[test]
1508+
fn flag_value_errors_have_error_kind_and_hint_756() {
1509+
// #756: missing/invalid flag-value errors must emit typed error_kind + non-null hint.
1510+
// Before #756: all returned error_kind:"unknown" + hint:null.
1511+
use std::process::Command;
1512+
let root = unique_temp_dir("flag-value-errors");
1513+
fs::create_dir_all(&root).expect("temp dir");
1514+
let bin = env!("CARGO_BIN_EXE_claw");
1515+
1516+
// Case 1: --reasoning-effort with invalid value
1517+
let out = Command::new(bin)
1518+
.current_dir(&root)
1519+
.args(["--output-format", "json", "--reasoning-effort", "HIGH"])
1520+
.output()
1521+
.expect("claw --reasoning-effort HIGH should run");
1522+
assert!(
1523+
!out.status.success(),
1524+
"invalid reasoning-effort must exit non-zero"
1525+
);
1526+
let raw = String::from_utf8_lossy(&out.stderr)
1527+
.lines()
1528+
.filter(|l| l.starts_with('{'))
1529+
.collect::<Vec<_>>()
1530+
.join("");
1531+
let parsed: serde_json::Value = serde_json::from_str(&raw)
1532+
.unwrap_or_else(|_| panic!("invalid --reasoning-effort must emit JSON; got: {raw}"));
1533+
assert_eq!(
1534+
parsed["error_kind"], "invalid_flag_value",
1535+
"invalid --reasoning-effort must be invalid_flag_value (#756): {parsed}"
1536+
);
1537+
assert!(
1538+
parsed["hint"].as_str().map_or(false, |h| h.contains("low")
1539+
|| h.contains("medium")
1540+
|| h.contains("high")),
1541+
"hint must mention valid values (#756): {parsed}"
1542+
);
1543+
1544+
// Case 2: --model flag with missing value (trailing flag)
1545+
let out2 = Command::new(bin)
1546+
.current_dir(&root)
1547+
.args(["--output-format", "json", "--model"])
1548+
.output()
1549+
.expect("claw --model (no value) should run");
1550+
assert!(
1551+
!out2.status.success(),
1552+
"missing --model value must exit non-zero"
1553+
);
1554+
let raw2 = String::from_utf8_lossy(&out2.stderr)
1555+
.lines()
1556+
.filter(|l| l.starts_with('{'))
1557+
.collect::<Vec<_>>()
1558+
.join("");
1559+
let parsed2: serde_json::Value = serde_json::from_str(&raw2)
1560+
.unwrap_or_else(|_| panic!("missing --model value must emit JSON; got: {raw2}"));
1561+
assert_eq!(
1562+
parsed2["error_kind"], "missing_flag_value",
1563+
"missing --model value must be missing_flag_value (#756): {parsed2}"
1564+
);
1565+
assert!(
1566+
parsed2["hint"].as_str().map_or(false, |h| !h.is_empty()),
1567+
"missing --model hint must be non-empty (#756): {parsed2}"
1568+
);
1569+
}
1570+
15071571
#[test]
15081572
fn short_p_flag_swallows_no_flags_755() {
15091573
// #755: `claw -p hello --output-format json` must parse --output-format json

0 commit comments

Comments
 (0)