diff --git a/ROADMAP.md b/ROADMAP.md index f6098f46..a5eb1800 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7751,3 +7751,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 791. **`claw config show ` and `claw config set ` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9968a27e`. The config arg parser emitted `"unexpected extra arguments after `claw config {}`: {}"` with no `\n` delimiter, so `split_error_hint` returned `None` and `fallback_hint_for_error_kind("unexpected_extra_args")` also returns `None`. Fix: appended `\nUsage: claw config [env|hooks|model|plugins|mcp|settings]` to the error format string. Integration test `config_extra_args_have_non_null_hint_791` covers both paths. 51 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori config-arg probe on `9968a27e`, 2026-05-27. 792. **`claw agents list --bogus-flag` and `claw skills list --bogus-flag` silently returned `status:"ok" count:0` instead of an error** — dogfooded 2026-05-27 on `93a159dc`. The `list ` arm in both handlers treated flag-shaped tokens (`--something`) as name substring filters. Since no agents/skills have `--bogus` in their name, result was empty success list — a false positive that masks typos and unknown flags. Fix: added flag-prefix guard at the top of both `list ` arms in `commands/src/lib.rs`; detected filter tokens starting with `-` return `unknown_option` + usage hint. Two new integration tests `agents_list_flag_shaped_filter_returns_unknown_option_792`, `skills_list_flag_shaped_filter_returns_unknown_option_792`. 53 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills list probe on `93a159dc`, 2026-05-27. + +793. **`claw plugins list --bogus-flag` silent empty success + `plugins uninstall ` had `hint:null`** — dogfooded 2026-05-27 on `abfa2e4c`. Two gaps: (1) `plugins list` filter branch in `print_plugins` treated `--bogus-flag` as an id substring filter, found no matches, returned `status:"ok"` empty list — same false-positive as #792 for agents/skills. (2) `plugins uninstall no-such` propagated `plugin_not_found` error via `?` with no `\n` delimiter; `plugin_not_found` was missing from `fallback_hint_for_error_kind` table. Fix: (1) added flag-prefix guard in `print_plugins` `is_list_action` branch (detects tokens starting with `-`, returns `unknown_option` + usage hint, exits 1); (2) added `"plugin_not_found"` → `"Run 'claw plugins list' to see installed plugins."` to fallback table. Two new tests `plugins_list_flag_shaped_filter_returns_unknown_option_793`, `plugins_uninstall_not_found_has_hint_793`. 55 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins lifecycle probe on `abfa2e4c`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4e216757..04773003 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -411,6 +411,9 @@ fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> { "session_path_is_directory" => Some( "--resume expects a .jsonl session file path, not a directory. Run `claw --output-format json /session list` to list managed sessions.", ), + // #793: plugins uninstall/enable/disable of non-existing plugin propagates through + // the ? operator with no \n delimiter, so split_error_hint returns None. + "plugin_not_found" => Some("Run `claw plugins list` to see installed plugins."), _ => None, } } @@ -6407,6 +6410,20 @@ impl LiveCli { } } else if is_list_action { if let Some(filter) = target { + // #793: flag-shaped tokens silently became substring filters on + // plugins list, returning empty success instead of an error. + if filter.starts_with('-') { + let obj = json!({ + "kind": "plugin", + "action": "list", + "status": "error", + "error_kind": "unknown_option", + "unexpected": filter, + "hint": "Usage: claw plugins list []\nFilters are id substrings, not flags.", + }); + println!("{}", serde_json::to_string_pretty(&obj)?); + std::process::exit(1); + } let needle = filter.to_lowercase(); payload .plugins 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 25caecd9..f14e8675 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -3063,3 +3063,96 @@ fn skills_list_flag_shaped_filter_returns_unknown_option_792() { "hint should reference correct usage (#792)" ); } + +#[test] +fn plugins_list_flag_shaped_filter_returns_unknown_option_793() { + // #793: `claw plugins list --bogus-flag` silently returned status:"ok" with empty + // plugins list instead of an error. The list filter branch in print_plugins treated + // "--bogus-flag" as an id substring filter and found no matches, producing a false-positive. + // Fix: added flag-prefix guard; filter tokens starting with "-" now return unknown_option. + let root = unique_temp_dir("plugins-list-flag-793"); + fs::create_dir_all(&root).expect("temp dir"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + let output = run_claw( + &root, + &[ + "--output-format", + "json", + "plugins", + "list", + "--unknown-flag", + ], + &[], + ); + assert!( + !output.status.success(), + "plugins list --unknown-flag must exit non-zero (#793)" + ); + let stdout = String::from_utf8_lossy(&output.stdout); + let j: serde_json::Value = serde_json::from_str(stdout.trim()) + .expect("plugins list flag-filter should emit valid JSON"); + assert_eq!( + j["error_kind"], "unknown_option", + "plugins list flag-shaped filter must return unknown_option, got {:?}", + j["error_kind"] + ); + assert_eq!(j["status"], "error"); + let h = j["hint"] + .as_str() + .expect("unknown_option must have hint (#793)"); + assert!( + h.contains("plugins list") || h.contains("filter"), + "hint should reference plugins list usage, got: {h:?}" + ); +} + +#[test] +fn plugins_uninstall_not_found_has_hint_793() { + // #793: `claw plugins uninstall no-such-plugin` returned plugin_not_found + hint:null. + // The error propagated from plugins_command_payload_for via ? with no \n delimiter; + // split_error_hint returned None and plugin_not_found wasn't in the fallback table. + // Fix: added "plugin_not_found" entry to fallback_hint_for_error_kind(). + let root = unique_temp_dir("plugins-uninstall-793"); + fs::create_dir_all(&root).expect("temp dir"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + let output = run_claw( + &root, + &[ + "--output-format", + "json", + "plugins", + "uninstall", + "no-such-xyz-793", + ], + &[], + ); + assert!( + !output.status.success(), + "plugins uninstall not-found must exit non-zero (#793)" + ); + // Error envelope goes to stderr (propagated via ? to main error handler) + let stderr = String::from_utf8_lossy(&output.stderr); + let j: serde_json::Value = stderr + .lines() + .find(|l| l.trim_start().starts_with('{')) + .and_then(|l| serde_json::from_str(l).ok()) + .expect("plugins uninstall not-found should emit JSON error envelope"); + assert_eq!(j["error_kind"], "plugin_not_found"); + let h = j["hint"] + .as_str() + .expect("plugin_not_found must have non-null hint (#793)"); + assert!( + h.contains("plugins list") || h.contains("claw plugins"), + "hint should reference plugins list, got: {h:?}" + ); +}