ralph_workflow/config/
path_resolver.rs1use std::collections::HashMap;
37use std::io;
38use std::path::{Path, PathBuf};
39use std::sync::{Arc, RwLock};
40
41pub trait ConfigEnvironment: Send + Sync {
51 fn unified_config_path(&self) -> Option<PathBuf>;
58
59 fn local_config_path(&self) -> Option<PathBuf> {
66 Some(PathBuf::from(".agent/ralph-workflow.toml"))
67 }
68
69 fn prompt_path(&self) -> PathBuf {
74 PathBuf::from("PROMPT.md")
75 }
76
77 fn file_exists(&self, path: &Path) -> bool;
79
80 fn read_file(&self, path: &Path) -> io::Result<String>;
82
83 fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
85
86 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
88
89 fn worktree_root(&self) -> Option<PathBuf> {
95 None }
97}
98
99#[derive(Debug, Default, Clone, Copy)]
105pub struct RealConfigEnvironment;
106
107impl ConfigEnvironment for RealConfigEnvironment {
108 fn unified_config_path(&self) -> Option<PathBuf> {
109 super::unified::unified_config_path()
110 }
111
112 fn file_exists(&self, path: &Path) -> bool {
113 path.exists()
114 }
115
116 fn read_file(&self, path: &Path) -> io::Result<String> {
117 std::fs::read_to_string(path)
118 }
119
120 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
121 if let Some(parent) = path.parent() {
122 std::fs::create_dir_all(parent)?;
123 }
124 std::fs::write(path, content)
125 }
126
127 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
128 std::fs::create_dir_all(path)
129 }
130
131 fn worktree_root(&self) -> Option<PathBuf> {
132 git2::Repository::discover(".")
133 .ok()
134 .and_then(|repo| repo.workdir().map(PathBuf::from))
135 }
136
137 fn local_config_path(&self) -> Option<PathBuf> {
138 self.worktree_root()
140 .map(|root| root.join(".agent/ralph-workflow.toml"))
141 .or_else(|| Some(PathBuf::from(".agent/ralph-workflow.toml")))
142 }
143}
144
145#[derive(Debug, Clone, Default)]
169pub struct MemoryConfigEnvironment {
170 unified_config_path: Option<PathBuf>,
171 prompt_path: Option<PathBuf>,
172 local_config_path: Option<PathBuf>,
173 worktree_root: Option<PathBuf>,
174 files: Arc<RwLock<HashMap<PathBuf, String>>>,
176 dirs: Arc<RwLock<std::collections::HashSet<PathBuf>>>,
178}
179
180impl MemoryConfigEnvironment {
181 pub fn new() -> Self {
183 Self::default()
184 }
185
186 #[must_use]
188 pub fn with_unified_config_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
189 self.unified_config_path = Some(path.into());
190 self
191 }
192
193 #[must_use]
195 pub fn with_local_config_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
196 self.local_config_path = Some(path.into());
197 self
198 }
199
200 #[must_use]
202 pub fn with_prompt_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
203 self.prompt_path = Some(path.into());
204 self
205 }
206
207 #[must_use]
209 pub fn with_file<P: Into<PathBuf>, S: Into<String>>(self, path: P, content: S) -> Self {
210 let path = path.into();
211 self.files.write()
212 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
213 .insert(path, content.into());
214 self
215 }
216
217 #[must_use]
219 pub fn with_worktree_root<P: Into<PathBuf>>(mut self, path: P) -> Self {
220 self.worktree_root = Some(path.into());
221 self
222 }
223
224 pub fn get_file(&self, path: &Path) -> Option<String> {
226 self.files.read()
227 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
228 .get(path).cloned()
229 }
230
231 pub fn was_written(&self, path: &Path) -> bool {
233 self.files.read()
234 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
235 .contains_key(path)
236 }
237}
238
239impl ConfigEnvironment for MemoryConfigEnvironment {
240 fn unified_config_path(&self) -> Option<PathBuf> {
241 self.unified_config_path.clone()
242 }
243
244 fn local_config_path(&self) -> Option<PathBuf> {
245 if let Some(ref path) = self.local_config_path {
247 return Some(path.clone());
248 }
249
250 self.worktree_root()
252 .map(|root| root.join(".agent/ralph-workflow.toml"))
253 .or_else(|| Some(PathBuf::from(".agent/ralph-workflow.toml")))
254 }
255
256 fn prompt_path(&self) -> PathBuf {
257 self.prompt_path
258 .clone()
259 .unwrap_or_else(|| PathBuf::from("PROMPT.md"))
260 }
261
262 fn file_exists(&self, path: &Path) -> bool {
263 self.files.read()
264 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
265 .contains_key(path)
266 }
267
268 fn read_file(&self, path: &Path) -> io::Result<String> {
269 self.files
270 .read()
271 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
272 .get(path)
273 .cloned()
274 .ok_or_else(|| {
275 io::Error::new(
276 io::ErrorKind::NotFound,
277 format!("File not found: {}", path.display()),
278 )
279 })
280 }
281
282 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
283 if let Some(parent) = path.parent() {
285 self.dirs.write()
286 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment dirs lock")
287 .insert(parent.to_path_buf());
288 }
289 self.files
290 .write()
291 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
292 .insert(path.to_path_buf(), content.to_string());
293 Ok(())
294 }
295
296 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
297 self.dirs.write()
298 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment dirs lock")
299 .insert(path.to_path_buf());
300 Ok(())
301 }
302
303 fn worktree_root(&self) -> Option<PathBuf> {
304 self.worktree_root.clone()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_real_environment_returns_path() {
314 let env = RealConfigEnvironment;
315 let path = env.unified_config_path();
317 if let Some(p) = path {
318 assert!(p.to_string_lossy().contains("ralph-workflow.toml"));
319 }
320 }
321
322 #[test]
323 fn test_memory_environment_with_custom_paths() {
324 let env = MemoryConfigEnvironment::new()
325 .with_unified_config_path("/custom/config.toml")
326 .with_prompt_path("/custom/PROMPT.md");
327
328 assert_eq!(
329 env.unified_config_path(),
330 Some(PathBuf::from("/custom/config.toml"))
331 );
332 assert_eq!(env.prompt_path(), PathBuf::from("/custom/PROMPT.md"));
333 }
334
335 #[test]
336 fn test_memory_environment_default_prompt_path() {
337 let env = MemoryConfigEnvironment::new();
338 assert_eq!(env.prompt_path(), PathBuf::from("PROMPT.md"));
339 }
340
341 #[test]
342 fn test_memory_environment_no_unified_config() {
343 let env = MemoryConfigEnvironment::new();
344 assert_eq!(env.unified_config_path(), None);
345 }
346
347 #[test]
348 fn test_memory_environment_file_operations() {
349 let env = MemoryConfigEnvironment::new();
350 let path = Path::new("/test/file.txt");
351
352 assert!(!env.file_exists(path));
354
355 env.write_file(path, "test content").unwrap();
357
358 assert!(env.file_exists(path));
360 assert_eq!(env.read_file(path).unwrap(), "test content");
361 assert!(env.was_written(path));
362 }
363
364 #[test]
365 fn test_memory_environment_with_prepopulated_file() {
366 let env =
367 MemoryConfigEnvironment::new().with_file("/test/existing.txt", "existing content");
368
369 assert!(env.file_exists(Path::new("/test/existing.txt")));
370 assert_eq!(
371 env.read_file(Path::new("/test/existing.txt")).unwrap(),
372 "existing content"
373 );
374 }
375
376 #[test]
377 fn test_memory_environment_read_nonexistent_file() {
378 let env = MemoryConfigEnvironment::new();
379 let result = env.read_file(Path::new("/nonexistent"));
380 assert!(result.is_err());
381 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
382 }
383
384 #[test]
385 fn test_memory_environment_with_worktree_root() {
386 let env = MemoryConfigEnvironment::new().with_worktree_root("/test/worktree");
387
388 assert_eq!(env.worktree_root(), Some(PathBuf::from("/test/worktree")));
389 assert_eq!(
390 env.local_config_path(),
391 Some(PathBuf::from("/test/worktree/.agent/ralph-workflow.toml"))
392 );
393 }
394
395 #[test]
396 fn test_memory_environment_without_worktree_root() {
397 let env = MemoryConfigEnvironment::new();
398
399 assert_eq!(env.worktree_root(), None);
400 assert_eq!(
401 env.local_config_path(),
402 Some(PathBuf::from(".agent/ralph-workflow.toml"))
403 );
404 }
405
406 #[test]
407 fn test_memory_environment_explicit_local_path_overrides_worktree() {
408 let env = MemoryConfigEnvironment::new()
409 .with_worktree_root("/test/worktree")
410 .with_local_config_path("/custom/path/config.toml");
411
412 assert_eq!(
414 env.local_config_path(),
415 Some(PathBuf::from("/custom/path/config.toml"))
416 );
417 }
418}