cli_flags_and_config_defaults.rs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. use std::fs;
  2. use std::path::{Path, PathBuf};
  3. use std::process::{Command, Output};
  4. use std::sync::atomic::{AtomicU64, Ordering};
  5. use std::time::{SystemTime, UNIX_EPOCH};
  6. use runtime::Session;
  7. static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
  8. #[test]
  9. fn status_command_applies_model_and_permission_mode_flags() {
  10. // given
  11. let temp_dir = unique_temp_dir("status-flags");
  12. fs::create_dir_all(&temp_dir).expect("temp dir should exist");
  13. // when
  14. let output = Command::new(env!("CARGO_BIN_EXE_claw"))
  15. .current_dir(&temp_dir)
  16. .args([
  17. "--model",
  18. "sonnet",
  19. "--permission-mode",
  20. "read-only",
  21. "status",
  22. ])
  23. .output()
  24. .expect("claw should launch");
  25. // then
  26. assert_success(&output);
  27. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  28. assert!(stdout.contains("Status"));
  29. assert!(stdout.contains("Model claude-sonnet-4-6"));
  30. assert!(stdout.contains("Permission mode read-only"));
  31. fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
  32. }
  33. #[test]
  34. fn resume_flag_loads_a_saved_session_and_dispatches_status() {
  35. // given
  36. let temp_dir = unique_temp_dir("resume-status");
  37. fs::create_dir_all(&temp_dir).expect("temp dir should exist");
  38. let session_path = write_session(&temp_dir, "resume-status");
  39. // when
  40. let output = Command::new(env!("CARGO_BIN_EXE_claw"))
  41. .current_dir(&temp_dir)
  42. .args([
  43. "--resume",
  44. session_path.to_str().expect("utf8 path"),
  45. "/status",
  46. ])
  47. .output()
  48. .expect("claw should launch");
  49. // then
  50. assert_success(&output);
  51. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  52. assert!(stdout.contains("Status"));
  53. assert!(stdout.contains("Messages 1"));
  54. assert!(stdout.contains("Session "));
  55. assert!(stdout.contains(session_path.to_str().expect("utf8 path")));
  56. fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
  57. }
  58. #[test]
  59. fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() {
  60. // given
  61. let temp_dir = unique_temp_dir("slash-dispatch");
  62. fs::create_dir_all(&temp_dir).expect("temp dir should exist");
  63. // when
  64. let help_output = Command::new(env!("CARGO_BIN_EXE_claw"))
  65. .current_dir(&temp_dir)
  66. .arg("/help")
  67. .output()
  68. .expect("claw should launch");
  69. let unknown_output = Command::new(env!("CARGO_BIN_EXE_claw"))
  70. .current_dir(&temp_dir)
  71. .arg("/stats")
  72. .output()
  73. .expect("claw should launch");
  74. // then
  75. assert_success(&help_output);
  76. let help_stdout = String::from_utf8(help_output.stdout).expect("stdout should be utf8");
  77. assert!(help_stdout.contains("Interactive slash commands:"));
  78. assert!(help_stdout.contains("/status"));
  79. assert!(
  80. !unknown_output.status.success(),
  81. "stdout:\n{}\n\nstderr:\n{}",
  82. String::from_utf8_lossy(&unknown_output.stdout),
  83. String::from_utf8_lossy(&unknown_output.stderr)
  84. );
  85. let stderr = String::from_utf8(unknown_output.stderr).expect("stderr should be utf8");
  86. assert!(stderr.contains("unknown slash command outside the REPL: /stats"));
  87. assert!(stderr.contains("Did you mean"));
  88. assert!(stderr.contains("/status"));
  89. fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
  90. }
  91. #[test]
  92. fn config_command_loads_defaults_from_standard_config_locations() {
  93. // given
  94. let temp_dir = unique_temp_dir("config-defaults");
  95. let config_home = temp_dir.join("home").join(".claw");
  96. fs::create_dir_all(temp_dir.join(".claw")).expect("project config dir should exist");
  97. fs::create_dir_all(&config_home).expect("home config dir should exist");
  98. fs::write(config_home.join("settings.json"), r#"{"model":"haiku"}"#)
  99. .expect("write user settings");
  100. fs::write(temp_dir.join(".claw.json"), r#"{"model":"sonnet"}"#)
  101. .expect("write project settings");
  102. fs::write(
  103. temp_dir.join(".claw").join("settings.local.json"),
  104. r#"{"model":"opus"}"#,
  105. )
  106. .expect("write local settings");
  107. let session_path = write_session(&temp_dir, "config-defaults");
  108. // when
  109. let output = command_in(&temp_dir)
  110. .env("CLAW_CONFIG_HOME", &config_home)
  111. .args([
  112. "--resume",
  113. session_path.to_str().expect("utf8 path"),
  114. "/config",
  115. "model",
  116. ])
  117. .output()
  118. .expect("claw should launch");
  119. // then
  120. assert_success(&output);
  121. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  122. assert!(stdout.contains("Config"));
  123. assert!(stdout.contains("Loaded files 3"));
  124. assert!(stdout.contains("Merged section: model"));
  125. assert!(stdout.contains("opus"));
  126. assert!(stdout.contains(
  127. config_home
  128. .join("settings.json")
  129. .to_str()
  130. .expect("utf8 path")
  131. ));
  132. assert!(stdout.contains(temp_dir.join(".claw.json").to_str().expect("utf8 path")));
  133. assert!(stdout.contains(
  134. temp_dir
  135. .join(".claw")
  136. .join("settings.local.json")
  137. .to_str()
  138. .expect("utf8 path")
  139. ));
  140. fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
  141. }
  142. fn command_in(cwd: &Path) -> Command {
  143. let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
  144. command.current_dir(cwd);
  145. command
  146. }
  147. fn write_session(root: &Path, label: &str) -> PathBuf {
  148. let session_path = root.join(format!("{label}.jsonl"));
  149. let mut session = Session::new();
  150. session
  151. .push_user_text(format!("session fixture for {label}"))
  152. .expect("session write should succeed");
  153. session
  154. .save_to_path(&session_path)
  155. .expect("session should persist");
  156. session_path
  157. }
  158. fn assert_success(output: &Output) {
  159. assert!(
  160. output.status.success(),
  161. "stdout:\n{}\n\nstderr:\n{}",
  162. String::from_utf8_lossy(&output.stdout),
  163. String::from_utf8_lossy(&output.stderr)
  164. );
  165. }
  166. fn unique_temp_dir(label: &str) -> PathBuf {
  167. let millis = SystemTime::now()
  168. .duration_since(UNIX_EPOCH)
  169. .expect("clock should be after epoch")
  170. .as_millis();
  171. let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
  172. std::env::temp_dir().join(format!(
  173. "claw-{label}-{}-{millis}-{counter}",
  174. std::process::id()
  175. ))
  176. }