fix: unknown slash command emits unknown_slash_command error_kind (#827)

Both direct-slash CLI path (claw /boguscommand) and resume slash path
(claw --resume session /boguscommand) previously emitted error_kind:unknown
(opaque fallback). Machine consumers could not distinguish unrecognized
slash commands from other error classes.

Fix:
- format_unknown_direct_slash_command: prefix with 'unknown_slash_command:'
- format_unknown_slash_command (resume path): prefix with 'unknown_slash_command:'
- Add classifier arm for 'unknown_slash_command:' prefix

One new regression test: direct_unknown_slash_command_emits_typed_error_kind
Uses the direct-slash CLI path (no session load needed; reproducible on CI).

572 tests pass, 1 pre-existing worker_boot failure unrelated.
This commit is contained in:
YeonGyu-Kim 2026-05-29 16:00:37 +09:00 committed by GitHub
parent 58902915f6
commit 9d05573f24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 36 additions and 3 deletions

View File

@ -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');

View File

@ -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)"
);
}