ralph_workflow/files/
backup.rs1use std::path::Path;
7
8use crate::workspace::Workspace;
9
10pub fn create_prompt_backup_with_workspace(
42 workspace: &dyn Workspace,
43) -> std::io::Result<Option<String>> {
44 let prompt_path = Path::new("PROMPT.md");
45
46 if !workspace.exists(prompt_path) {
48 return Ok(None);
49 }
50
51 let agent_dir = Path::new(".agent");
53 let backup_base = Path::new(".agent/PROMPT.md.backup");
54 let backup_1 = Path::new(".agent/PROMPT.md.backup.1");
55 let backup_2 = Path::new(".agent/PROMPT.md.backup.2");
56
57 workspace.create_dir_all(agent_dir)?;
58
59 let content = workspace.read(prompt_path).map_err(|e| {
61 std::io::Error::new(
62 e.kind(),
63 format!("Failed to read PROMPT.md for backup: {e}"),
64 )
65 })?;
66
67 let _ = workspace.remove_if_exists(backup_2);
70
71 if workspace.exists(backup_1) {
73 let _ = workspace.rename(backup_1, backup_2);
74 }
75
76 if workspace.exists(backup_base) {
78 let _ = workspace.rename(backup_base, backup_1);
79 }
80
81 workspace.write_atomic(backup_base, &content).map_err(|e| {
83 std::io::Error::new(e.kind(), format!("Failed to write PROMPT.md backup: {e}"))
84 })?;
85
86 let readonly_warning = [backup_base, backup_1, backup_2]
88 .iter()
89 .filter(|backup_path| workspace.exists(backup_path))
90 .find_map(|backup_path| {
91 workspace
92 .set_readonly(backup_path)
93 .err()
94 .map(|e| e.to_string())
95 });
96
97 Ok(readonly_warning)
98}
99
100pub fn make_prompt_read_only_with_workspace(workspace: &dyn Workspace) -> Option<String> {
111 let prompt_path = Path::new("PROMPT.md");
112
113 if !workspace.exists(prompt_path) {
115 return None;
116 }
117
118 match workspace.set_readonly(prompt_path) {
120 Ok(()) => None,
121 Err(e) => Some(format!(
122 "Failed to set read-only permissions on PROMPT.md: {e}"
123 )),
124 }
125}
126
127pub fn make_prompt_writable_with_workspace(workspace: &dyn Workspace) -> Option<String> {
138 let prompt_path = Path::new("PROMPT.md");
139
140 if !workspace.exists(prompt_path) {
142 return None;
143 }
144
145 match workspace.set_writable(prompt_path) {
147 Ok(()) => None,
148 Err(e) => Some(format!("Failed to set write permissions on PROMPT.md: {e}")),
149 }
150}
151
152const DIFF_BACKUP_PATH: &str = ".agent/DIFF.backup";
158
159pub fn write_diff_backup_with_workspace(
177 workspace: &dyn Workspace,
178 diff_content: &str,
179) -> std::io::Result<std::path::PathBuf> {
180 let backup_path = Path::new(DIFF_BACKUP_PATH);
181
182 workspace.create_dir_all(Path::new(".agent"))?;
184
185 workspace.write(backup_path, diff_content)?;
187
188 Ok(backup_path.to_path_buf())
189}
190
191#[cfg(all(test, feature = "test-utils"))]
198mod workspace_tests {
199 use super::*;
200 use crate::workspace::{MemoryWorkspace, Workspace};
201
202 #[test]
203 fn test_create_prompt_backup_with_workspace_creates_file() {
204 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "# Test Content\n");
205
206 let result = create_prompt_backup_with_workspace(&workspace);
207 assert!(result.is_ok());
208
209 assert!(workspace.exists(Path::new(".agent/PROMPT.md.backup")));
211 assert_eq!(
212 workspace.get_file(".agent/PROMPT.md.backup").unwrap(),
213 "# Test Content\n"
214 );
215 }
216
217 #[test]
218 fn test_create_prompt_backup_with_workspace_missing_prompt() {
219 let workspace = MemoryWorkspace::new_test();
220 let result = create_prompt_backup_with_workspace(&workspace);
223 assert!(result.is_ok());
224 assert!(result.unwrap().is_none()); assert!(!workspace.exists(Path::new(".agent/PROMPT.md.backup")));
228 }
229
230 #[test]
231 fn test_create_prompt_backup_with_workspace_rotation() {
232 let workspace = MemoryWorkspace::new_test()
233 .with_file("PROMPT.md", "# Version 1\n")
234 .with_dir(".agent");
235
236 create_prompt_backup_with_workspace(&workspace).unwrap();
238 assert_eq!(
239 workspace.get_file(".agent/PROMPT.md.backup").unwrap(),
240 "# Version 1\n"
241 );
242
243 workspace
245 .write(Path::new("PROMPT.md"), "# Version 2\n")
246 .unwrap();
247 create_prompt_backup_with_workspace(&workspace).unwrap();
248
249 assert_eq!(
251 workspace.get_file(".agent/PROMPT.md.backup").unwrap(),
252 "# Version 2\n"
253 );
254 assert_eq!(
255 workspace.get_file(".agent/PROMPT.md.backup.1").unwrap(),
256 "# Version 1\n"
257 );
258
259 workspace
261 .write(Path::new("PROMPT.md"), "# Version 3\n")
262 .unwrap();
263 create_prompt_backup_with_workspace(&workspace).unwrap();
264
265 assert_eq!(
267 workspace.get_file(".agent/PROMPT.md.backup").unwrap(),
268 "# Version 3\n"
269 );
270 assert_eq!(
271 workspace.get_file(".agent/PROMPT.md.backup.1").unwrap(),
272 "# Version 2\n"
273 );
274 assert_eq!(
275 workspace.get_file(".agent/PROMPT.md.backup.2").unwrap(),
276 "# Version 1\n"
277 );
278 }
279
280 #[test]
281 fn test_create_prompt_backup_with_workspace_deletes_oldest() {
282 let workspace = MemoryWorkspace::new_test().with_dir(".agent");
283
284 for i in 1..=4 {
286 workspace
287 .write(Path::new("PROMPT.md"), &format!("# Version {i}\n"))
288 .unwrap();
289 create_prompt_backup_with_workspace(&workspace).unwrap();
290 }
291
292 assert!(workspace.exists(Path::new(".agent/PROMPT.md.backup")));
294 assert!(workspace.exists(Path::new(".agent/PROMPT.md.backup.1")));
295 assert!(workspace.exists(Path::new(".agent/PROMPT.md.backup.2")));
296
297 assert_eq!(
299 workspace.get_file(".agent/PROMPT.md.backup").unwrap(),
300 "# Version 4\n"
301 );
302 assert_eq!(
303 workspace.get_file(".agent/PROMPT.md.backup.1").unwrap(),
304 "# Version 3\n"
305 );
306 assert_eq!(
307 workspace.get_file(".agent/PROMPT.md.backup.2").unwrap(),
308 "# Version 2\n"
309 );
310 }
311
312 #[test]
313 fn test_make_prompt_read_only_with_workspace() {
314 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "# Test\n");
315
316 let result = make_prompt_read_only_with_workspace(&workspace);
318 assert!(result.is_none());
319 }
320
321 #[test]
322 fn test_make_prompt_read_only_with_workspace_missing() {
323 let workspace = MemoryWorkspace::new_test();
324 let result = make_prompt_read_only_with_workspace(&workspace);
327 assert!(result.is_none()); }
329
330 #[test]
331 fn test_make_prompt_writable_with_workspace() {
332 let workspace = MemoryWorkspace::new_test().with_file("PROMPT.md", "# Test\n");
333
334 let result = make_prompt_writable_with_workspace(&workspace);
335 assert!(result.is_none());
336 }
337
338 #[test]
339 fn test_write_diff_backup_with_workspace() {
340 let workspace = MemoryWorkspace::new_test();
341 let diff = "+added\n-removed";
342
343 let result = write_diff_backup_with_workspace(&workspace, diff);
344 assert!(result.is_ok());
345
346 let path = result.unwrap();
347 assert_eq!(path, Path::new(".agent/DIFF.backup"));
348 assert_eq!(workspace.get_file(".agent/DIFF.backup").unwrap(), diff);
349 }
350
351 #[test]
352 fn test_write_diff_backup_creates_agent_dir() {
353 let workspace = MemoryWorkspace::new_test();
354 let diff = "some diff content";
357 let result = write_diff_backup_with_workspace(&workspace, diff);
358 assert!(result.is_ok());
359
360 assert!(workspace.exists(Path::new(".agent")));
362 assert!(workspace.exists(Path::new(".agent/DIFF.backup")));
363 assert_eq!(workspace.get_file(".agent/DIFF.backup").unwrap(), diff);
364 }
365
366 #[test]
367 fn test_write_diff_backup_overwrites_existing() {
368 let workspace = MemoryWorkspace::new_test().with_file(".agent/DIFF.backup", "old content");
369
370 let new_diff = "new diff content";
371 let result = write_diff_backup_with_workspace(&workspace, new_diff);
372 assert!(result.is_ok());
373
374 assert_eq!(workspace.get_file(".agent/DIFF.backup").unwrap(), new_diff);
376 }
377}