From 18b4cee5fdc0a166a2eeb133816ed1be6f551275 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 13:34:09 +0900 Subject: [PATCH] fix(#795): skill_not_found and unsupported_skills_action now return non-null hints via fallback table --- ROADMAP.md | 2 + rust/crates/rusty-claude-cli/src/main.rs | 8 ++ .../tests/output_format_contract.rs | 81 +++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 6a8e9d1b..10e86612 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7755,3 +7755,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 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. 794. **`claw plugins install /nonexistent/path` returned `error_kind:"unknown"` + `hint:null`** — dogfooded 2026-05-27 on `57a57ef7`. The error message `"plugin source '/path' was not found"` had no classifier arm, falling to `"unknown"`. Fix: added `plugin_source_not_found` classifier arm (`message.contains("plugin source") && message.contains("was not found")`); added `"plugin_source_not_found"` → `"Check that the path or URL is correct..."` to `fallback_hint_for_error_kind`. Unit test assertion added to `test_classify_error_kind`; integration test `plugins_install_not_found_path_returns_typed_kind_794` added. 56 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori plugins install probe on `57a57ef7`, 2026-05-27. + +795. **`claw skills install /nonexistent` returned `skill_not_found + hint:null` and `claw skills uninstall x` returned `unsupported_skills_action + hint:null`** — dogfooded 2026-05-27 on `491f179a`. Both error kinds were missing from `fallback_hint_for_error_kind` table, so even though classify returned a typed kind, the hint field was always null. Fix: added `"skill_not_found"` → hint suggesting `claw skills list` / `claw skills install`; added `"unsupported_skills_action"` → hint listing supported actions. Integration test `skills_install_not_found_and_unsupported_action_have_hints_795` covers both paths. 57 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills lifecycle probe on `491f179a`, 2026-05-27. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 91bf0a41..a6510fa7 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -421,6 +421,14 @@ fn fallback_hint_for_error_kind(kind: &str) -> Option<&'static str> { "plugin_source_not_found" => Some( "Check that the path or URL is correct. Use a local directory or a valid registry id.", ), + // #795: skills install/show of a non-existing skill path or name + "skill_not_found" => Some( + "Run `claw skills list` to see available skills, or `claw skills install ` to install a new one.", + ), + // #795: unsupported action on skills (e.g. /skills uninstall) with no \n hint + "unsupported_skills_action" => Some( + "Supported: list, install , show , help. Run `claw skills help` for details.", + ), _ => None, } } 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 f0e79a3c..de9d4268 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -3201,3 +3201,84 @@ fn plugins_install_not_found_path_returns_typed_kind_794() { .expect("plugin_source_not_found must have non-null hint (#794)"); assert!(!h.is_empty(), "hint must be non-empty"); } + +#[test] +fn skills_install_not_found_and_unsupported_action_have_hints_795() { + // #795: `claw skills install /nonexistent` returned skill_not_found + hint:null, and + // `claw skills uninstall x` returned unsupported_skills_action + hint:null. Both error + // kinds were missing from fallback_hint_for_error_kind table. Fix: added both entries. + let root = unique_temp_dir("skills-install-795"); + fs::create_dir_all(&root).expect("temp dir"); + std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(&root) + .output() + .ok(); + + // skills install with nonexistent local path + let out1 = run_claw( + &root, + &[ + "--output-format", + "json", + "skills", + "install", + "/nonexistent-xyz-795", + ], + &[], + ); + assert!( + !out1.status.success(), + "skills install not-found must exit non-zero (#795)" + ); + 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("skills install not-found should emit JSON error"); + assert_eq!( + j1["error_kind"], "skill_not_found", + "skills install not-found should be skill_not_found, got {:?}", + j1["error_kind"] + ); + let h1 = j1["hint"] + .as_str() + .expect("skill_not_found must have non-null hint (#795)"); + assert!( + h1.contains("skills list") || h1.contains("skills install"), + "hint should reference skills commands, got: {h1:?}" + ); + + // skills uninstall (unsupported action) + let out2 = run_claw( + &root, + &[ + "--output-format", + "json", + "skills", + "uninstall", + "some-skill", + ], + &[], + ); + assert!( + !out2.status.success(), + "skills uninstall must exit non-zero (#795)" + ); + 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("skills uninstall should emit JSON error"); + assert_eq!( + j2["error_kind"], "unsupported_skills_action", + "skills uninstall should be unsupported_skills_action, got {:?}", + j2["error_kind"] + ); + let h2 = j2["hint"] + .as_str() + .expect("unsupported_skills_action must have non-null hint (#795)"); + assert!(!h2.is_empty(), "hint must be non-empty"); +}