merge fix/p2-19-subcommand-help-fallthrough

This commit is contained in:
Yeachan-Heo
2026-04-05 16:54:59 +00:00
2 changed files with 89 additions and 129 deletions

View File

@@ -2142,13 +2142,22 @@ pub fn handle_plugins_slash_command(
}
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
if let Some(args) = normalize_optional_args(args) {
if let Some(help_path) = help_path_from_args(args) {
return Ok(match help_path.as_slice() {
[] => render_agents_usage(None),
_ => render_agents_usage(Some(&help_path.join(" "))),
});
}
}
match normalize_optional_args(args) {
None | Some("list") => {
let roots = discover_definition_roots(cwd, "agents");
let agents = load_agents_from_roots(&roots)?;
Ok(render_agents_report(&agents))
}
Some("-h" | "--help" | "help") => Ok(render_agents_usage(None)),
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
Some(args) => Ok(render_agents_usage(Some(args))),
}
}
@@ -2162,6 +2171,16 @@ pub fn handle_mcp_slash_command(
}
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
if let Some(args) = normalize_optional_args(args) {
if let Some(help_path) = help_path_from_args(args) {
return Ok(match help_path.as_slice() {
[] => render_skills_usage(None),
["install", ..] => render_skills_usage(Some("install")),
_ => render_skills_usage(Some(&help_path.join(" "))),
});
}
}
match normalize_optional_args(args) {
None | Some("list") => {
let roots = discover_skill_roots(cwd);
@@ -2177,7 +2196,7 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
let install = install_skill(target, cwd)?;
Ok(render_skill_install_report(&install))
}
Some("-h" | "--help" | "help") => Ok(render_skills_usage(None)),
Some(args) if is_help_arg(args) => Ok(render_skills_usage(None)),
Some(args) => Ok(render_skills_usage(Some(args))),
}
}
@@ -2187,6 +2206,16 @@ fn render_mcp_report_for(
cwd: &Path,
args: Option<&str>,
) -> Result<String, runtime::ConfigError> {
if let Some(args) = normalize_optional_args(args) {
if let Some(help_path) = help_path_from_args(args) {
return Ok(match help_path.as_slice() {
[] => render_mcp_usage(None),
["show", ..] => render_mcp_usage(Some("show")),
_ => render_mcp_usage(Some(&help_path.join(" "))),
});
}
}
match normalize_optional_args(args) {
None | Some("list") => {
let runtime_config = loader.load()?;
@@ -2195,7 +2224,7 @@ fn render_mcp_report_for(
runtime_config.mcp().servers(),
))
}
Some("-h" | "--help" | "help") => Ok(render_mcp_usage(None)),
Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
Some("show") => Ok(render_mcp_usage(Some("show"))),
Some(args) if args.split_whitespace().next() == Some("show") => {
let mut parts = args.split_whitespace();
@@ -3036,6 +3065,16 @@ fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
args.map(str::trim).filter(|value| !value.is_empty())
}
fn is_help_arg(arg: &str) -> bool {
matches!(arg, "help" | "-h" | "--help")
}
fn help_path_from_args(args: &str) -> Option<Vec<&str>> {
let parts = args.split_whitespace().collect::<Vec<_>>();
let help_index = parts.iter().position(|part| is_help_arg(part))?;
Some(parts[..help_index].to_vec())
}
fn render_agents_usage(unexpected: Option<&str>) -> String {
let mut lines = vec![
"Agents".to_string(),
@@ -4005,7 +4044,17 @@ mod tests {
let skills_unexpected =
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
assert!(skills_unexpected.contains("Unexpected show help"));
assert!(skills_unexpected.contains("Unexpected show"));
let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
.expect("nested skills help");
assert!(skills_install_help.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_install_help.contains("Unexpected install"));
let skills_unknown_help =
super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
assert!(skills_unknown_help.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_unknown_help.contains("Unexpected show"));
let _ = fs::remove_dir_all(cwd);
}
@@ -4022,6 +4071,16 @@ mod tests {
super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
assert!(unexpected.contains("Unexpected show alpha beta"));
let nested_help =
super::handle_mcp_slash_command(Some("show --help"), &cwd).expect("mcp help");
assert!(nested_help.contains("Usage /mcp [list|show <server>|help]"));
assert!(nested_help.contains("Unexpected show"));
let unknown_help =
super::handle_mcp_slash_command(Some("inspect --help"), &cwd).expect("mcp usage");
assert!(unknown_help.contains("Usage /mcp [list|show <server>|help]"));
assert!(unknown_help.contains("Unexpected inspect"));
let _ = fs::remove_dir_all(cwd);
}

View File

@@ -5,7 +5,6 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use runtime::Session;
use serde_json::Value;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -38,64 +37,6 @@ fn status_command_applies_model_and_permission_mode_flags() {
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn status_command_emits_structured_json_when_requested() {
// given
let temp_dir = unique_temp_dir("status-json");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
// when
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(&temp_dir)
.args([
"--model",
"sonnet",
"--permission-mode",
"read-only",
"--output-format",
"json",
"status",
])
.output()
.expect("claw should launch");
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
let parsed: Value = serde_json::from_str(stdout.trim()).expect("status output should be json");
assert_eq!(parsed["kind"], "status");
assert_eq!(parsed["model"], "claude-sonnet-4-6");
assert_eq!(parsed["permission_mode"], "read-only");
assert_eq!(parsed["workspace"]["session"], "live-repl");
assert!(parsed["sandbox"].is_object());
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn sandbox_command_emits_structured_json_when_requested() {
// given
let temp_dir = unique_temp_dir("sandbox-json");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
// when
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(&temp_dir)
.args(["--output-format", "json", "sandbox"])
.output()
.expect("claw should launch");
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
let parsed: Value = serde_json::from_str(stdout.trim()).expect("sandbox output should be json");
assert_eq!(parsed["kind"], "sandbox");
assert!(parsed["sandbox"].is_object());
assert!(parsed["sandbox"]["requested"].is_object());
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
// given
@@ -220,77 +161,37 @@ fn config_command_loads_defaults_from_standard_config_locations() {
}
#[test]
fn doctor_command_runs_as_a_local_shell_entrypoint() {
// given
let temp_dir = unique_temp_dir("doctor-entrypoint");
let config_home = temp_dir.join("home").join(".claw");
fs::create_dir_all(&config_home).expect("config home should exist");
fn nested_help_flags_render_usage_instead_of_falling_through() {
let temp_dir = unique_temp_dir("nested-help");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
// when
let output = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
.arg("doctor")
let mcp_output = command_in(&temp_dir)
.args(["mcp", "show", "--help"])
.output()
.expect("claw doctor should launch");
.expect("claw should launch");
assert_success(&mcp_output);
let mcp_stdout = String::from_utf8(mcp_output.stdout).expect("stdout should be utf8");
assert!(mcp_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(mcp_stdout.contains("Unexpected show"));
assert!(!mcp_stdout.contains("server `--help` is not configured"));
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
assert!(stdout.contains("Doctor"));
assert!(stdout.contains("Auth"));
assert!(stdout.contains("Config"));
assert!(stdout.contains("Workspace"));
assert!(stdout.contains("Sandbox"));
assert!(!stdout.contains("Thinking"));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn local_subcommand_help_does_not_fall_through_to_runtime_or_provider_calls() {
// given
let temp_dir = unique_temp_dir("subcommand-help");
let config_home = temp_dir.join("home").join(".claw");
fs::create_dir_all(&config_home).expect("config home should exist");
// when
let doctor_help = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
.args(["doctor", "--help"])
let skills_output = command_in(&temp_dir)
.args(["skills", "install", "--help"])
.output()
.expect("doctor help should launch");
let status_help = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN")
.env("ANTHROPIC_BASE_URL", "http://127.0.0.1:9")
.args(["status", "--help"])
.expect("claw should launch");
assert_success(&skills_output);
let skills_stdout = String::from_utf8(skills_output.stdout).expect("stdout should be utf8");
assert!(skills_stdout.contains("Usage /skills [list|install <path>|help]"));
assert!(skills_stdout.contains("Unexpected install"));
let unknown_output = command_in(&temp_dir)
.args(["mcp", "inspect", "--help"])
.output()
.expect("status help should launch");
// then
assert_success(&doctor_help);
let doctor_stdout = String::from_utf8(doctor_help.stdout).expect("stdout should be utf8");
assert!(doctor_stdout.contains("Usage claw doctor"));
assert!(doctor_stdout.contains("local-only health report"));
assert!(!doctor_stdout.contains("Thinking"));
assert_success(&status_help);
let status_stdout = String::from_utf8(status_help.stdout).expect("stdout should be utf8");
assert!(status_stdout.contains("Usage claw status"));
assert!(status_stdout.contains("local workspace snapshot"));
assert!(!status_stdout.contains("Thinking"));
let doctor_stderr = String::from_utf8(doctor_help.stderr).expect("stderr should be utf8");
let status_stderr = String::from_utf8(status_help.stderr).expect("stderr should be utf8");
assert!(!doctor_stderr.contains("auth_unavailable"));
assert!(!status_stderr.contains("auth_unavailable"));
.expect("claw should launch");
assert_success(&unknown_output);
let unknown_stdout = String::from_utf8(unknown_output.stdout).expect("stdout should be utf8");
assert!(unknown_stdout.contains("Usage /mcp [list|show <server>|help]"));
assert!(unknown_stdout.contains("Unexpected inspect"));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}