Преглед изворни кода

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
YeonGyu-Kim пре 2 месеци
родитељ
комит
65064c01db

+ 200 - 0
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()
+    ))
+}

+ 165 - 7
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)