fix(#770): cost/clear/memory/ultraplan/model with args now return interactive_only instead of falling to credential check

This commit is contained in:
YeonGyu-Kim
2026-05-27 02:10:41 +09:00
parent 9e1be05634
commit 3a1d88386c
3 changed files with 81 additions and 0 deletions

View File

@@ -1175,6 +1175,28 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
"interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session <action>` or start `claw` and run `/session [list|exists|switch|fork|delete]`."
))
}
// #770: same fallthrough gap as #767 — these slash commands had no multi-arg match arm
// and fell to CliAction::Prompt reaching the credential gate when called with args.
"cost" => Err(
"interactive_only: `claw cost` is a slash command.\nUse `claw --resume SESSION.jsonl /cost` or start `claw` and run `/cost`."
.to_string(),
),
"clear" => Err(
"interactive_only: `claw clear` is a slash command.\nUse `claw --resume SESSION.jsonl /clear [--confirm]` or start `claw` and run `/clear`."
.to_string(),
),
"memory" => Err(
"interactive_only: `claw memory` is a slash command.\nStart `claw` and run `/memory` inside the REPL."
.to_string(),
),
"ultraplan" => Err(
"interactive_only: `claw ultraplan` is a slash command.\nStart `claw` and run `/ultraplan` inside the REPL."
.to_string(),
),
"model" if rest.len() > 1 => Err(
"interactive_only: `claw model` is a slash command.\nStart `claw` and run `/model [model-name]` inside the REPL."
.to_string(),
),
"skills" => {
let args = join_optional_args(&rest[1..]);
if let Some(action) = args.as_deref() {

View File

@@ -2019,3 +2019,60 @@ fn session_with_unknown_subcommand_returns_interactive_only_not_credentials_767(
);
}
}
#[test]
fn slash_only_verbs_with_args_return_interactive_only_not_credentials_770() {
// #770: `claw cost breakdown`, `claw clear --force`, `claw memory reset`,
// `claw ultraplan bogus`, `claw model opus extra` all fell through to
// CliAction::Prompt and reached the credential gate, returning
// error_kind:"missing_credentials". These are all slash-only commands;
// any multi-token invocation should return interactive_only guidance.
let root = unique_temp_dir("slash-verbs-770");
fs::create_dir_all(&root).expect("temp dir should exist");
let cases: &[&[&str]] = &[
&["cost", "breakdown"],
&["clear", "--force"],
&["memory", "reset"],
&["ultraplan", "bogus"],
&["model", "opus", "extra"],
];
for args in cases {
let full_args: Vec<&str> = std::iter::once("--output-format")
.chain(std::iter::once("json"))
.chain(args.iter().copied())
.collect();
let output = run_claw(&root, &full_args, &[]);
assert!(
!output.status.success(),
"claw {} should exit non-zero",
args.join(" ")
);
let stderr = String::from_utf8_lossy(&output.stderr);
let json_line = stderr
.lines()
.find(|l| l.trim_start().starts_with('{'))
.unwrap_or_else(|| {
panic!(
"claw {} stderr should contain JSON, got: {stderr}",
args.join(" ")
)
});
let parsed: serde_json::Value =
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
assert_eq!(
parsed["error_kind"],
"interactive_only",
"claw {} must return error_kind:interactive_only (#770), not missing_credentials",
args.join(" ")
);
let hint = parsed["hint"].as_str().unwrap_or("");
assert!(
!hint.is_empty(),
"claw {} must return non-null hint (#770)",
args.join(" ")
);
}
}