From 89735dbd3320668265a2b43efcc71f8b84f94984 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 01:33:24 +0900 Subject: [PATCH] fix(#766): claw diff extra args now classified as unexpected_extra_args with hint; track #767 session subcommand gap --- ROADMAP.md | 4 ++ rust/crates/rusty-claude-cli/src/main.rs | 12 +++++- .../tests/output_format_contract.rs | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 48bcd148..e3096613 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7697,3 +7697,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 764. **`config_parse_error` returned `hint: null` despite #763 adding the classifier** — dogfooded 2026-05-27 on `c86dc73d`. #763 fixed `error_kind` classification but `hint` remained `null` because `ConfigError::Parse` Display impl emitted only the bare serde_json error string (no `\n` delimiter). `split_error_hint()` found nothing to split. Fix: updated `Display for ConfigError::Parse` in `runtime/src/config.rs` to append `\nFix: open the file shown above and correct the JSON syntax, then retry.`. Integration test `config_parse_error_has_typed_error_kind_and_hint_764` added to `output_format_contract.rs` asserting non-zero exit + `error_kind:config_parse_error` + non-empty hint. 31 contract tests pass. Source: Jobdori follow-up probe on `c86dc73d`, 2026-05-27. 765. **`claw login`/`claw logout` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `4ea255ca` (gaebal-gajae pinpoint against `88ce1810`, revised ID after #763/#764 landed). `removed_auth_surface_error()` emitted single-line string with no `\n` delimiter; `split_error_hint()` couldn't extract hint, and no `removed_subcommand` classifier arm existed. Fix: (1) `removed_auth_surface_error()` now emits two-line format (`has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.`); (2) `classify_error_kind()` arm added matching `has been removed.`; (3) unit test assertions and integration test `login_logout_removed_subcommands_have_error_kind_and_hint_765` added verifying both `error_kind:removed_subcommand` and non-null hint mentioning the env var migration. 32 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae + Jobdori probe on `4ea255ca`, 2026-05-27. + +766. **`claw diff ` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `d29a8e21`. `claw diff --bogus --output-format json` emitted bare error string `"unexpected extra arguments after \`claw diff\`: --bogus"` with no `\n` delimiter and no classifier arm. Fix: (1) added `\nUsage: claw diff` to the error format string; (2) added `unexpected_extra_args` classifier arm matching `starts_with("unexpected extra arguments")`; (3) unit test assertion + integration test `diff_extra_args_have_typed_error_kind_and_hint_766` added. 33 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori probe on `d29a8e21`, 2026-05-27. + +767. **`claw session bogus --output-format json` ignores JSON flag and falls through to credential check** — dogfooded 2026-05-27 on `d29a8e21`. `claw --output-format json session bogus` dispatches to the full interactive REPL runtime instead of rejecting `bogus` as an unknown session subcommand. Output is `error_kind:"missing_credentials"` rather than `error_kind:"unknown_session_subcommand"`. Root cause: `session` arg parser has no unknown-subcommand guard before dispatch; `bogus` is silently accepted as a session ID / switch target and reaches the credential-check gate. Fix needed: validate known session subcommands (`list`, `exists`, `switch`, `fork`, `delete`) before dispatch, return structured `unknown_session_subcommand` error for unrecognized tokens. [SCOPE: claw-code] Source: Jobdori probe on `d29a8e21`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index d34b32e4..8c1785a0 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -328,6 +328,9 @@ fn classify_error_kind(message: &str) -> &'static str { } else if message.contains("has been removed.") { // #765: removed subcommands (login, logout) — hint contains migration guidance "removed_subcommand" + } else if message.starts_with("unexpected extra arguments") { + // #766: extra positionals after commands that take no arguments (e.g. claw diff) + "unexpected_extra_args" } else if message.starts_with("unknown_option:") { "unknown_option" } else if message.contains("is a slash command") @@ -1145,7 +1148,7 @@ fn parse_args(args: &[String]) -> Result { "diff" => { if rest.len() > 1 { return Err(format!( - "unexpected extra arguments after `claw diff`: {}", + "unexpected extra arguments after `claw diff`: {}\nUsage: claw diff", rest[1..].join(" ") )); } @@ -12941,6 +12944,13 @@ mod tests { ), "removed_subcommand" ); + // #766: unexpected extra arguments must classify as unexpected_extra_args + assert_eq!( + classify_error_kind( + "unexpected extra arguments after `claw diff`: --bogus\nUsage: claw diff" + ), + "unexpected_extra_args" + ); assert_eq!( classify_error_kind( "`claw logout` has been removed.\nSet ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead." diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index bc4dd11c..c1f12deb 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -1903,3 +1903,40 @@ fn login_logout_removed_subcommands_have_error_kind_and_hint_765() { ); } } + +#[test] +fn diff_extra_args_have_typed_error_kind_and_hint_766() { + // #766: `claw diff --bogus` returned error_kind:"unknown" + hint:null. + // `diff` takes no arguments; extra args were unclassified with no remediation. + let root = unique_temp_dir("diff-extra-args-766"); + fs::create_dir_all(&root).expect("temp dir should exist"); + // Need a git repo for diff to parse past arg validation + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + let output = run_claw(&root, &["--output-format", "json", "diff", "--bogus"], &[]); + assert!( + !output.status.success(), + "claw diff --bogus should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let json_line = stderr + .lines() + .find(|l| l.trim_start().starts_with('{')) + .expect("stderr should contain a JSON error envelope"); + let parsed: serde_json::Value = + serde_json::from_str(json_line).expect("error envelope should be valid JSON"); + + assert_eq!( + parsed["error_kind"], "unexpected_extra_args", + "claw diff --bogus must return error_kind:unexpected_extra_args (#766)" + ); + let hint = parsed["hint"].as_str().unwrap_or(""); + assert!( + !hint.is_empty(), + "claw diff --bogus must return non-null hint (#766), got: {hint:?}" + ); +}