1use std::fs;
41use std::io;
42use std::path::{Path, PathBuf};
43
44#[derive(Debug, Clone)]
52pub struct DirEntry {
53 path: PathBuf,
55 is_file: bool,
57 is_dir: bool,
59 modified: Option<std::time::SystemTime>,
61}
62
63impl DirEntry {
64 pub fn new(path: PathBuf, is_file: bool, is_dir: bool) -> Self {
66 Self {
67 path,
68 is_file,
69 is_dir,
70 modified: None,
71 }
72 }
73
74 pub fn with_modified(
76 path: PathBuf,
77 is_file: bool,
78 is_dir: bool,
79 modified: std::time::SystemTime,
80 ) -> Self {
81 Self {
82 path,
83 is_file,
84 is_dir,
85 modified: Some(modified),
86 }
87 }
88
89 pub fn path(&self) -> &Path {
91 &self.path
92 }
93
94 pub fn is_file(&self) -> bool {
96 self.is_file
97 }
98
99 pub fn is_dir(&self) -> bool {
101 self.is_dir
102 }
103
104 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
106 self.path.file_name()
107 }
108
109 pub fn modified(&self) -> Option<std::time::SystemTime> {
111 self.modified
112 }
113}
114
115pub trait Workspace: Send + Sync {
124 fn root(&self) -> &Path;
126
127 fn read(&self, relative: &Path) -> io::Result<String>;
133
134 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>>;
136
137 fn write(&self, relative: &Path, content: &str) -> io::Result<()>;
140
141 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()>;
144
145 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()>;
148
149 fn exists(&self, relative: &Path) -> bool;
151
152 fn is_file(&self, relative: &Path) -> bool;
154
155 fn is_dir(&self, relative: &Path) -> bool;
157
158 fn remove(&self, relative: &Path) -> io::Result<()>;
160
161 fn remove_if_exists(&self, relative: &Path) -> io::Result<()>;
163
164 fn remove_dir_all(&self, relative: &Path) -> io::Result<()>;
169
170 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()>;
172
173 fn create_dir_all(&self, relative: &Path) -> io::Result<()>;
175
176 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>>;
182
183 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
188
189 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()>;
214
215 fn set_readonly(&self, relative: &Path) -> io::Result<()>;
225
226 fn set_writable(&self, relative: &Path) -> io::Result<()>;
235
236 fn absolute(&self, relative: &Path) -> PathBuf {
242 self.root().join(relative)
243 }
244
245 fn absolute_str(&self, relative: &str) -> String {
247 self.root().join(relative).display().to_string()
248 }
249
250 fn agent_dir(&self) -> PathBuf {
256 self.root().join(".agent")
257 }
258
259 fn agent_logs(&self) -> PathBuf {
261 self.root().join(".agent/logs")
262 }
263
264 fn agent_tmp(&self) -> PathBuf {
266 self.root().join(".agent/tmp")
267 }
268
269 fn plan_md(&self) -> PathBuf {
271 self.root().join(".agent/PLAN.md")
272 }
273
274 fn issues_md(&self) -> PathBuf {
276 self.root().join(".agent/ISSUES.md")
277 }
278
279 fn status_md(&self) -> PathBuf {
281 self.root().join(".agent/STATUS.md")
282 }
283
284 fn notes_md(&self) -> PathBuf {
286 self.root().join(".agent/NOTES.md")
287 }
288
289 fn commit_message(&self) -> PathBuf {
291 self.root().join(".agent/commit-message.txt")
292 }
293
294 fn checkpoint(&self) -> PathBuf {
296 self.root().join(".agent/checkpoint.json")
297 }
298
299 fn start_commit(&self) -> PathBuf {
301 self.root().join(".agent/start_commit")
302 }
303
304 fn review_baseline(&self) -> PathBuf {
306 self.root().join(".agent/review_baseline.txt")
307 }
308
309 fn prompt_md(&self) -> PathBuf {
311 self.root().join("PROMPT.md")
312 }
313
314 fn prompt_backup(&self) -> PathBuf {
316 self.root().join(".agent/PROMPT.md.backup")
317 }
318
319 fn agent_config(&self) -> PathBuf {
321 self.root().join(".agent/config.toml")
322 }
323
324 fn agents_toml(&self) -> PathBuf {
326 self.root().join(".agent/agents.toml")
327 }
328
329 fn pipeline_log(&self) -> PathBuf {
331 self.root().join(".agent/logs/pipeline.log")
332 }
333
334 fn xsd_path(&self, name: &str) -> PathBuf {
336 self.root().join(format!(".agent/tmp/{}.xsd", name))
337 }
338
339 fn xml_path(&self, name: &str) -> PathBuf {
341 self.root().join(format!(".agent/tmp/{}.xml", name))
342 }
343
344 fn log_path(&self, name: &str) -> PathBuf {
346 self.root().join(format!(".agent/logs/{}", name))
347 }
348}
349
350#[derive(Debug, Clone)]
358pub struct WorkspaceFs {
359 root: PathBuf,
360}
361
362impl WorkspaceFs {
363 pub fn new(repo_root: PathBuf) -> Self {
369 Self { root: repo_root }
370 }
371}
372
373impl Workspace for WorkspaceFs {
374 fn root(&self) -> &Path {
375 &self.root
376 }
377
378 fn read(&self, relative: &Path) -> io::Result<String> {
379 fs::read_to_string(self.root.join(relative))
380 }
381
382 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
383 fs::read(self.root.join(relative))
384 }
385
386 fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
387 let path = self.root.join(relative);
388 if let Some(parent) = path.parent() {
389 fs::create_dir_all(parent)?;
390 }
391 fs::write(path, content)
392 }
393
394 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
395 let path = self.root.join(relative);
396 if let Some(parent) = path.parent() {
397 fs::create_dir_all(parent)?;
398 }
399 fs::write(path, content)
400 }
401
402 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
403 use std::io::Write;
404 let path = self.root.join(relative);
405 if let Some(parent) = path.parent() {
406 fs::create_dir_all(parent)?;
407 }
408 let mut file = fs::OpenOptions::new()
409 .create(true)
410 .append(true)
411 .open(path)?;
412 file.write_all(content)?;
413 file.flush()
414 }
415
416 fn exists(&self, relative: &Path) -> bool {
417 self.root.join(relative).exists()
418 }
419
420 fn is_file(&self, relative: &Path) -> bool {
421 self.root.join(relative).is_file()
422 }
423
424 fn is_dir(&self, relative: &Path) -> bool {
425 self.root.join(relative).is_dir()
426 }
427
428 fn remove(&self, relative: &Path) -> io::Result<()> {
429 fs::remove_file(self.root.join(relative))
430 }
431
432 fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
433 let path = self.root.join(relative);
434 if path.exists() {
435 fs::remove_file(path)?;
436 }
437 Ok(())
438 }
439
440 fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
441 fs::remove_dir_all(self.root.join(relative))
442 }
443
444 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
445 let path = self.root.join(relative);
446 if path.exists() {
447 fs::remove_dir_all(path)?;
448 }
449 Ok(())
450 }
451
452 fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
453 fs::create_dir_all(self.root.join(relative))
454 }
455
456 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
457 let abs_path = self.root.join(relative);
458 let mut entries = Vec::new();
459 for entry in fs::read_dir(abs_path)? {
460 let entry = entry?;
461 let metadata = entry.metadata()?;
462 let rel_path = relative.join(entry.file_name());
464 let modified = metadata.modified().ok();
465 if let Some(mod_time) = modified {
466 entries.push(DirEntry::with_modified(
467 rel_path,
468 metadata.is_file(),
469 metadata.is_dir(),
470 mod_time,
471 ));
472 } else {
473 entries.push(DirEntry::new(
474 rel_path,
475 metadata.is_file(),
476 metadata.is_dir(),
477 ));
478 }
479 }
480 Ok(entries)
481 }
482
483 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
484 fs::rename(self.root.join(from), self.root.join(to))
485 }
486
487 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
488 use std::io::Write;
489 use tempfile::NamedTempFile;
490
491 let path = self.root.join(relative);
492
493 if let Some(parent) = path.parent() {
495 fs::create_dir_all(parent)?;
496 }
497
498 let parent_dir = path.parent().unwrap_or_else(|| Path::new("."));
501 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
502
503 #[cfg(unix)]
506 {
507 use std::os::unix::fs::PermissionsExt;
508 let mut perms = fs::metadata(temp_file.path())?.permissions();
509 perms.set_mode(0o600);
510 fs::set_permissions(temp_file.path(), perms)?;
511 }
512
513 temp_file.write_all(content.as_bytes())?;
515 temp_file.flush()?;
516 temp_file.as_file().sync_all()?;
517
518 temp_file.persist(&path).map_err(|e| e.error)?;
520
521 Ok(())
522 }
523
524 fn set_readonly(&self, relative: &Path) -> io::Result<()> {
525 let path = self.root.join(relative);
526 if !path.exists() {
527 return Ok(());
528 }
529
530 let metadata = fs::metadata(&path)?;
531 let mut perms = metadata.permissions();
532
533 #[cfg(unix)]
534 {
535 use std::os::unix::fs::PermissionsExt;
536 perms.set_mode(0o444);
537 }
538
539 #[cfg(windows)]
540 {
541 perms.set_readonly(true);
542 }
543
544 fs::set_permissions(path, perms)
545 }
546
547 fn set_writable(&self, relative: &Path) -> io::Result<()> {
548 let path = self.root.join(relative);
549 if !path.exists() {
550 return Ok(());
551 }
552
553 let metadata = fs::metadata(&path)?;
554 let mut perms = metadata.permissions();
555
556 #[cfg(unix)]
557 {
558 use std::os::unix::fs::PermissionsExt;
559 perms.set_mode(0o644);
560 }
561
562 #[cfg(windows)]
563 {
564 perms.set_readonly(false);
565 }
566
567 fs::set_permissions(path, perms)
568 }
569}
570
571#[cfg(any(test, feature = "test-utils"))]
577#[derive(Debug, Clone)]
578struct MemoryFile {
579 content: Vec<u8>,
580 modified: std::time::SystemTime,
581}
582
583#[cfg(any(test, feature = "test-utils"))]
584impl MemoryFile {
585 fn new(content: Vec<u8>) -> Self {
586 Self {
587 content,
588 modified: std::time::SystemTime::now(),
589 }
590 }
591
592 fn with_modified(content: Vec<u8>, modified: std::time::SystemTime) -> Self {
593 Self { content, modified }
594 }
595}
596
597#[cfg(any(test, feature = "test-utils"))]
605#[derive(Debug)]
606pub struct MemoryWorkspace {
607 root: PathBuf,
608 files: std::sync::RwLock<std::collections::HashMap<PathBuf, MemoryFile>>,
609 directories: std::sync::RwLock<std::collections::HashSet<PathBuf>>,
610}
611
612#[cfg(any(test, feature = "test-utils"))]
613impl MemoryWorkspace {
614 pub fn new(root: PathBuf) -> Self {
618 Self {
619 root,
620 files: std::sync::RwLock::new(std::collections::HashMap::new()),
621 directories: std::sync::RwLock::new(std::collections::HashSet::new()),
622 }
623 }
624
625 pub fn new_test() -> Self {
627 Self::new(PathBuf::from("/test/repo"))
628 }
629
630 pub fn with_file(self, path: &str, content: &str) -> Self {
634 let path_buf = PathBuf::from(path);
635 if let Some(parent) = path_buf.parent() {
637 let mut dirs = self.directories.write().unwrap();
638 let mut current = PathBuf::new();
639 for component in parent.components() {
640 current.push(component);
641 dirs.insert(current.clone());
642 }
643 }
644 self.files
645 .write()
646 .unwrap()
647 .insert(path_buf, MemoryFile::new(content.as_bytes().to_vec()));
648 self
649 }
650
651 pub fn with_file_at_time(
655 self,
656 path: &str,
657 content: &str,
658 modified: std::time::SystemTime,
659 ) -> Self {
660 let path_buf = PathBuf::from(path);
661 if let Some(parent) = path_buf.parent() {
663 let mut dirs = self.directories.write().unwrap();
664 let mut current = PathBuf::new();
665 for component in parent.components() {
666 current.push(component);
667 dirs.insert(current.clone());
668 }
669 }
670 self.files.write().unwrap().insert(
671 path_buf,
672 MemoryFile::with_modified(content.as_bytes().to_vec(), modified),
673 );
674 self
675 }
676
677 pub fn with_file_bytes(self, path: &str, content: &[u8]) -> Self {
681 let path_buf = PathBuf::from(path);
682 if let Some(parent) = path_buf.parent() {
684 let mut dirs = self.directories.write().unwrap();
685 let mut current = PathBuf::new();
686 for component in parent.components() {
687 current.push(component);
688 dirs.insert(current.clone());
689 }
690 }
691 self.files
692 .write()
693 .unwrap()
694 .insert(path_buf, MemoryFile::new(content.to_vec()));
695 self
696 }
697
698 pub fn with_dir(self, path: &str) -> Self {
700 let path_buf = PathBuf::from(path);
701 {
702 let mut dirs = self.directories.write().unwrap();
703 let mut current = PathBuf::new();
704 for component in path_buf.components() {
705 current.push(component);
706 dirs.insert(current.clone());
707 }
708 }
709 self
710 }
711
712 pub fn list_files_in_dir(&self, dir: &str) -> Vec<PathBuf> {
716 let dir_path = PathBuf::from(dir);
717 self.files
718 .read()
719 .unwrap()
720 .keys()
721 .filter(|path| {
722 path.parent()
723 .map(|p| p == dir_path || p.starts_with(&dir_path))
724 .unwrap_or(false)
725 })
726 .cloned()
727 .collect()
728 }
729
730 pub fn get_modified(&self, path: &str) -> Option<std::time::SystemTime> {
732 self.files
733 .read()
734 .unwrap()
735 .get(&PathBuf::from(path))
736 .map(|f| f.modified)
737 }
738
739 pub fn list_directories(&self) -> Vec<PathBuf> {
741 self.directories.read().unwrap().iter().cloned().collect()
742 }
743
744 pub fn written_files(&self) -> std::collections::HashMap<PathBuf, Vec<u8>> {
746 self.files
747 .read()
748 .unwrap()
749 .iter()
750 .map(|(k, v)| (k.clone(), v.content.clone()))
751 .collect()
752 }
753
754 pub fn get_file(&self, path: &str) -> Option<String> {
756 self.files
757 .read()
758 .unwrap()
759 .get(&PathBuf::from(path))
760 .map(|f| String::from_utf8_lossy(&f.content).to_string())
761 }
762
763 pub fn get_file_bytes(&self, path: &str) -> Option<Vec<u8>> {
765 self.files
766 .read()
767 .unwrap()
768 .get(&PathBuf::from(path))
769 .map(|f| f.content.clone())
770 }
771
772 pub fn was_written(&self, path: &str) -> bool {
774 self.files
775 .read()
776 .unwrap()
777 .contains_key(&PathBuf::from(path))
778 }
779
780 pub fn clear(&self) {
782 self.files.write().unwrap().clear();
783 self.directories.write().unwrap().clear();
784 }
785}
786
787#[cfg(any(test, feature = "test-utils"))]
788impl Workspace for MemoryWorkspace {
789 fn root(&self) -> &Path {
790 &self.root
791 }
792
793 fn read(&self, relative: &Path) -> io::Result<String> {
794 self.files
795 .read()
796 .unwrap()
797 .get(relative)
798 .map(|f| String::from_utf8_lossy(&f.content).to_string())
799 .ok_or_else(|| {
800 io::Error::new(
801 io::ErrorKind::NotFound,
802 format!("File not found: {}", relative.display()),
803 )
804 })
805 }
806
807 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
808 self.files
809 .read()
810 .unwrap()
811 .get(relative)
812 .map(|f| f.content.clone())
813 .ok_or_else(|| {
814 io::Error::new(
815 io::ErrorKind::NotFound,
816 format!("File not found: {}", relative.display()),
817 )
818 })
819 }
820
821 fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
822 if let Some(parent) = relative.parent() {
824 let mut dirs = self.directories.write().unwrap();
825 let mut current = PathBuf::new();
826 for component in parent.components() {
827 current.push(component);
828 dirs.insert(current.clone());
829 }
830 }
831 self.files.write().unwrap().insert(
832 relative.to_path_buf(),
833 MemoryFile::new(content.as_bytes().to_vec()),
834 );
835 Ok(())
836 }
837
838 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
839 if let Some(parent) = relative.parent() {
841 let mut dirs = self.directories.write().unwrap();
842 let mut current = PathBuf::new();
843 for component in parent.components() {
844 current.push(component);
845 dirs.insert(current.clone());
846 }
847 }
848 self.files
849 .write()
850 .unwrap()
851 .insert(relative.to_path_buf(), MemoryFile::new(content.to_vec()));
852 Ok(())
853 }
854
855 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
856 if let Some(parent) = relative.parent() {
858 let mut dirs = self.directories.write().unwrap();
859 let mut current = PathBuf::new();
860 for component in parent.components() {
861 current.push(component);
862 dirs.insert(current.clone());
863 }
864 }
865 let mut files = self.files.write().unwrap();
866 let entry = files
867 .entry(relative.to_path_buf())
868 .or_insert_with(|| MemoryFile::new(Vec::new()));
869 entry.content.extend_from_slice(content);
870 entry.modified = std::time::SystemTime::now();
871 Ok(())
872 }
873
874 fn exists(&self, relative: &Path) -> bool {
875 self.files.read().unwrap().contains_key(relative)
876 || self.directories.read().unwrap().contains(relative)
877 }
878
879 fn is_file(&self, relative: &Path) -> bool {
880 self.files.read().unwrap().contains_key(relative)
881 }
882
883 fn is_dir(&self, relative: &Path) -> bool {
884 self.directories.read().unwrap().contains(relative)
885 }
886
887 fn remove(&self, relative: &Path) -> io::Result<()> {
888 self.files
889 .write()
890 .unwrap()
891 .remove(relative)
892 .map(|_| ())
893 .ok_or_else(|| {
894 io::Error::new(
895 io::ErrorKind::NotFound,
896 format!("File not found: {}", relative.display()),
897 )
898 })
899 }
900
901 fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
902 self.files.write().unwrap().remove(relative);
903 Ok(())
904 }
905
906 fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
907 if !self.directories.read().unwrap().contains(relative) {
909 return Err(io::Error::new(
910 io::ErrorKind::NotFound,
911 format!("Directory not found: {}", relative.display()),
912 ));
913 }
914 self.remove_dir_all_if_exists(relative)
915 }
916
917 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
918 {
920 let mut files = self.files.write().unwrap();
921 let to_remove: Vec<PathBuf> = files
922 .keys()
923 .filter(|path| path.starts_with(relative))
924 .cloned()
925 .collect();
926 for path in to_remove {
927 files.remove(&path);
928 }
929 }
930 {
932 let mut dirs = self.directories.write().unwrap();
933 let to_remove: Vec<PathBuf> = dirs
934 .iter()
935 .filter(|path| path.starts_with(relative) || *path == relative)
936 .cloned()
937 .collect();
938 for path in to_remove {
939 dirs.remove(&path);
940 }
941 }
942 Ok(())
943 }
944
945 fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
946 let mut dirs = self.directories.write().unwrap();
947 let mut current = PathBuf::new();
948 for component in relative.components() {
949 current.push(component);
950 dirs.insert(current.clone());
951 }
952 Ok(())
953 }
954
955 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
956 let files = self.files.read().unwrap();
957 let dirs = self.directories.read().unwrap();
958
959 if !relative.as_os_str().is_empty() && !dirs.contains(relative) {
961 return Err(io::Error::new(
962 io::ErrorKind::NotFound,
963 format!("Directory not found: {}", relative.display()),
964 ));
965 }
966
967 let mut entries = Vec::new();
968 let mut seen = std::collections::HashSet::new();
969
970 for (path, mem_file) in files.iter() {
972 if let Some(parent) = path.parent() {
973 if parent == relative {
974 if let Some(name) = path.file_name() {
975 if seen.insert(name.to_os_string()) {
976 entries.push(DirEntry::with_modified(
977 path.clone(),
978 true,
979 false,
980 mem_file.modified,
981 ));
982 }
983 }
984 }
985 }
986 }
987
988 for dir_path in dirs.iter() {
990 if let Some(parent) = dir_path.parent() {
991 if parent == relative {
992 if let Some(name) = dir_path.file_name() {
993 if seen.insert(name.to_os_string()) {
994 entries.push(DirEntry::new(dir_path.clone(), false, true));
995 }
996 }
997 }
998 }
999 }
1000
1001 Ok(entries)
1002 }
1003
1004 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1005 let mut files = self.files.write().unwrap();
1006 if let Some(file) = files.remove(from) {
1007 if let Some(parent) = to.parent() {
1009 let mut dirs = self.directories.write().unwrap();
1010 let mut current = PathBuf::new();
1011 for component in parent.components() {
1012 current.push(component);
1013 dirs.insert(current.clone());
1014 }
1015 }
1016 files.insert(to.to_path_buf(), file);
1017 Ok(())
1018 } else {
1019 Err(io::Error::new(
1020 io::ErrorKind::NotFound,
1021 format!("File not found: {}", from.display()),
1022 ))
1023 }
1024 }
1025
1026 fn set_readonly(&self, _relative: &Path) -> io::Result<()> {
1027 Ok(())
1029 }
1030
1031 fn set_writable(&self, _relative: &Path) -> io::Result<()> {
1032 Ok(())
1034 }
1035
1036 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
1037 self.write(relative, content)
1040 }
1041}
1042
1043#[cfg(any(test, feature = "test-utils"))]
1044impl Clone for MemoryWorkspace {
1045 fn clone(&self) -> Self {
1046 Self {
1047 root: self.root.clone(),
1048 files: std::sync::RwLock::new(self.files.read().unwrap().clone()),
1049 directories: std::sync::RwLock::new(self.directories.read().unwrap().clone()),
1050 }
1051 }
1052}
1053
1054#[cfg(test)]
1059mod tests {
1060 use super::*;
1061
1062 #[test]
1067 fn test_workspace_fs_root() {
1068 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1069 assert_eq!(ws.root(), Path::new("/test/repo"));
1070 }
1071
1072 #[test]
1073 fn test_workspace_fs_agent_paths() {
1074 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1075
1076 assert_eq!(ws.agent_dir(), PathBuf::from("/test/repo/.agent"));
1077 assert_eq!(ws.agent_logs(), PathBuf::from("/test/repo/.agent/logs"));
1078 assert_eq!(ws.agent_tmp(), PathBuf::from("/test/repo/.agent/tmp"));
1079 assert_eq!(ws.plan_md(), PathBuf::from("/test/repo/.agent/PLAN.md"));
1080 assert_eq!(ws.issues_md(), PathBuf::from("/test/repo/.agent/ISSUES.md"));
1081 assert_eq!(
1082 ws.commit_message(),
1083 PathBuf::from("/test/repo/.agent/commit-message.txt")
1084 );
1085 assert_eq!(
1086 ws.checkpoint(),
1087 PathBuf::from("/test/repo/.agent/checkpoint.json")
1088 );
1089 assert_eq!(
1090 ws.start_commit(),
1091 PathBuf::from("/test/repo/.agent/start_commit")
1092 );
1093 assert_eq!(ws.prompt_md(), PathBuf::from("/test/repo/PROMPT.md"));
1094 }
1095
1096 #[test]
1097 fn test_workspace_fs_dynamic_paths() {
1098 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1099
1100 assert_eq!(
1101 ws.xsd_path("plan"),
1102 PathBuf::from("/test/repo/.agent/tmp/plan.xsd")
1103 );
1104 assert_eq!(
1105 ws.xml_path("issues"),
1106 PathBuf::from("/test/repo/.agent/tmp/issues.xml")
1107 );
1108 assert_eq!(
1109 ws.log_path("agent.log"),
1110 PathBuf::from("/test/repo/.agent/logs/agent.log")
1111 );
1112 }
1113
1114 #[test]
1115 fn test_workspace_fs_absolute() {
1116 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1117
1118 let abs = ws.absolute(Path::new(".agent/tmp/plan.xml"));
1119 assert_eq!(abs, PathBuf::from("/test/repo/.agent/tmp/plan.xml"));
1120
1121 let abs_str = ws.absolute_str(".agent/tmp/plan.xml");
1122 assert_eq!(abs_str, "/test/repo/.agent/tmp/plan.xml");
1123 }
1124
1125 #[test]
1130 fn test_memory_workspace_read_write() {
1131 let ws = MemoryWorkspace::new_test();
1132
1133 ws.write(Path::new(".agent/test.txt"), "hello").unwrap();
1134 assert_eq!(ws.read(Path::new(".agent/test.txt")).unwrap(), "hello");
1135 assert!(ws.was_written(".agent/test.txt"));
1136 }
1137
1138 #[test]
1139 fn test_memory_workspace_with_file() {
1140 let ws = MemoryWorkspace::new_test().with_file("existing.txt", "pre-existing content");
1141
1142 assert_eq!(
1143 ws.read(Path::new("existing.txt")).unwrap(),
1144 "pre-existing content"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_memory_workspace_exists() {
1150 let ws = MemoryWorkspace::new_test().with_file("exists.txt", "content");
1151
1152 assert!(ws.exists(Path::new("exists.txt")));
1153 assert!(!ws.exists(Path::new("not_exists.txt")));
1154 }
1155
1156 #[test]
1157 fn test_memory_workspace_remove() {
1158 let ws = MemoryWorkspace::new_test().with_file("to_delete.txt", "content");
1159
1160 assert!(ws.exists(Path::new("to_delete.txt")));
1161 ws.remove(Path::new("to_delete.txt")).unwrap();
1162 assert!(!ws.exists(Path::new("to_delete.txt")));
1163 }
1164
1165 #[test]
1166 fn test_memory_workspace_read_nonexistent_fails() {
1167 let ws = MemoryWorkspace::new_test();
1168
1169 let result = ws.read(Path::new("nonexistent.txt"));
1170 assert!(result.is_err());
1171 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
1172 }
1173
1174 #[test]
1175 fn test_memory_workspace_written_files() {
1176 let ws = MemoryWorkspace::new_test();
1177
1178 ws.write(Path::new("file1.txt"), "content1").unwrap();
1179 ws.write(Path::new("file2.txt"), "content2").unwrap();
1180
1181 let files = ws.written_files();
1182 assert_eq!(files.len(), 2);
1183 assert_eq!(
1184 String::from_utf8_lossy(files.get(&PathBuf::from("file1.txt")).unwrap()),
1185 "content1"
1186 );
1187 }
1188
1189 #[test]
1190 fn test_memory_workspace_get_file() {
1191 let ws = MemoryWorkspace::new_test();
1192
1193 ws.write(Path::new("test.txt"), "test content").unwrap();
1194 assert_eq!(ws.get_file("test.txt"), Some("test content".to_string()));
1195 assert_eq!(ws.get_file("nonexistent.txt"), None);
1196 }
1197
1198 #[test]
1199 fn test_memory_workspace_clear() {
1200 let ws = MemoryWorkspace::new_test().with_file("file.txt", "content");
1201
1202 assert!(ws.exists(Path::new("file.txt")));
1203 ws.clear();
1204 assert!(!ws.exists(Path::new("file.txt")));
1205 }
1206
1207 #[test]
1208 fn test_memory_workspace_absolute_str() {
1209 let ws = MemoryWorkspace::new_test();
1210
1211 assert_eq!(
1212 ws.absolute_str(".agent/tmp/commit_message.xml"),
1213 "/test/repo/.agent/tmp/commit_message.xml"
1214 );
1215 }
1216
1217 #[test]
1218 fn test_memory_workspace_creates_parent_dirs() {
1219 let ws = MemoryWorkspace::new_test();
1220
1221 ws.write(Path::new("a/b/c/file.txt"), "content").unwrap();
1222
1223 assert!(ws.is_dir(Path::new("a")));
1225 assert!(ws.is_dir(Path::new("a/b")));
1226 assert!(ws.is_dir(Path::new("a/b/c")));
1227 assert!(ws.is_file(Path::new("a/b/c/file.txt")));
1228 }
1229
1230 #[test]
1231 fn test_memory_workspace_rename() {
1232 let ws = MemoryWorkspace::new_test().with_file("old.txt", "content");
1233
1234 ws.rename(Path::new("old.txt"), Path::new("new.txt"))
1235 .unwrap();
1236
1237 assert!(!ws.exists(Path::new("old.txt")));
1238 assert!(ws.exists(Path::new("new.txt")));
1239 assert_eq!(ws.read(Path::new("new.txt")).unwrap(), "content");
1240 }
1241
1242 #[test]
1243 fn test_memory_workspace_rename_creates_parent_dirs() {
1244 let ws = MemoryWorkspace::new_test().with_file("old.txt", "content");
1245
1246 ws.rename(Path::new("old.txt"), Path::new("a/b/new.txt"))
1247 .unwrap();
1248
1249 assert!(!ws.exists(Path::new("old.txt")));
1250 assert!(ws.is_dir(Path::new("a")));
1251 assert!(ws.is_dir(Path::new("a/b")));
1252 assert!(ws.exists(Path::new("a/b/new.txt")));
1253 }
1254
1255 #[test]
1256 fn test_memory_workspace_rename_nonexistent_fails() {
1257 let ws = MemoryWorkspace::new_test();
1258
1259 let result = ws.rename(Path::new("nonexistent.txt"), Path::new("new.txt"));
1260 assert!(result.is_err());
1261 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
1262 }
1263
1264 #[test]
1265 fn test_memory_workspace_set_readonly_noop() {
1266 let ws = MemoryWorkspace::new_test().with_file("test.txt", "content");
1268
1269 ws.set_readonly(Path::new("test.txt")).unwrap();
1271 ws.set_writable(Path::new("test.txt")).unwrap();
1272
1273 assert_eq!(ws.read(Path::new("test.txt")).unwrap(), "content");
1275 }
1276
1277 #[test]
1278 fn test_memory_workspace_write_atomic() {
1279 let ws = MemoryWorkspace::new_test();
1280
1281 ws.write_atomic(Path::new("atomic.txt"), "atomic content")
1282 .unwrap();
1283
1284 assert_eq!(ws.read(Path::new("atomic.txt")).unwrap(), "atomic content");
1285 }
1286
1287 #[test]
1288 fn test_memory_workspace_write_atomic_creates_parent_dirs() {
1289 let ws = MemoryWorkspace::new_test();
1290
1291 ws.write_atomic(Path::new("a/b/c/atomic.txt"), "nested atomic")
1292 .unwrap();
1293
1294 assert!(ws.is_dir(Path::new("a")));
1295 assert!(ws.is_dir(Path::new("a/b")));
1296 assert!(ws.is_dir(Path::new("a/b/c")));
1297 assert_eq!(
1298 ws.read(Path::new("a/b/c/atomic.txt")).unwrap(),
1299 "nested atomic"
1300 );
1301 }
1302
1303 #[test]
1304 fn test_memory_workspace_write_atomic_overwrites() {
1305 let ws = MemoryWorkspace::new_test().with_file("existing.txt", "old content");
1306
1307 ws.write_atomic(Path::new("existing.txt"), "new content")
1308 .unwrap();
1309
1310 assert_eq!(ws.read(Path::new("existing.txt")).unwrap(), "new content");
1311 }
1312}