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) -> io::Result<StateValidation> {
37 let mut issues = Vec::new();
38
39 if !workspace.exists(agent_dir) {
40 return Ok(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 Ok(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(
116 workspace: &dyn Workspace,
117 agent_dir: &Path,
118) -> io::Result<RecoveryStatus> {
119 if !workspace.exists(agent_dir) {
120 workspace.create_dir_all(&agent_dir.join("logs"))?;
121 return Ok(RecoveryStatus::Recovered);
122 }
123
124 let validation = validate_agent_state_with_workspace(workspace, agent_dir)?;
125 if validation.is_valid {
126 workspace.create_dir_all(&agent_dir.join("logs"))?;
127 return Ok(RecoveryStatus::Valid);
128 }
129
130 remove_zero_length_files_with_workspace(workspace, agent_dir)?;
132 workspace.create_dir_all(&agent_dir.join("logs"))?;
133
134 let post_validation = validate_agent_state_with_workspace(workspace, agent_dir)?;
135 if post_validation.is_valid {
136 Ok(RecoveryStatus::Recovered)
137 } else {
138 Ok(RecoveryStatus::Unrecoverable(format!(
139 "Unresolved .agent issues: {}",
140 post_validation.issues.join(", ")
141 )))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::workspace::MemoryWorkspace;
149 use std::path::Path;
150
151 #[test]
152 fn auto_repair_with_workspace_creates_missing_directory() {
153 let workspace = MemoryWorkspace::new_test();
154 let agent_dir = Path::new(".agent");
155
156 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
157
158 assert_eq!(status, RecoveryStatus::Recovered);
159 assert!(workspace.exists(&agent_dir.join("logs")));
160 }
161
162 #[test]
163 fn auto_repair_with_workspace_removes_zero_length_files() {
164 let workspace = MemoryWorkspace::new_test()
165 .with_file(".agent/logs/.keep", "")
166 .with_file(".agent/PLAN.md", ""); let agent_dir = Path::new(".agent");
169 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
170
171 assert_eq!(status, RecoveryStatus::Recovered);
172 assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
173 }
174
175 #[test]
176 fn auto_repair_with_workspace_valid_state() {
177 let workspace = MemoryWorkspace::new_test()
178 .with_file(".agent/logs/.keep", "")
179 .with_file(".agent/PLAN.md", "# Plan\nSome content");
180
181 let agent_dir = Path::new(".agent");
182 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
183
184 assert_eq!(status, RecoveryStatus::Valid);
185 assert!(workspace.exists(&agent_dir.join("PLAN.md")));
186 }
187
188 #[test]
189 fn auto_repair_with_workspace_multiple_zero_length_files() {
190 let workspace = MemoryWorkspace::new_test()
191 .with_file(".agent/logs/.keep", "")
192 .with_file(".agent/PLAN.md", "")
193 .with_file(".agent/ISSUES.md", "")
194 .with_file(".agent/STATUS.md", "valid content");
195
196 let agent_dir = Path::new(".agent");
197 let status = auto_repair_with_workspace(&workspace, agent_dir).unwrap();
198
199 assert_eq!(status, RecoveryStatus::Recovered);
200 assert!(!workspace.exists(&agent_dir.join("PLAN.md")));
201 assert!(!workspace.exists(&agent_dir.join("ISSUES.md")));
202 assert!(workspace.exists(&agent_dir.join("STATUS.md")));
203 }
204}