Files
claw-code/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs
Yeachan-Heo 1f53d961ff Route nested CLI help requests to usage instead of operand fallthrough
The direct CLI wrappers for agents, skills, and mcp treated nested help flags as ordinary operands. That made commands like `claw mcp show --help` report a missing server and `claw skills install --help` fall into filesystem install logic instead of surfacing usage.

This change normalizes help-path arguments before dispatch so nested help stays on the help path. The regression tests cover both handler-level behavior and end-to-end CLI output for nested help and unknown subcommands with trailing help flags.

Constraint: Keep the fix scoped to direct CLI slash-command wrappers without changing unrelated parser behavior
Rejected: Rework top-level argument parsing for all subcommands | broader risk than needed for the regression
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If more nested subcommands are added, extend the help-path normalization table before relying on raw operand dispatch
Tested: cargo build -p commands -p rusty-claude-cli
Tested: cargo test -p commands -p rusty-claude-cli
Not-tested: cargo clippy -p commands -p rusty-claude-cli --all-targets --no-deps -- -D warnings (pre-existing warnings in untouched files block clean run)
2026-04-05 16:38:43 +00:00

237 lines
7.9 KiB
Rust

use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use runtime::Session;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn status_command_applies_model_and_permission_mode_flags() {
// given
let temp_dir = unique_temp_dir("status-flags");
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",
"status",
])
.output()
.expect("claw should launch");
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
assert!(stdout.contains("Status"));
assert!(stdout.contains("Model claude-sonnet-4-6"));
assert!(stdout.contains("Permission mode read-only"));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn resume_flag_loads_a_saved_session_and_dispatches_status() {
// given
let temp_dir = unique_temp_dir("resume-status");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
let session_path = write_session(&temp_dir, "resume-status");
// when
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(&temp_dir)
.args([
"--resume",
session_path.to_str().expect("utf8 path"),
"/status",
])
.output()
.expect("claw should launch");
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
assert!(stdout.contains("Status"));
assert!(stdout.contains("Messages 1"));
assert!(stdout.contains("Session "));
assert!(stdout.contains(session_path.to_str().expect("utf8 path")));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() {
// given
let temp_dir = unique_temp_dir("slash-dispatch");
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
// when
let help_output = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(&temp_dir)
.arg("/help")
.output()
.expect("claw should launch");
let unknown_output = Command::new(env!("CARGO_BIN_EXE_claw"))
.current_dir(&temp_dir)
.arg("/zstats")
.output()
.expect("claw should launch");
// then
assert_success(&help_output);
let help_stdout = String::from_utf8(help_output.stdout).expect("stdout should be utf8");
assert!(help_stdout.contains("Interactive slash commands:"));
assert!(help_stdout.contains("/status"));
assert!(
!unknown_output.status.success(),
"stdout:\n{}\n\nstderr:\n{}",
String::from_utf8_lossy(&unknown_output.stdout),
String::from_utf8_lossy(&unknown_output.stderr)
);
let stderr = String::from_utf8(unknown_output.stderr).expect("stderr should be utf8");
assert!(stderr.contains("unknown slash command outside the REPL: /zstats"));
assert!(stderr.contains("Did you mean"));
assert!(stderr.contains("/status"));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
fn config_command_loads_defaults_from_standard_config_locations() {
// given
let temp_dir = unique_temp_dir("config-defaults");
let config_home = temp_dir.join("home").join(".claw");
fs::create_dir_all(temp_dir.join(".claw")).expect("project config dir should exist");
fs::create_dir_all(&config_home).expect("home config dir should exist");
fs::write(config_home.join("settings.json"), r#"{"model":"haiku"}"#)
.expect("write user settings");
fs::write(temp_dir.join(".claw.json"), r#"{"model":"sonnet"}"#)
.expect("write project settings");
fs::write(
temp_dir.join(".claw").join("settings.local.json"),
r#"{"model":"opus"}"#,
)
.expect("write local settings");
let session_path = write_session(&temp_dir, "config-defaults");
// when
let output = command_in(&temp_dir)
.env("CLAW_CONFIG_HOME", &config_home)
.args([
"--resume",
session_path.to_str().expect("utf8 path"),
"/config",
"model",
])
.output()
.expect("claw should launch");
// then
assert_success(&output);
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
assert!(stdout.contains("Config"));
assert!(stdout.contains("Loaded files 3"));
assert!(stdout.contains("Merged section: model"));
assert!(stdout.contains("opus"));
assert!(stdout.contains(
config_home
.join("settings.json")
.to_str()
.expect("utf8 path")
));
assert!(stdout.contains(temp_dir.join(".claw.json").to_str().expect("utf8 path")));
assert!(stdout.contains(
temp_dir
.join(".claw")
.join("settings.local.json")
.to_str()
.expect("utf8 path")
));
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
}
#[test]
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");
let mcp_output = command_in(&temp_dir)
.args(["mcp", "show", "--help"])
.output()
.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"));
let skills_output = command_in(&temp_dir)
.args(["skills", "install", "--help"])
.output()
.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("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");
}
fn command_in(cwd: &Path) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
command.current_dir(cwd);
command
}
fn write_session(root: &Path, label: &str) -> PathBuf {
let session_path = root.join(format!("{label}.jsonl"));
let mut session = Session::new();
session
.push_user_text(format!("session fixture for {label}"))
.expect("session write should succeed");
session
.save_to_path(&session_path)
.expect("session should persist");
session_path
}
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"stdout:\n{}\n\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn unique_temp_dir(label: &str) -> PathBuf {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_millis();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"claw-{label}-{}-{millis}-{counter}",
std::process::id()
))
}