ralph_workflow/files/io/
recovery.rs1use std::io;
8use std::path::Path;
9
10use crate::workspace::Workspace;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum RecoveryStatus {
15 Valid,
17 Recovered,
19 Unrecoverable(String),
21}
22
23#[derive(Debug, Clone)]
24struct StateValidation {
25 pub is_valid: bool,
26 pub issues: Vec<String>,
27}
28
29fn validate_agent_state_with_workspace(
34 workspace: &dyn Workspace,
35 agent_dir: &Path,
36) -> StateValidation {
37 let mut issues = Vec::new();
38
39 if !workspace.exists(agent_dir) {
40 return StateValidation {
41 is_valid: false,
42 issues: vec![".agent/ directory does not exist".to_string()],
43 };
44 }
45
46 if let Ok(entries) = workspace.read_dir(agent_dir) {
48 for entry in entries {
49 let path = entry.path();
50 if !entry.is_file() {
51 continue;
52 }
53 if workspace.read(path).is_err() {
54 issues.push(format!("Corrupted file: {}", path.display()));
55 }
56 }
57 }
58
59 for filename in [
61 "PLAN.md",
62 "ISSUES.md",
63 "STATUS.md",
64 "NOTES.md",
65 "commit-message.txt",
66 ] {
67 let file_path = agent_dir.join(filename);
68 if !workspace.exists(&file_path) {
69 continue;
70 }
71 if let Ok(content) = workspace.read(&file_path) {
72 if content.is_empty() {
73 issues.push(format!("Zero-length file: {filename}"));
74 }
75 }
76 }
77
78 StateValidation {
79 is_valid: issues.is_empty(),
80 issues,
81 }
82}
83
84fn remove_zero_length_files_with_workspace(
85 workspace: &dyn Workspace,
86 agent_dir: &Path,
87) -> io::Result<usize> {
88 let mut removed = 0;
89
90 for filename in [
91 "PLAN.md",
92 "ISSUES.md",
93 "STATUS.md",
94 "NOTES.md",
95 "commit-message.txt",
96 ] {
97 let file_path = agent_dir.join(filename);
98 if !workspace.exists(&file_path) {
99 continue;
100 }
101 if let Ok(content) = workspace.read(&file_path) {
102 if content.is_empty() {
103 workspace.remove(&file_path)?;
104 removed += 1;
105 }
106 }
107 }
108
109 Ok(removed)
110}
111
112pub fn auto_repair_with_workspace(
120 workspace: &dyn Workspace,
121 agent_dir: &Path,
122) -> io::Result<RecoveryStatus> {
123 if !workspace.exists(agent_dir) {
124 workspace.create_dir_all(&agent_dir.join("logs"))?;
125 return Ok(RecoveryStatus::Recovered);
126 }
127
128 let validation = validate_agent_state_with_workspace(workspace, agent_dir);
129 if validation.is_valid {
130 workspace.create_dir_all(&agent_dir.join("logs"))?;
131 return Ok(RecoveryStatus::Valid);
132 }
133
134 remove_zero_length_files_with_workspace(workspace, agent_dir)?;
136 workspace.create_dir_all(&agent_dir.join("logs"))?;
137
138 let post_validation = validate_agent_state_with_workspace(workspace, agent_dir);
139 if post_validation.is_valid {
140 Ok(RecoveryStatus::Recovered)
141 } else {
142 Ok(RecoveryStatus::Unrecoverable(format!(
143 "Unresolved .agent issues: {}",
144 post_validation.issues.join(", ")
145 )))
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::workspace::MemoryWorkspace;
153 use std::path::Path;
154
155 #[test]
156 fn auto_repair_with_workspace_creates_missing_directory() {
157 let workspace = MemoryWorkspace::new_test();
158 let agent_dir = Path::new(".agent");
159
160 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
161
162 assert_eq!(status, RecoveryStatus::Recovered);
163 assert!(workspace.exists(&agent_dir.join("logs")));
164 }
165
166 #[test]
167 fn auto_repair_with_workspace_removes_zero_length_files() {
168 let workspace = MemoryWorkspace::new_test()
169 .with_file(".agent/logs/.keep", "")
170 .with_file(".agent/PLAN.md", ""); let agent_dir = Path::new(".agent");
173 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
174
175 assert_eq!(status, RecoveryStatus::Recovered);
176 assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
177 }
178
179 #[test]
180 fn auto_repair_with_workspace_valid_state() {
181 let workspace = MemoryWorkspace::new_test()
182 .with_file(".agent/logs/.keep", "")
183 .with_file(".agent/PLAN.md", "# Plan\nSome content");
184
185 let agent_dir = Path::new(".agent");
186 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
187
188 assert_eq!(status, RecoveryStatus::Valid);
189 assert!(workspace.exists(&agent_dir.join("PLAN.md")));
190 }
191
192 #[test]
193 fn auto_repair_with_workspace_multiple_zero_length_files() {
194 let workspace = MemoryWorkspace::new_test()
195 .with_file(".agent/logs/.keep", "")
196 .with_file(".agent/PLAN.md", "")
197 .with_file(".agent/ISSUES.md", "")
198 .with_file(".agent/STATUS.md", "valid content");
199
200 let agent_dir = Path::new(".agent");
201 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
202
203 assert_eq!(status, RecoveryStatus::Recovered);
204 assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
205 assert!(!workspace.exists(&agent_dir.join("ISSUES.md")));
206 assert!(workspace.exists(&agent_dir.join("STATUS.md")));
207 }
208}