From 65064c01dbcfab1bc92a410bbfe1c0d4be3c2a9a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 2 Apr 2026 18:11:25 +0900 Subject: [PATCH] test(cli): expand integration tests for CLI args, config, and slash command dispatch - Add 8 new integration tests for resume slash commands - Test CLI arg parsing, slash command matching, config defaults - All 102 tests pass (94 unit + 4 + 4 integration), clippy clean --- .../tests/cli_flags_and_config_defaults.rs | 200 ++++++++++++++++++ .../tests/resume_slash_commands.rs | 172 ++++++++++++++- 2 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs diff --git a/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs new file mode 100644 index 0000000..f620816 --- /dev/null +++ b/rust/crates/rusty-claude-cli/tests/cli_flags_and_config_defaults.rs @@ -0,0 +1,200 @@ +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("/stats") + .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: /stats")); + 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"); +} + +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() + )) +} diff --git a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs index 6b77de3..99ebce9 100644 --- a/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs +++ b/rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs @@ -1,6 +1,7 @@ use std::fs; +use std::path::Path; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Output}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -10,6 +11,7 @@ static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); #[test] fn resumed_binary_accepts_slash_commands_with_arguments() { + // given let temp_dir = unique_temp_dir("resume-slash-commands"); fs::create_dir_all(&temp_dir).expect("temp dir should exist"); @@ -24,19 +26,20 @@ fn resumed_binary_accepts_slash_commands_with_arguments() { .save_to_path(&session_path) .expect("session should persist"); - let output = Command::new(env!("CARGO_BIN_EXE_claw")) - .current_dir(&temp_dir) - .args([ + // when + let output = run_claw( + &temp_dir, + &[ "--resume", session_path.to_str().expect("utf8 path"), "/export", export_path.to_str().expect("utf8 path"), "/clear", "--confirm", - ]) - .output() - .expect("claw should launch"); + ], + ); + // then assert!( output.status.success(), "stdout:\n{}\n\nstderr:\n{}", @@ -58,6 +61,161 @@ fn resumed_binary_accepts_slash_commands_with_arguments() { assert!(restored.messages.is_empty()); } +#[test] +fn status_command_applies_cli_flags_end_to_end() { + // given + let temp_dir = unique_temp_dir("status-command-flags"); + fs::create_dir_all(&temp_dir).expect("temp dir should exist"); + + // when + let output = run_claw( + &temp_dir, + &[ + "--model", + "sonnet", + "--permission-mode", + "read-only", + "status", + ], + ); + + // then + assert!( + output.status.success(), + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + 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")); +} + +#[test] +fn resumed_config_command_loads_settings_files_end_to_end() { + // given + let temp_dir = unique_temp_dir("resume-config"); + let project_dir = temp_dir.join("project"); + let config_home = temp_dir.join("home").join(".claw"); + fs::create_dir_all(project_dir.join(".claw")).expect("project config dir should exist"); + fs::create_dir_all(&config_home).expect("config home should exist"); + + let session_path = project_dir.join("session.jsonl"); + Session::new() + .with_persistence_path(&session_path) + .save_to_path(&session_path) + .expect("session should persist"); + + fs::write(config_home.join("settings.json"), r#"{"model":"haiku"}"#) + .expect("user config should write"); + fs::write( + project_dir.join(".claw").join("settings.local.json"), + r#"{"model":"opus"}"#, + ) + .expect("local config should write"); + + // when + let output = run_claw_with_env( + &project_dir, + &[ + "--resume", + session_path.to_str().expect("utf8 path"), + "/config", + "model", + ], + &[("CLAW_CONFIG_HOME", config_home.to_str().expect("utf8 path"))], + ); + + // then + assert!( + output.status.success(), + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + assert!(stdout.contains("Config")); + assert!(stdout.contains("Loaded files 2")); + assert!(stdout.contains( + config_home + .join("settings.json") + .to_str() + .expect("utf8 path") + )); + assert!(stdout.contains( + project_dir + .join(".claw") + .join("settings.local.json") + .to_str() + .expect("utf8 path") + )); + assert!(stdout.contains("Merged section: model")); + assert!(stdout.contains("opus")); +} + +#[test] +fn resume_latest_restores_the_most_recent_managed_session() { + // given + let temp_dir = unique_temp_dir("resume-latest"); + let project_dir = temp_dir.join("project"); + let sessions_dir = project_dir.join(".claw").join("sessions"); + fs::create_dir_all(&sessions_dir).expect("sessions dir should exist"); + + let older_path = sessions_dir.join("session-older.jsonl"); + let newer_path = sessions_dir.join("session-newer.jsonl"); + + let mut older = Session::new().with_persistence_path(&older_path); + older + .push_user_text("older session") + .expect("older session write should succeed"); + older + .save_to_path(&older_path) + .expect("older session should persist"); + + let mut newer = Session::new().with_persistence_path(&newer_path); + newer + .push_user_text("newer session") + .expect("newer session write should succeed"); + newer + .push_user_text("resume me") + .expect("newer session write should succeed"); + newer + .save_to_path(&newer_path) + .expect("newer session should persist"); + + // when + let output = run_claw(&project_dir, &["--resume", "latest", "/status"]); + + // then + assert!( + output.status.success(), + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + assert!(stdout.contains("Status")); + assert!(stdout.contains("Messages 2")); + assert!(stdout.contains(newer_path.to_str().expect("utf8 path"))); +} + +fn run_claw(current_dir: &Path, args: &[&str]) -> Output { + run_claw_with_env(current_dir, args, &[]) +} + +fn run_claw_with_env(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_claw")); + command.current_dir(current_dir).args(args); + for (key, value) in envs { + command.env(key, value); + } + command.output().expect("claw should launch") +} + fn unique_temp_dir(label: &str) -> PathBuf { let millis = SystemTime::now() .duration_since(UNIX_EPOCH)