resume_slash_commands.rs 7.9 KB

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