1pub const AGENT_DIR: &str = ".agent";
46
47pub const AGENT_TMP: &str = ".agent/tmp";
49
50pub const AGENT_LOGS: &str = ".agent/logs";
52
53pub const PLAN_MD: &str = ".agent/PLAN.md";
55
56pub const ISSUES_MD: &str = ".agent/ISSUES.md";
58
59pub const STATUS_MD: &str = ".agent/STATUS.md";
61
62pub const NOTES_MD: &str = ".agent/NOTES.md";
64
65pub const COMMIT_MESSAGE_TXT: &str = ".agent/commit-message.txt";
67
68pub const CHECKPOINT_JSON: &str = ".agent/checkpoint.json";
70
71pub const START_COMMIT: &str = ".agent/start_commit";
73
74pub const REVIEW_BASELINE_TXT: &str = ".agent/review_baseline.txt";
76
77pub const PROMPT_MD: &str = "PROMPT.md";
79
80pub const PROMPT_BACKUP: &str = ".agent/PROMPT.md.backup";
82
83pub const AGENT_CONFIG_TOML: &str = ".agent/config.toml";
85
86pub const AGENTS_TOML: &str = ".agent/agents.toml";
88
89pub const PIPELINE_LOG: &str = ".agent/logs/pipeline.log";
91
92use std::fs;
93use std::io;
94use std::path::{Path, PathBuf};
95
96#[derive(Debug, Clone)]
104pub struct DirEntry {
105 path: PathBuf,
107 is_file: bool,
109 is_dir: bool,
111 modified: Option<std::time::SystemTime>,
113}
114
115impl DirEntry {
116 pub fn new(path: PathBuf, is_file: bool, is_dir: bool) -> Self {
118 Self {
119 path,
120 is_file,
121 is_dir,
122 modified: None,
123 }
124 }
125
126 pub fn with_modified(
128 path: PathBuf,
129 is_file: bool,
130 is_dir: bool,
131 modified: std::time::SystemTime,
132 ) -> Self {
133 Self {
134 path,
135 is_file,
136 is_dir,
137 modified: Some(modified),
138 }
139 }
140
141 pub fn path(&self) -> &Path {
143 &self.path
144 }
145
146 pub fn is_file(&self) -> bool {
148 self.is_file
149 }
150
151 pub fn is_dir(&self) -> bool {
153 self.is_dir
154 }
155
156 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
158 self.path.file_name()
159 }
160
161 pub fn modified(&self) -> Option<std::time::SystemTime> {
163 self.modified
164 }
165}
166
167pub trait Workspace: Send + Sync {
176 fn root(&self) -> &Path;
178
179 fn read(&self, relative: &Path) -> io::Result<String>;
185
186 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>>;
188
189 fn write(&self, relative: &Path, content: &str) -> io::Result<()>;
192
193 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()>;
196
197 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()>;
200
201 fn exists(&self, relative: &Path) -> bool;
203
204 fn is_file(&self, relative: &Path) -> bool;
206
207 fn is_dir(&self, relative: &Path) -> bool;
209
210 fn remove(&self, relative: &Path) -> io::Result<()>;
212
213 fn remove_if_exists(&self, relative: &Path) -> io::Result<()>;
215
216 fn remove_dir_all(&self, relative: &Path) -> io::Result<()>;
221
222 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()>;
224
225 fn create_dir_all(&self, relative: &Path) -> io::Result<()>;
227
228 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>>;
234
235 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
240
241 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()>;
266
267 fn set_readonly(&self, relative: &Path) -> io::Result<()>;
277
278 fn set_writable(&self, relative: &Path) -> io::Result<()>;
287
288 fn absolute(&self, relative: &Path) -> PathBuf {
294 self.root().join(relative)
295 }
296
297 fn absolute_str(&self, relative: &str) -> String {
299 self.root().join(relative).display().to_string()
300 }
301
302 fn agent_dir(&self) -> PathBuf {
308 self.root().join(AGENT_DIR)
309 }
310
311 fn agent_logs(&self) -> PathBuf {
313 self.root().join(AGENT_LOGS)
314 }
315
316 fn agent_tmp(&self) -> PathBuf {
318 self.root().join(AGENT_TMP)
319 }
320
321 fn plan_md(&self) -> PathBuf {
323 self.root().join(PLAN_MD)
324 }
325
326 fn issues_md(&self) -> PathBuf {
328 self.root().join(ISSUES_MD)
329 }
330
331 fn status_md(&self) -> PathBuf {
333 self.root().join(STATUS_MD)
334 }
335
336 fn notes_md(&self) -> PathBuf {
338 self.root().join(NOTES_MD)
339 }
340
341 fn commit_message(&self) -> PathBuf {
343 self.root().join(COMMIT_MESSAGE_TXT)
344 }
345
346 fn checkpoint(&self) -> PathBuf {
348 self.root().join(CHECKPOINT_JSON)
349 }
350
351 fn start_commit(&self) -> PathBuf {
353 self.root().join(START_COMMIT)
354 }
355
356 fn review_baseline(&self) -> PathBuf {
358 self.root().join(REVIEW_BASELINE_TXT)
359 }
360
361 fn prompt_md(&self) -> PathBuf {
363 self.root().join(PROMPT_MD)
364 }
365
366 fn prompt_backup(&self) -> PathBuf {
368 self.root().join(PROMPT_BACKUP)
369 }
370
371 fn agent_config(&self) -> PathBuf {
373 self.root().join(AGENT_CONFIG_TOML)
374 }
375
376 fn agents_toml(&self) -> PathBuf {
378 self.root().join(AGENTS_TOML)
379 }
380
381 fn pipeline_log(&self) -> PathBuf {
383 self.root().join(PIPELINE_LOG)
384 }
385
386 fn xsd_path(&self, name: &str) -> PathBuf {
388 self.root().join(format!(".agent/tmp/{}.xsd", name))
389 }
390
391 fn xml_path(&self, name: &str) -> PathBuf {
393 self.root().join(format!(".agent/tmp/{}.xml", name))
394 }
395
396 fn log_path(&self, name: &str) -> PathBuf {
398 self.root().join(format!(".agent/logs/{}", name))
399 }
400}
401
402#[derive(Debug, Clone)]
410pub struct WorkspaceFs {
411 root: PathBuf,
412}
413
414impl WorkspaceFs {
415 pub fn new(repo_root: PathBuf) -> Self {
421 Self { root: repo_root }
422 }
423}
424
425impl Workspace for WorkspaceFs {
426 fn root(&self) -> &Path {
427 &self.root
428 }
429
430 fn read(&self, relative: &Path) -> io::Result<String> {
431 fs::read_to_string(self.root.join(relative))
432 }
433
434 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
435 fs::read(self.root.join(relative))
436 }
437
438 fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
439 let path = self.root.join(relative);
440 if let Some(parent) = path.parent() {
441 fs::create_dir_all(parent)?;
442 }
443 fs::write(path, content)
444 }
445
446 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
447 let path = self.root.join(relative);
448 if let Some(parent) = path.parent() {
449 fs::create_dir_all(parent)?;
450 }
451 fs::write(path, content)
452 }
453
454 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
455 use std::io::Write;
456 let path = self.root.join(relative);
457 if let Some(parent) = path.parent() {
458 fs::create_dir_all(parent)?;
459 }
460 let mut file = fs::OpenOptions::new()
461 .create(true)
462 .append(true)
463 .open(path)?;
464 file.write_all(content)?;
465 file.flush()
466 }
467
468 fn exists(&self, relative: &Path) -> bool {
469 self.root.join(relative).exists()
470 }
471
472 fn is_file(&self, relative: &Path) -> bool {
473 self.root.join(relative).is_file()
474 }
475
476 fn is_dir(&self, relative: &Path) -> bool {
477 self.root.join(relative).is_dir()
478 }
479
480 fn remove(&self, relative: &Path) -> io::Result<()> {
481 fs::remove_file(self.root.join(relative))
482 }
483
484 fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
485 let path = self.root.join(relative);
486 if path.exists() {
487 fs::remove_file(path)?;
488 }
489 Ok(())
490 }
491
492 fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
493 fs::remove_dir_all(self.root.join(relative))
494 }
495
496 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
497 let path = self.root.join(relative);
498 if path.exists() {
499 fs::remove_dir_all(path)?;
500 }
501 Ok(())
502 }
503
504 fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
505 fs::create_dir_all(self.root.join(relative))
506 }
507
508 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
509 let abs_path = self.root.join(relative);
510 let mut entries = Vec::new();
511 for entry in fs::read_dir(abs_path)? {
512 let entry = entry?;
513 let metadata = entry.metadata()?;
514 let rel_path = relative.join(entry.file_name());
516 let modified = metadata.modified().ok();
517 if let Some(mod_time) = modified {
518 entries.push(DirEntry::with_modified(
519 rel_path,
520 metadata.is_file(),
521 metadata.is_dir(),
522 mod_time,
523 ));
524 } else {
525 entries.push(DirEntry::new(
526 rel_path,
527 metadata.is_file(),
528 metadata.is_dir(),
529 ));
530 }
531 }
532 Ok(entries)
533 }
534
535 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
536 fs::rename(self.root.join(from), self.root.join(to))
537 }
538
539 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
540 use std::io::Write;
541 use tempfile::NamedTempFile;
542
543 let path = self.root.join(relative);
544
545 if let Some(parent) = path.parent() {
547 fs::create_dir_all(parent)?;
548 }
549
550 let parent_dir = path.parent().unwrap_or_else(|| Path::new("."));
553 let mut temp_file = NamedTempFile::new_in(parent_dir)?;
554
555 #[cfg(unix)]
558 {
559 use std::os::unix::fs::PermissionsExt;
560 let mut perms = fs::metadata(temp_file.path())?.permissions();
561 perms.set_mode(0o600);
562 fs::set_permissions(temp_file.path(), perms)?;
563 }
564
565 temp_file.write_all(content.as_bytes())?;
567 temp_file.flush()?;
568 temp_file.as_file().sync_all()?;
569
570 temp_file.persist(&path).map_err(|e| e.error)?;
572
573 Ok(())
574 }
575
576 fn set_readonly(&self, relative: &Path) -> io::Result<()> {
577 let path = self.root.join(relative);
578 if !path.exists() {
579 return Ok(());
580 }
581
582 let metadata = fs::metadata(&path)?;
583 let mut perms = metadata.permissions();
584
585 #[cfg(unix)]
586 {
587 use std::os::unix::fs::PermissionsExt;
588 perms.set_mode(0o444);
589 }
590
591 #[cfg(windows)]
592 {
593 perms.set_readonly(true);
594 }
595
596 fs::set_permissions(path, perms)
597 }
598
599 fn set_writable(&self, relative: &Path) -> io::Result<()> {
600 let path = self.root.join(relative);
601 if !path.exists() {
602 return Ok(());
603 }
604
605 let metadata = fs::metadata(&path)?;
606 let mut perms = metadata.permissions();
607
608 #[cfg(unix)]
609 {
610 use std::os::unix::fs::PermissionsExt;
611 perms.set_mode(0o644);
612 }
613
614 #[cfg(windows)]
615 {
616 perms.set_readonly(false);
617 }
618
619 fs::set_permissions(path, perms)
620 }
621}
622
623#[cfg(any(test, feature = "test-utils"))]
629#[derive(Debug, Clone)]
630struct MemoryFile {
631 content: Vec<u8>,
632 modified: std::time::SystemTime,
633}
634
635#[cfg(any(test, feature = "test-utils"))]
636impl MemoryFile {
637 fn new(content: Vec<u8>) -> Self {
638 Self {
639 content,
640 modified: std::time::SystemTime::now(),
641 }
642 }
643
644 fn with_modified(content: Vec<u8>, modified: std::time::SystemTime) -> Self {
645 Self { content, modified }
646 }
647}
648
649#[cfg(any(test, feature = "test-utils"))]
657#[derive(Debug)]
658pub struct MemoryWorkspace {
659 root: PathBuf,
660 files: std::sync::RwLock<std::collections::HashMap<PathBuf, MemoryFile>>,
661 directories: std::sync::RwLock<std::collections::HashSet<PathBuf>>,
662}
663
664#[cfg(any(test, feature = "test-utils"))]
665impl MemoryWorkspace {
666 pub fn new(root: PathBuf) -> Self {
670 Self {
671 root,
672 files: std::sync::RwLock::new(std::collections::HashMap::new()),
673 directories: std::sync::RwLock::new(std::collections::HashSet::new()),
674 }
675 }
676
677 pub fn new_test() -> Self {
679 Self::new(PathBuf::from("/test/repo"))
680 }
681
682 fn ensure_parent_dirs(&self, path: &Path) {
686 if let Some(parent) = path.parent() {
687 if parent.as_os_str().is_empty() {
688 return;
689 }
690 let mut dirs = self.directories.write().unwrap();
691 let mut current = PathBuf::new();
692 for component in parent.components() {
693 current.push(component);
694 dirs.insert(current.clone());
695 }
696 }
697 }
698
699 fn ensure_dir_path(&self, path: &Path) {
703 let mut dirs = self.directories.write().unwrap();
704 let mut current = PathBuf::new();
705 for component in path.components() {
706 current.push(component);
707 dirs.insert(current.clone());
708 }
709 }
710
711 pub fn with_file(self, path: &str, content: &str) -> Self {
715 let path_buf = PathBuf::from(path);
716 self.ensure_parent_dirs(&path_buf);
717 self.files
718 .write()
719 .unwrap()
720 .insert(path_buf, MemoryFile::new(content.as_bytes().to_vec()));
721 self
722 }
723
724 pub fn with_file_at_time(
728 self,
729 path: &str,
730 content: &str,
731 modified: std::time::SystemTime,
732 ) -> Self {
733 let path_buf = PathBuf::from(path);
734 self.ensure_parent_dirs(&path_buf);
735 self.files.write().unwrap().insert(
736 path_buf,
737 MemoryFile::with_modified(content.as_bytes().to_vec(), modified),
738 );
739 self
740 }
741
742 pub fn with_file_bytes(self, path: &str, content: &[u8]) -> Self {
746 let path_buf = PathBuf::from(path);
747 self.ensure_parent_dirs(&path_buf);
748 self.files
749 .write()
750 .unwrap()
751 .insert(path_buf, MemoryFile::new(content.to_vec()));
752 self
753 }
754
755 pub fn with_dir(self, path: &str) -> Self {
757 let path_buf = PathBuf::from(path);
758 self.ensure_dir_path(&path_buf);
759 self
760 }
761
762 pub fn list_files_in_dir(&self, dir: &str) -> Vec<PathBuf> {
766 let dir_path = PathBuf::from(dir);
767 self.files
768 .read()
769 .unwrap()
770 .keys()
771 .filter(|path| {
772 path.parent()
773 .map(|p| p == dir_path || p.starts_with(&dir_path))
774 .unwrap_or(false)
775 })
776 .cloned()
777 .collect()
778 }
779
780 pub fn get_modified(&self, path: &str) -> Option<std::time::SystemTime> {
782 self.files
783 .read()
784 .unwrap()
785 .get(&PathBuf::from(path))
786 .map(|f| f.modified)
787 }
788
789 pub fn list_directories(&self) -> Vec<PathBuf> {
791 self.directories.read().unwrap().iter().cloned().collect()
792 }
793
794 pub fn written_files(&self) -> std::collections::HashMap<PathBuf, Vec<u8>> {
796 self.files
797 .read()
798 .unwrap()
799 .iter()
800 .map(|(k, v)| (k.clone(), v.content.clone()))
801 .collect()
802 }
803
804 pub fn get_file(&self, path: &str) -> Option<String> {
806 self.files
807 .read()
808 .unwrap()
809 .get(&PathBuf::from(path))
810 .map(|f| String::from_utf8_lossy(&f.content).to_string())
811 }
812
813 pub fn get_file_bytes(&self, path: &str) -> Option<Vec<u8>> {
815 self.files
816 .read()
817 .unwrap()
818 .get(&PathBuf::from(path))
819 .map(|f| f.content.clone())
820 }
821
822 pub fn was_written(&self, path: &str) -> bool {
824 self.files
825 .read()
826 .unwrap()
827 .contains_key(&PathBuf::from(path))
828 }
829
830 pub fn clear(&self) {
832 self.files.write().unwrap().clear();
833 self.directories.write().unwrap().clear();
834 }
835}
836
837#[cfg(any(test, feature = "test-utils"))]
838impl Workspace for MemoryWorkspace {
839 fn root(&self) -> &Path {
840 &self.root
841 }
842
843 fn read(&self, relative: &Path) -> io::Result<String> {
844 self.files
845 .read()
846 .unwrap()
847 .get(relative)
848 .map(|f| String::from_utf8_lossy(&f.content).to_string())
849 .ok_or_else(|| {
850 io::Error::new(
851 io::ErrorKind::NotFound,
852 format!("File not found: {}", relative.display()),
853 )
854 })
855 }
856
857 fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
858 self.files
859 .read()
860 .unwrap()
861 .get(relative)
862 .map(|f| f.content.clone())
863 .ok_or_else(|| {
864 io::Error::new(
865 io::ErrorKind::NotFound,
866 format!("File not found: {}", relative.display()),
867 )
868 })
869 }
870
871 fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
872 self.ensure_parent_dirs(relative);
873 self.files.write().unwrap().insert(
874 relative.to_path_buf(),
875 MemoryFile::new(content.as_bytes().to_vec()),
876 );
877 Ok(())
878 }
879
880 fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
881 self.ensure_parent_dirs(relative);
882 self.files
883 .write()
884 .unwrap()
885 .insert(relative.to_path_buf(), MemoryFile::new(content.to_vec()));
886 Ok(())
887 }
888
889 fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
890 self.ensure_parent_dirs(relative);
891 let mut files = self.files.write().unwrap();
892 let entry = files
893 .entry(relative.to_path_buf())
894 .or_insert_with(|| MemoryFile::new(Vec::new()));
895 entry.content.extend_from_slice(content);
896 entry.modified = std::time::SystemTime::now();
897 Ok(())
898 }
899
900 fn exists(&self, relative: &Path) -> bool {
901 self.files.read().unwrap().contains_key(relative)
902 || self.directories.read().unwrap().contains(relative)
903 }
904
905 fn is_file(&self, relative: &Path) -> bool {
906 self.files.read().unwrap().contains_key(relative)
907 }
908
909 fn is_dir(&self, relative: &Path) -> bool {
910 self.directories.read().unwrap().contains(relative)
911 }
912
913 fn remove(&self, relative: &Path) -> io::Result<()> {
914 self.files
915 .write()
916 .unwrap()
917 .remove(relative)
918 .map(|_| ())
919 .ok_or_else(|| {
920 io::Error::new(
921 io::ErrorKind::NotFound,
922 format!("File not found: {}", relative.display()),
923 )
924 })
925 }
926
927 fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
928 self.files.write().unwrap().remove(relative);
929 Ok(())
930 }
931
932 fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
933 if !self.directories.read().unwrap().contains(relative) {
935 return Err(io::Error::new(
936 io::ErrorKind::NotFound,
937 format!("Directory not found: {}", relative.display()),
938 ));
939 }
940 self.remove_dir_all_if_exists(relative)
941 }
942
943 fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
944 {
946 let mut files = self.files.write().unwrap();
947 let to_remove: Vec<PathBuf> = files
948 .keys()
949 .filter(|path| path.starts_with(relative))
950 .cloned()
951 .collect();
952 for path in to_remove {
953 files.remove(&path);
954 }
955 }
956 {
958 let mut dirs = self.directories.write().unwrap();
959 let to_remove: Vec<PathBuf> = dirs
960 .iter()
961 .filter(|path| path.starts_with(relative) || *path == relative)
962 .cloned()
963 .collect();
964 for path in to_remove {
965 dirs.remove(&path);
966 }
967 }
968 Ok(())
969 }
970
971 fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
972 self.ensure_dir_path(relative);
973 Ok(())
974 }
975
976 fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
977 let files = self.files.read().unwrap();
978 let dirs = self.directories.read().unwrap();
979
980 if !relative.as_os_str().is_empty() && !dirs.contains(relative) {
982 return Err(io::Error::new(
983 io::ErrorKind::NotFound,
984 format!("Directory not found: {}", relative.display()),
985 ));
986 }
987
988 let mut entries = Vec::new();
989 let mut seen = std::collections::HashSet::new();
990
991 for (path, mem_file) in files.iter() {
993 if let Some(parent) = path.parent() {
994 if parent == relative {
995 if let Some(name) = path.file_name() {
996 if seen.insert(name.to_os_string()) {
997 entries.push(DirEntry::with_modified(
998 path.clone(),
999 true,
1000 false,
1001 mem_file.modified,
1002 ));
1003 }
1004 }
1005 }
1006 }
1007 }
1008
1009 for dir_path in dirs.iter() {
1011 if let Some(parent) = dir_path.parent() {
1012 if parent == relative {
1013 if let Some(name) = dir_path.file_name() {
1014 if seen.insert(name.to_os_string()) {
1015 entries.push(DirEntry::new(dir_path.clone(), false, true));
1016 }
1017 }
1018 }
1019 }
1020 }
1021
1022 Ok(entries)
1023 }
1024
1025 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1026 self.ensure_parent_dirs(to);
1028 let mut files = self.files.write().unwrap();
1029 if let Some(file) = files.remove(from) {
1030 files.insert(to.to_path_buf(), file);
1031 Ok(())
1032 } else {
1033 Err(io::Error::new(
1034 io::ErrorKind::NotFound,
1035 format!("File not found: {}", from.display()),
1036 ))
1037 }
1038 }
1039
1040 fn set_readonly(&self, _relative: &Path) -> io::Result<()> {
1041 Ok(())
1043 }
1044
1045 fn set_writable(&self, _relative: &Path) -> io::Result<()> {
1046 Ok(())
1048 }
1049
1050 fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
1051 self.write(relative, content)
1054 }
1055}
1056
1057#[cfg(any(test, feature = "test-utils"))]
1058impl Clone for MemoryWorkspace {
1059 fn clone(&self) -> Self {
1060 Self {
1061 root: self.root.clone(),
1062 files: std::sync::RwLock::new(self.files.read().unwrap().clone()),
1063 directories: std::sync::RwLock::new(self.directories.read().unwrap().clone()),
1064 }
1065 }
1066}
1067
1068#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1081 fn test_workspace_fs_root() {
1082 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1083 assert_eq!(ws.root(), Path::new("/test/repo"));
1084 }
1085
1086 #[test]
1087 fn test_workspace_fs_agent_paths() {
1088 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1089
1090 assert_eq!(ws.agent_dir(), PathBuf::from("/test/repo/.agent"));
1091 assert_eq!(ws.agent_logs(), PathBuf::from("/test/repo/.agent/logs"));
1092 assert_eq!(ws.agent_tmp(), PathBuf::from("/test/repo/.agent/tmp"));
1093 assert_eq!(ws.plan_md(), PathBuf::from("/test/repo/.agent/PLAN.md"));
1094 assert_eq!(ws.issues_md(), PathBuf::from("/test/repo/.agent/ISSUES.md"));
1095 assert_eq!(
1096 ws.commit_message(),
1097 PathBuf::from("/test/repo/.agent/commit-message.txt")
1098 );
1099 assert_eq!(
1100 ws.checkpoint(),
1101 PathBuf::from("/test/repo/.agent/checkpoint.json")
1102 );
1103 assert_eq!(
1104 ws.start_commit(),
1105 PathBuf::from("/test/repo/.agent/start_commit")
1106 );
1107 assert_eq!(ws.prompt_md(), PathBuf::from("/test/repo/PROMPT.md"));
1108 }
1109
1110 #[test]
1111 fn test_workspace_fs_dynamic_paths() {
1112 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1113
1114 assert_eq!(
1115 ws.xsd_path("plan"),
1116 PathBuf::from("/test/repo/.agent/tmp/plan.xsd")
1117 );
1118 assert_eq!(
1119 ws.xml_path("issues"),
1120 PathBuf::from("/test/repo/.agent/tmp/issues.xml")
1121 );
1122 assert_eq!(
1123 ws.log_path("agent.log"),
1124 PathBuf::from("/test/repo/.agent/logs/agent.log")
1125 );
1126 }
1127
1128 #[test]
1129 fn test_workspace_fs_absolute() {
1130 let ws = WorkspaceFs::new(PathBuf::from("/test/repo"));
1131
1132 let abs = ws.absolute(Path::new(".agent/tmp/plan.xml"));
1133 assert_eq!(abs, PathBuf::from("/test/repo/.agent/tmp/plan.xml"));
1134
1135 let abs_str = ws.absolute_str(".agent/tmp/plan.xml");
1136 assert_eq!(abs_str, "/test/repo/.agent/tmp/plan.xml");
1137 }
1138
1139 #[test]
1144 fn test_memory_workspace_read_write() {
1145 let ws = MemoryWorkspace::new_test();
1146
1147 ws.write(Path::new(".agent/test.txt"), "hello").unwrap();
1148 assert_eq!(ws.read(Path::new(".agent/test.txt")).unwrap(), "hello");
1149 assert!(ws.was_written(".agent/test.txt"));
1150 }
1151
1152 #[test]
1153 fn test_memory_workspace_with_file() {
1154 let ws = MemoryWorkspace::new_test().with_file("existing.txt", "pre-existing content");
1155
1156 assert_eq!(
1157 ws.read(Path::new("existing.txt")).unwrap(),
1158 "pre-existing content"
1159 );
1160 }
1161
1162 #[test]
1163 fn test_memory_workspace_exists() {
1164 let ws = MemoryWorkspace::new_test().with_file("exists.txt", "content");
1165
1166 assert!(ws.exists(Path::new("exists.txt")));
1167 assert!(!ws.exists(Path::new("not_exists.txt")));
1168 }
1169
1170 #[test]
1171 fn test_memory_workspace_remove() {
1172 let ws = MemoryWorkspace::new_test().with_file("to_delete.txt", "content");
1173
1174 assert!(ws.exists(Path::new("to_delete.txt")));
1175 ws.remove(Path::new("to_delete.txt")).unwrap();
1176 assert!(!ws.exists(Path::new("to_delete.txt")));
1177 }
1178
1179 #[test]
1180 fn test_memory_workspace_read_nonexistent_fails() {
1181 let ws = MemoryWorkspace::new_test();
1182
1183 let result = ws.read(Path::new("nonexistent.txt"));
1184 assert!(result.is_err());
1185 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
1186 }
1187
1188 #[test]
1189 fn test_memory_workspace_written_files() {
1190 let ws = MemoryWorkspace::new_test();
1191
1192 ws.write(Path::new("file1.txt"), "content1").unwrap();
1193 ws.write(Path::new("file2.txt"), "content2").unwrap();
1194
1195 let files = ws.written_files();
1196 assert_eq!(files.len(), 2);
1197 assert_eq!(
1198 String::from_utf8_lossy(files.get(&PathBuf::from("file1.txt")).unwrap()),
1199 "content1"
1200 );
1201 }
1202
1203 #[test]
1204 fn test_memory_workspace_get_file() {
1205 let ws = MemoryWorkspace::new_test();
1206
1207 ws.write(Path::new("test.txt"), "test content").unwrap();
1208 assert_eq!(ws.get_file("test.txt"), Some("test content".to_string()));
1209 assert_eq!(ws.get_file("nonexistent.txt"), None);
1210 }
1211
1212 #[test]
1213 fn test_memory_workspace_clear() {
1214 let ws = MemoryWorkspace::new_test().with_file("file.txt", "content");
1215
1216 assert!(ws.exists(Path::new("file.txt")));
1217 ws.clear();
1218 assert!(!ws.exists(Path::new("file.txt")));
1219 }
1220
1221 #[test]
1222 fn test_memory_workspace_absolute_str() {
1223 let ws = MemoryWorkspace::new_test();
1224
1225 assert_eq!(
1226 ws.absolute_str(".agent/tmp/commit_message.xml"),
1227 "/test/repo/.agent/tmp/commit_message.xml"
1228 );
1229 }
1230
1231 #[test]
1232 fn test_memory_workspace_creates_parent_dirs() {
1233 let ws = MemoryWorkspace::new_test();
1234
1235 ws.write(Path::new("a/b/c/file.txt"), "content").unwrap();
1236
1237 assert!(ws.is_dir(Path::new("a")));
1239 assert!(ws.is_dir(Path::new("a/b")));
1240 assert!(ws.is_dir(Path::new("a/b/c")));
1241 assert!(ws.is_file(Path::new("a/b/c/file.txt")));
1242 }
1243
1244 #[test]
1245 fn test_memory_workspace_rename() {
1246 let ws = MemoryWorkspace::new_test().with_file("old.txt", "content");
1247
1248 ws.rename(Path::new("old.txt"), Path::new("new.txt"))
1249 .unwrap();
1250
1251 assert!(!ws.exists(Path::new("old.txt")));
1252 assert!(ws.exists(Path::new("new.txt")));
1253 assert_eq!(ws.read(Path::new("new.txt")).unwrap(), "content");
1254 }
1255
1256 #[test]
1257 fn test_memory_workspace_rename_creates_parent_dirs() {
1258 let ws = MemoryWorkspace::new_test().with_file("old.txt", "content");
1259
1260 ws.rename(Path::new("old.txt"), Path::new("a/b/new.txt"))
1261 .unwrap();
1262
1263 assert!(!ws.exists(Path::new("old.txt")));
1264 assert!(ws.is_dir(Path::new("a")));
1265 assert!(ws.is_dir(Path::new("a/b")));
1266 assert!(ws.exists(Path::new("a/b/new.txt")));
1267 }
1268
1269 #[test]
1270 fn test_memory_workspace_rename_nonexistent_fails() {
1271 let ws = MemoryWorkspace::new_test();
1272
1273 let result = ws.rename(Path::new("nonexistent.txt"), Path::new("new.txt"));
1274 assert!(result.is_err());
1275 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
1276 }
1277
1278 #[test]
1279 fn test_memory_workspace_set_readonly_noop() {
1280 let ws = MemoryWorkspace::new_test().with_file("test.txt", "content");
1282
1283 ws.set_readonly(Path::new("test.txt")).unwrap();
1285 ws.set_writable(Path::new("test.txt")).unwrap();
1286
1287 assert_eq!(ws.read(Path::new("test.txt")).unwrap(), "content");
1289 }
1290
1291 #[test]
1292 fn test_memory_workspace_write_atomic() {
1293 let ws = MemoryWorkspace::new_test();
1294
1295 ws.write_atomic(Path::new("atomic.txt"), "atomic content")
1296 .unwrap();
1297
1298 assert_eq!(ws.read(Path::new("atomic.txt")).unwrap(), "atomic content");
1299 }
1300
1301 #[test]
1302 fn test_memory_workspace_write_atomic_creates_parent_dirs() {
1303 let ws = MemoryWorkspace::new_test();
1304
1305 ws.write_atomic(Path::new("a/b/c/atomic.txt"), "nested atomic")
1306 .unwrap();
1307
1308 assert!(ws.is_dir(Path::new("a")));
1309 assert!(ws.is_dir(Path::new("a/b")));
1310 assert!(ws.is_dir(Path::new("a/b/c")));
1311 assert_eq!(
1312 ws.read(Path::new("a/b/c/atomic.txt")).unwrap(),
1313 "nested atomic"
1314 );
1315 }
1316
1317 #[test]
1318 fn test_memory_workspace_write_atomic_overwrites() {
1319 let ws = MemoryWorkspace::new_test().with_file("existing.txt", "old content");
1320
1321 ws.write_atomic(Path::new("existing.txt"), "new content")
1322 .unwrap();
1323
1324 assert_eq!(ws.read(Path::new("existing.txt")).unwrap(), "new content");
1325 }
1326}