mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-06 03:08:48 +03:00
Merge remote-tracking branch 'origin/dori/integration-tests'
This commit is contained in:
@@ -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()
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::{Command, Output};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resumed_binary_accepts_slash_commands_with_arguments() {
|
fn resumed_binary_accepts_slash_commands_with_arguments() {
|
||||||
|
// given
|
||||||
let temp_dir = unique_temp_dir("resume-slash-commands");
|
let temp_dir = unique_temp_dir("resume-slash-commands");
|
||||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
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)
|
.save_to_path(&session_path)
|
||||||
.expect("session should persist");
|
.expect("session should persist");
|
||||||
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
// when
|
||||||
.current_dir(&temp_dir)
|
let output = run_claw(
|
||||||
.args([
|
&temp_dir,
|
||||||
|
&[
|
||||||
"--resume",
|
"--resume",
|
||||||
session_path.to_str().expect("utf8 path"),
|
session_path.to_str().expect("utf8 path"),
|
||||||
"/export",
|
"/export",
|
||||||
export_path.to_str().expect("utf8 path"),
|
export_path.to_str().expect("utf8 path"),
|
||||||
"/clear",
|
"/clear",
|
||||||
"--confirm",
|
"--confirm",
|
||||||
])
|
],
|
||||||
.output()
|
);
|
||||||
.expect("claw should launch");
|
|
||||||
|
|
||||||
|
// then
|
||||||
assert!(
|
assert!(
|
||||||
output.status.success(),
|
output.status.success(),
|
||||||
"stdout:\n{}\n\nstderr:\n{}",
|
"stdout:\n{}\n\nstderr:\n{}",
|
||||||
@@ -58,6 +61,161 @@ fn resumed_binary_accepts_slash_commands_with_arguments() {
|
|||||||
assert!(restored.messages.is_empty());
|
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 {
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
||||||
let millis = SystemTime::now()
|
let millis = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
Reference in New Issue
Block a user