ralph_workflow/files/io/
agent_files.rs1use std::fs;
7use std::io::{self, BufRead};
8use std::path::Path;
9
10use super::{
11 context::overwrite_one_liner, context::VAGUE_ISSUES_LINE, context::VAGUE_NOTES_LINE,
12 context::VAGUE_STATUS_LINE, integrity, recovery,
13};
14
15pub const GENERATED_FILES: &[&str] = &[
17 ".no_agent_commit",
18 ".agent/PLAN.md",
19 ".agent/commit-message.txt",
20 ".agent/checkpoint.json.tmp",
21];
22
23pub fn file_contains_marker(file_path: &Path, marker: &str) -> io::Result<bool> {
37 if !file_path.exists() {
38 return Ok(false);
39 }
40
41 let file = fs::File::open(file_path)?;
42 let reader = io::BufReader::new(file);
43
44 for line in reader.lines().map_while(Result::ok) {
45 if line.contains(marker) {
46 return Ok(true);
47 }
48 }
49
50 Ok(false)
51}
52
53pub fn ensure_files(isolation_mode: bool) -> io::Result<()> {
60 let agent_dir = Path::new(".agent");
61
62 if let recovery::RecoveryStatus::Unrecoverable(msg) = recovery::auto_repair(agent_dir)? {
65 return Err(io::Error::other(format!(
66 "Failed to repair .agent state: {msg}"
67 )));
68 }
69
70 integrity::check_filesystem_ready(agent_dir)?;
71 fs::create_dir_all(".agent/logs")?;
72
73 if !isolation_mode {
75 overwrite_one_liner(Path::new(".agent/STATUS.md"), VAGUE_STATUS_LINE)?;
78 overwrite_one_liner(Path::new(".agent/NOTES.md"), VAGUE_NOTES_LINE)?;
79 overwrite_one_liner(Path::new(".agent/ISSUES.md"), VAGUE_ISSUES_LINE)?;
80 }
81
82 Ok(())
83}
84
85pub fn delete_plan_file() -> io::Result<()> {
90 let plan_path = Path::new(".agent/PLAN.md");
91 if plan_path.exists() {
92 fs::remove_file(plan_path)?;
93 }
94 Ok(())
95}
96
97pub fn delete_commit_message_file() -> io::Result<()> {
102 let msg_path = Path::new(".agent/commit-message.txt");
103 if msg_path.exists() {
104 fs::remove_file(msg_path)?;
105 }
106 Ok(())
107}
108
109pub fn read_commit_message_file() -> io::Result<String> {
115 let msg_path = Path::new(".agent/commit-message.txt");
116 if msg_path.exists() && !integrity::verify_file_not_corrupted(msg_path)? {
117 return Err(io::Error::new(
118 io::ErrorKind::InvalidData,
119 ".agent/commit-message.txt appears corrupted",
120 ));
121 }
122 let content = fs::read_to_string(msg_path).map_err(|e| {
123 io::Error::new(
124 e.kind(),
125 format!("Failed to read .agent/commit-message.txt: {e}"),
126 )
127 })?;
128 let trimmed = content.trim();
129 if trimmed.is_empty() {
130 return Err(io::Error::new(
131 io::ErrorKind::InvalidData,
132 ".agent/commit-message.txt is empty",
133 ));
134 }
135 Ok(trimmed.to_string())
136}
137
138pub fn write_commit_message_file(message: &str) -> io::Result<()> {
151 let msg_path = Path::new(".agent/commit-message.txt");
152 if let Some(parent) = msg_path.parent() {
153 fs::create_dir_all(parent)?;
154 }
155 integrity::write_file_atomic(msg_path, message)?;
156 Ok(())
157}
158
159pub fn cleanup_generated_files() {
168 for file in GENERATED_FILES {
169 let _ = fs::remove_file(file);
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use tempfile::TempDir;
177 use test_helpers::with_temp_cwd;
178
179 #[test]
180 fn test_file_contains_marker() {
181 let dir = TempDir::new().unwrap();
182 let file_path = dir.path().join("test.txt");
183 fs::write(&file_path, "line1\nMARKER_TEST\nline3").unwrap();
184
185 assert!(file_contains_marker(&file_path, "MARKER_TEST").unwrap());
186 assert!(!file_contains_marker(&file_path, "NONEXISTENT").unwrap());
187 }
188
189 #[test]
190 fn test_file_contains_marker_missing() {
191 let result = file_contains_marker(Path::new("/nonexistent/file.txt"), "MARKER");
192 assert!(!result.unwrap());
193 }
194
195 #[test]
196 fn test_delete_plan_file() {
197 let dir = TempDir::new().unwrap();
198 let agent_dir = dir.path().join(".agent");
199 fs::create_dir_all(&agent_dir).unwrap();
200 let plan_path = agent_dir.join("PLAN.md");
201 fs::write(&plan_path, "test plan").unwrap();
202 assert!(plan_path.exists());
203
204 fs::remove_file(&plan_path).unwrap();
206 assert!(!plan_path.exists());
207 }
208
209 #[test]
210 fn test_read_commit_message_file() {
211 with_temp_cwd(|_dir| {
212 fs::create_dir_all(".agent").unwrap();
213 fs::write(".agent/commit-message.txt", "feat: test commit\n").unwrap();
214
215 let msg = read_commit_message_file().unwrap();
216 assert_eq!(msg, "feat: test commit");
217 });
218 }
219
220 #[test]
221 fn test_read_commit_message_file_empty() {
222 with_temp_cwd(|_dir| {
223 fs::create_dir_all(".agent").unwrap();
224 fs::write(".agent/commit-message.txt", " \n").unwrap();
225 assert!(read_commit_message_file().is_err());
226 });
227 }
228
229 #[test]
230 fn test_ensure_files_isolation_mode() {
231 with_temp_cwd(|_dir| {
232 ensure_files(true).unwrap();
233
234 assert!(!Path::new("PROMPT.md").exists());
236
237 assert!(!Path::new(".agent/STATUS.md").exists());
239 assert!(!Path::new(".agent/NOTES.md").exists());
240 assert!(!Path::new(".agent/ISSUES.md").exists());
241 });
242 }
243
244 #[test]
245 fn test_ensure_files_non_isolation_mode() {
246 with_temp_cwd(|_dir| {
247 ensure_files(false).unwrap();
248
249 assert!(!Path::new("PROMPT.md").exists());
251 assert!(Path::new(".agent/STATUS.md").exists());
252 assert!(Path::new(".agent/NOTES.md").exists());
253 assert!(Path::new(".agent/ISSUES.md").exists());
254 });
255 }
256}