fix: /approve and /deny outside REPL emit interactive_only error_kind (#828)

/approve, /yes, /deny, /no (and /y, /n) are valid REPL-only slash
commands. Outside the REPL they were falling through to
format_unknown_direct_slash_command -> error_kind:unknown_slash_command.

Fix: intercept them in the SlashCommand::Unknown arm and emit
interactive_only: prefix so classify_error_kind returns the correct kind.

One new test: approve_deny_outside_repl_emits_interactive_only (covers
/approve, /yes, /deny, /no)

572 tests pass, 1 pre-existing worker_boot failure unrelated.
This commit is contained in:
YeonGyu-Kim
2026-05-29 16:36:54 +09:00
committed by GitHub
parent 9d05573f24
commit 187aebd74f
3 changed files with 42 additions and 1 deletions

View File

@@ -1739,7 +1739,20 @@ fn parse_direct_slash_cli_action(
}),
}
}
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
Ok(Some(SlashCommand::Unknown(name))) => {
// #828: /approve and /deny are valid REPL-only slash commands that
// are not SlashCommand enum variants (they require an active tool
// call in the REPL to be meaningful). Emit interactive_only so
// machine consumers see the correct error_kind instead of
// unknown_slash_command.
if matches!(name.as_str(), "approve" | "yes" | "y" | "deny" | "no" | "n") {
Err(format!(
"interactive_only: /{name} requires an active tool call in the REPL.\nStart `claw` and use /{name} to approve or deny a pending tool execution."
))
} else {
Err(format_unknown_direct_slash_command(&name))
}
}
Ok(Some(command)) => Err({
let _ = command;
format!(

View File

@@ -3993,3 +3993,25 @@ fn direct_unknown_slash_command_emits_typed_error_kind() {
"direct unknown slash JSON must have empty stderr (#827)"
);
}
// #828: /approve and /deny outside REPL must emit interactive_only, not unknown_slash_command
#[test]
fn approve_deny_outside_repl_emits_interactive_only() {
let root = unique_temp_dir("approve-deny-828");
std::fs::create_dir_all(&root).expect("create temp dir");
for cmd in &["/approve", "/yes", "/deny", "/no"] {
let output = run_claw(&root, &["--output-format", "json", cmd], &[]);
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())
.unwrap_or_else(|_| panic!("{cmd} must emit JSON (#828), got: {stdout:?}"));
assert_eq!(
j["error_kind"], "interactive_only",
"{cmd} outside REPL must emit interactive_only (#828): {j}"
);
assert!(
stderr.is_empty(),
"{cmd} JSON must have empty stderr (#828): {stderr:?}"
);
}
}