From 22b423b651430e447b9b7fbc8b22c090bb204e6e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 08:39:11 +0900 Subject: [PATCH] fix(#786): dump-manifests --manifests-dir missing-value errors now return typed missing_flag_value kind + non-null hint --- ROADMAP.md | 2 + rust/crates/rusty-claude-cli/src/main.rs | 5 +- .../tests/output_format_contract.rs | 75 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4ed1f43b..42c858da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7737,3 +7737,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 784. **`claw export` had two opaque arg-error paths returning `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `81fe0ccb` (pinpoint by Gaebal-gajae). `claw export --output` (missing flag value) emitted plain `"missing value for --output"` with no typed prefix; `claw export a.md b.md` (extra positional) emitted plain `"unexpected export argument: second.md"`. Both classified as `unknown+null`. Fix: (1) `--output` missing-value error now uses `missing_flag_value:` prefix + `\n` usage hint; (2) extra positional now uses `unexpected_extra_args:` prefix + `\n` usage hint; (3) classifier `unexpected_extra_args` arm extended to match both `starts_with("unexpected extra arguments")` (prose form, #766) and `starts_with("unexpected_extra_args:")` (typed prefix form, #784). Integration test `export_arg_errors_have_typed_kind_and_hint_784` covers both paths. 43 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `81fe0ccb`, 2026-05-27. 785. **`claw dump` (typo/near-miss for dump-manifests) returned `error_kind:"unknown"` — no classifier arm for `"unknown subcommand:"` prose prefix** — dogfooded 2026-05-27 on `e628b4bb`. Any unknown top-level subcommand that triggers the suggestion path emitted `"unknown subcommand: .\nDid you mean "` but `classify_error_kind` had no arm for that prefix; all fell to the `"unknown"` catch-all. The hint was non-null (the suggestion text was extracted by `split_error_hint`) but `error_kind` was undifferentiated. Fix: added `starts_with("unknown subcommand:")` → `"unknown_subcommand"` arm. Unit test assertion + integration test `unknown_subcommand_returns_typed_kind_785` using `claw dump` as the trigger. 44 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori subcommand-classifier probe on `e628b4bb`, 2026-05-27. + +786. **`claw dump-manifests --manifests-dir` (missing value) and `--manifests-dir=` (empty) both returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `87f43347` (pinpoint by Gaebal-gajae). Both missing `--manifests-dir` branches in `parse_dump_manifests_args` emitted plain `"--manifests-dir requires a path"` with no typed prefix; `classify_error_kind` had no matching arm so they fell to `"unknown"`. Fix: both branches now use `missing_flag_value:` prefix + `\n` usage hint. Integration test `dump_manifests_missing_dir_has_typed_kind_and_hint_786` covers both cases. 45 CLI contract tests pass. [SCOPE: claw-code] Source: Gaebal-gajae pinpoint + Jobdori implementation on `87f43347`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 593e43d4..72563baf 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -2159,14 +2159,15 @@ fn parse_dump_manifests_args( if arg == "--manifests-dir" { let value = args .get(index + 1) - .ok_or_else(|| String::from("--manifests-dir requires a path"))?; + .ok_or_else(|| String::from("missing_flag_value: --manifests-dir requires a path.\nUsage: claw dump-manifests --manifests-dir [--output-format json]"))?; manifests_dir = Some(PathBuf::from(value)); index += 2; continue; } if let Some(value) = arg.strip_prefix("--manifests-dir=") { if value.is_empty() { - return Err(String::from("--manifests-dir requires a path")); + // #786: empty --manifests-dir= is also a missing value + return Err(String::from("missing_flag_value: --manifests-dir requires a path.\nUsage: claw dump-manifests --manifests-dir [--output-format json]")); } manifests_dir = Some(PathBuf::from(value)); index += 1; 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 ec8529f1..417aa09d 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -2537,3 +2537,78 @@ fn unknown_subcommand_returns_typed_kind_785() { "hint should reference the suggested subcommand or help, got: {hint:?}" ); } + +#[test] +fn dump_manifests_missing_dir_has_typed_kind_and_hint_786() { + // #786: `claw dump-manifests --manifests-dir` (no value) and `--manifests-dir=` (empty) + // both emitted plain "--manifests-dir requires a path" with error_kind:"unknown" + hint:null. + // Fix: use missing_flag_value: prefix + \n usage hint. + let root = unique_temp_dir("dump-manifests-missing-dir-786"); + fs::create_dir_all(&root).expect("temp dir"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + // Case 1: --manifests-dir with no following value (next arg is --output-format) + let out1 = run_claw( + &root, + &[ + "--output-format", + "json", + "dump-manifests", + "--manifests-dir", + "--output-format", + "json", + ], + &[], + ); + assert!(!out1.status.success()); + let stderr1 = String::from_utf8_lossy(&out1.stderr); + let j1: serde_json::Value = stderr1 + .lines() + .find(|l| l.trim_start().starts_with('{')) + .and_then(|l| serde_json::from_str(l).ok()) + .expect("missing --manifests-dir value should emit JSON error"); + assert_eq!( + j1["error_kind"], "missing_flag_value", + "missing --manifests-dir value should be missing_flag_value, got {:?}", + j1["error_kind"] + ); + let h1 = j1["hint"] + .as_str() + .expect("missing_flag_value must have hint (#786)"); + assert!( + h1.contains("dump-manifests") || h1.contains("manifests-dir"), + "hint should reference dump-manifests usage, got: {h1:?}" + ); + + // Case 2: --manifests-dir= with empty value + let out2 = run_claw( + &root, + &[ + "--output-format", + "json", + "dump-manifests", + "--manifests-dir=", + ], + &[], + ); + assert!(!out2.status.success()); + let stderr2 = String::from_utf8_lossy(&out2.stderr); + let j2: serde_json::Value = stderr2 + .lines() + .find(|l| l.trim_start().starts_with('{')) + .and_then(|l| serde_json::from_str(l).ok()) + .expect("empty --manifests-dir= should emit JSON error"); + assert_eq!( + j2["error_kind"], "missing_flag_value", + "empty --manifests-dir= should be missing_flag_value, got {:?}", + j2["error_kind"] + ); + let h2 = j2["hint"] + .as_str() + .expect("missing_flag_value must have hint (#786)"); + assert!(!h2.is_empty(), "hint must not be empty"); +}