diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 739f06a6..057a973d 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -271,7 +271,9 @@ Run `claw --help` for usage." /// matching against the error messages produced throughout the CLI surface. fn classify_error_kind(message: &str) -> &'static str { // Check specific patterns first (more specific before generic) - if message.starts_with("command_not_found:") { + if message.starts_with("unknown_slash_command:") { + "unknown_slash_command" + } else if message.starts_with("command_not_found:") { "command_not_found" } else if message.contains("missing Anthropic credentials") { "missing_credentials" @@ -1764,7 +1766,10 @@ fn format_unknown_option(option: &str) -> String { } fn format_unknown_direct_slash_command(name: &str) -> String { - let mut message = format!("unknown slash command outside the REPL: /{name}"); + // #827: prefix with classifier-friendly token so classify_error_kind + // returns "unknown_slash_command" instead of the opaque fallback. + let mut message = + format!("unknown_slash_command: unknown slash command outside the REPL: /{name}"); if let Some(suggestions) = render_suggestion_line("Did you mean", &suggest_slash_commands(name)) { message.push('\n'); @@ -1779,7 +1784,9 @@ fn format_unknown_direct_slash_command(name: &str) -> String { } fn format_unknown_slash_command(name: &str) -> String { - let mut message = format!("Unknown slash command: /{name}"); + // #827: prefix with classifier-friendly token so classify_error_kind + // can return "unknown_slash_command" instead of the opaque fallback. + let mut message = format!("unknown_slash_command: Unknown slash command: /{name}"); if let Some(suggestions) = render_suggestion_line("Did you mean", &suggest_slash_commands(name)) { message.push('\n'); 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 6dd72909..8d7ff7b1 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -3967,3 +3967,29 @@ fn multi_word_unknown_subcommand_falls_through_to_prompt_826() { "multi-word fallthrough JSON must have empty stderr: {stderr:?}" ); } + +// #827: direct /unknown-slash-command must emit typed error_kind, not "unknown" +// Uses the direct-slash CLI path (no session load needed; reproducible on CI). +#[test] +fn direct_unknown_slash_command_emits_typed_error_kind() { + let root = unique_temp_dir("direct-unknown-slash-827"); + std::fs::create_dir_all(&root).expect("create temp dir"); + let output = run_claw(&root, &["--output-format", "json", "/boguscommand"], &[]); + assert_eq!(output.status.code(), Some(1), "unknown slash should exit 1"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let j: serde_json::Value = + serde_json::from_str(stdout.trim()).expect("unknown slash must emit JSON (#827)"); + assert_ne!( + j["error_kind"], "unknown", + "direct unknown slash must not emit opaque \'unknown\' error_kind (#827): {j}" + ); + assert_eq!( + j["error_kind"], "unknown_slash_command", + "direct unknown slash must emit unknown_slash_command (#827): {j}" + ); + assert!( + stderr.is_empty(), + "direct unknown slash JSON must have empty stderr (#827)" + ); +}