resume_slash_commands.rs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. use std::fs;
  2. use std::path::Path;
  3. use std::path::PathBuf;
  4. use std::process::{Command, Output};
  5. use std::sync::atomic::{AtomicU64, Ordering};
  6. use std::time::{SystemTime, UNIX_EPOCH};
  7. use runtime::Session;
  8. static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
  9. #[test]
  10. fn resumed_binary_accepts_slash_commands_with_arguments() {
  11. // given
  12. let temp_dir = unique_temp_dir("resume-slash-commands");
  13. fs::create_dir_all(&temp_dir).expect("temp dir should exist");
  14. let session_path = temp_dir.join("session.jsonl");
  15. let export_path = temp_dir.join("notes.txt");
  16. let mut session = Session::new();
  17. session
  18. .push_user_text("ship the slash command harness")
  19. .expect("session write should succeed");
  20. session
  21. .save_to_path(&session_path)
  22. .expect("session should persist");
  23. // when
  24. let output = run_claw(
  25. &temp_dir,
  26. &[
  27. "--resume",
  28. session_path.to_str().expect("utf8 path"),
  29. "/export",
  30. export_path.to_str().expect("utf8 path"),
  31. "/clear",
  32. "--confirm",
  33. ],
  34. );
  35. // then
  36. assert!(
  37. output.status.success(),
  38. "stdout:\n{}\n\nstderr:\n{}",
  39. String::from_utf8_lossy(&output.stdout),
  40. String::from_utf8_lossy(&output.stderr)
  41. );
  42. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  43. assert!(stdout.contains("Export"));
  44. assert!(stdout.contains("wrote transcript"));
  45. assert!(stdout.contains(export_path.to_str().expect("utf8 path")));
  46. assert!(stdout.contains("Cleared resumed session file"));
  47. let export = fs::read_to_string(&export_path).expect("export file should exist");
  48. assert!(export.contains("# Conversation Export"));
  49. assert!(export.contains("ship the slash command harness"));
  50. let restored = Session::load_from_path(&session_path).expect("cleared session should load");
  51. assert!(restored.messages.is_empty());
  52. }
  53. #[test]
  54. fn status_command_applies_cli_flags_end_to_end() {
  55. // given
  56. let temp_dir = unique_temp_dir("status-command-flags");
  57. fs::create_dir_all(&temp_dir).expect("temp dir should exist");
  58. // when
  59. let output = run_claw(
  60. &temp_dir,
  61. &[
  62. "--model",
  63. "sonnet",
  64. "--permission-mode",
  65. "read-only",
  66. "status",
  67. ],
  68. );
  69. // then
  70. assert!(
  71. output.status.success(),
  72. "stdout:\n{}\n\nstderr:\n{}",
  73. String::from_utf8_lossy(&output.stdout),
  74. String::from_utf8_lossy(&output.stderr)
  75. );
  76. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  77. assert!(stdout.contains("Status"));
  78. assert!(stdout.contains("Model claude-sonnet-4-6"));
  79. assert!(stdout.contains("Permission mode read-only"));
  80. }
  81. #[test]
  82. fn resumed_config_command_loads_settings_files_end_to_end() {
  83. // given
  84. let temp_dir = unique_temp_dir("resume-config");
  85. let project_dir = temp_dir.join("project");
  86. let config_home = temp_dir.join("home").join(".claw");
  87. fs::create_dir_all(project_dir.join(".claw")).expect("project config dir should exist");
  88. fs::create_dir_all(&config_home).expect("config home should exist");
  89. let session_path = project_dir.join("session.jsonl");
  90. Session::new()
  91. .with_persistence_path(&session_path)
  92. .save_to_path(&session_path)
  93. .expect("session should persist");
  94. fs::write(config_home.join("settings.json"), r#"{"model":"haiku"}"#)
  95. .expect("user config should write");
  96. fs::write(
  97. project_dir.join(".claw").join("settings.local.json"),
  98. r#"{"model":"opus"}"#,
  99. )
  100. .expect("local config should write");
  101. // when
  102. let output = run_claw_with_env(
  103. &project_dir,
  104. &[
  105. "--resume",
  106. session_path.to_str().expect("utf8 path"),
  107. "/config",
  108. "model",
  109. ],
  110. &[("CLAW_CONFIG_HOME", config_home.to_str().expect("utf8 path"))],
  111. );
  112. // then
  113. assert!(
  114. output.status.success(),
  115. "stdout:\n{}\n\nstderr:\n{}",
  116. String::from_utf8_lossy(&output.stdout),
  117. String::from_utf8_lossy(&output.stderr)
  118. );
  119. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  120. assert!(stdout.contains("Config"));
  121. assert!(stdout.contains("Loaded files 2"));
  122. assert!(stdout.contains(
  123. config_home
  124. .join("settings.json")
  125. .to_str()
  126. .expect("utf8 path")
  127. ));
  128. assert!(stdout.contains(
  129. project_dir
  130. .join(".claw")
  131. .join("settings.local.json")
  132. .to_str()
  133. .expect("utf8 path")
  134. ));
  135. assert!(stdout.contains("Merged section: model"));
  136. assert!(stdout.contains("opus"));
  137. }
  138. #[test]
  139. fn resume_latest_restores_the_most_recent_managed_session() {
  140. // given
  141. let temp_dir = unique_temp_dir("resume-latest");
  142. let project_dir = temp_dir.join("project");
  143. let sessions_dir = project_dir.join(".claw").join("sessions");
  144. fs::create_dir_all(&sessions_dir).expect("sessions dir should exist");
  145. let older_path = sessions_dir.join("session-older.jsonl");
  146. let newer_path = sessions_dir.join("session-newer.jsonl");
  147. let mut older = Session::new().with_persistence_path(&older_path);
  148. older
  149. .push_user_text("older session")
  150. .expect("older session write should succeed");
  151. older
  152. .save_to_path(&older_path)
  153. .expect("older session should persist");
  154. let mut newer = Session::new().with_persistence_path(&newer_path);
  155. newer
  156. .push_user_text("newer session")
  157. .expect("newer session write should succeed");
  158. newer
  159. .push_user_text("resume me")
  160. .expect("newer session write should succeed");
  161. newer
  162. .save_to_path(&newer_path)
  163. .expect("newer session should persist");
  164. // when
  165. let output = run_claw(&project_dir, &["--resume", "latest", "/status"]);
  166. // then
  167. assert!(
  168. output.status.success(),
  169. "stdout:\n{}\n\nstderr:\n{}",
  170. String::from_utf8_lossy(&output.stdout),
  171. String::from_utf8_lossy(&output.stderr)
  172. );
  173. let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
  174. assert!(stdout.contains("Status"));
  175. assert!(stdout.contains("Messages 2"));
  176. assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
  177. }
  178. fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
  179. run_claw_with_env(current_dir, args, &[])
  180. }
  181. fn run_claw_with_env(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output {
  182. let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
  183. command.current_dir(current_dir).args(args);
  184. for (key, value) in envs {
  185. command.env(key, value);
  186. }
  187. command.output().expect("claw should launch")
  188. }
  189. fn unique_temp_dir(label: &str) -> PathBuf {
  190. let millis = SystemTime::now()
  191. .duration_since(UNIX_EPOCH)
  192. .expect("clock should be after epoch")
  193. .as_millis();
  194. let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
  195. std::env::temp_dir().join(format!(
  196. "claw-{label}-{}-{millis}-{counter}",
  197. std::process::id()
  198. ))
  199. }