1use std::io;
56use std::path::{Component, Path, PathBuf};
57
58use uuid::Uuid;
59
60use crate::principal::PrincipalId;
61
62pub const LAYOUT_VERSION: &str = "1";
64
65fn reject_parent_traversal(path: &Path, var_name: &str) -> io::Result<()> {
67 if path.components().any(|c| matches!(c, Component::ParentDir)) {
68 return Err(io::Error::new(
69 io::ErrorKind::InvalidInput,
70 format!("{var_name} must not contain '..' path components"),
71 ));
72 }
73 Ok(())
74}
75
76#[derive(Debug, Clone)]
85pub struct AstridHome {
86 root: PathBuf,
87}
88
89impl AstridHome {
90 pub fn resolve() -> io::Result<Self> {
98 let astrid_home = std::env::var("ASTRID_HOME").ok();
99 let home = if astrid_home.is_none() {
100 std::env::var("HOME").ok()
101 } else {
102 None
103 };
104 Self::resolve_with_env(astrid_home, home)
105 }
106
107 fn resolve_with_env(astrid_home: Option<String>, home: Option<String>) -> io::Result<Self> {
109 let root = if let Some(custom) = astrid_home {
110 let p = PathBuf::from(&custom);
111 if !p.is_absolute() {
112 return Err(io::Error::new(
113 io::ErrorKind::InvalidInput,
114 "ASTRID_HOME must be an absolute path",
115 ));
116 }
117 reject_parent_traversal(&p, "ASTRID_HOME")?;
118 p
119 } else {
120 let home = home.ok_or_else(|| {
121 io::Error::new(
122 io::ErrorKind::NotFound,
123 "neither ASTRID_HOME nor HOME environment variable is set",
124 )
125 })?;
126 let home_path = PathBuf::from(&home);
127 if !home_path.is_absolute() {
128 return Err(io::Error::new(
129 io::ErrorKind::InvalidInput,
130 "HOME must be an absolute path",
131 ));
132 }
133 reject_parent_traversal(&home_path, "HOME")?;
134 home_path.join(".astrid")
135 };
136
137 Ok(Self { root })
138 }
139
140 #[must_use]
142 pub fn from_path(root: impl Into<PathBuf>) -> Self {
143 Self { root: root.into() }
144 }
145
146 pub fn ensure(&self) -> io::Result<()> {
160 let dirs = [
161 self.etc_dir(),
162 self.hooks_dir(),
163 self.var_dir(),
164 self.run_dir(),
165 self.log_dir(),
166 self.keys_dir(),
167 self.bin_dir(),
168 self.wit_dir(),
169 self.home_dir(),
170 ];
171 for dir in &dirs {
172 std::fs::create_dir_all(dir)?;
173 }
174
175 let version_path = self.etc_dir().join("layout-version");
177 if !version_path.exists() {
178 std::fs::write(&version_path, LAYOUT_VERSION)?;
179 }
180
181 #[cfg(unix)]
182 {
183 use std::os::unix::fs::PermissionsExt;
184 let perms = std::fs::Permissions::from_mode(0o700);
185 std::fs::set_permissions(self.root(), perms.clone())?;
186 for dir in &dirs {
187 std::fs::set_permissions(dir, perms.clone())?;
188 }
189 }
190 Ok(())
191 }
192
193 #[must_use]
197 pub fn root(&self) -> &Path {
198 &self.root
199 }
200
201 #[must_use]
203 pub fn etc_dir(&self) -> PathBuf {
204 self.root.join("etc")
205 }
206
207 #[must_use]
209 pub fn config_path(&self) -> PathBuf {
210 self.etc_dir().join("config.toml")
211 }
212
213 #[must_use]
215 pub fn servers_config_path(&self) -> PathBuf {
216 self.etc_dir().join("servers.toml")
217 }
218
219 #[must_use]
221 pub fn gateway_config_path(&self) -> PathBuf {
222 self.etc_dir().join("gateway.toml")
223 }
224
225 #[must_use]
227 pub fn hooks_dir(&self) -> PathBuf {
228 self.etc_dir().join("hooks")
229 }
230
231 #[must_use]
242 pub fn profiles_dir(&self) -> PathBuf {
243 self.etc_dir().join("profiles")
244 }
245
246 #[must_use]
250 pub fn profile_path(&self, id: &PrincipalId) -> PathBuf {
251 self.profiles_dir().join(format!("{id}.toml"))
252 }
253
254 #[must_use]
256 pub fn var_dir(&self) -> PathBuf {
257 self.root.join("var")
258 }
259
260 #[must_use]
262 pub fn state_db_path(&self) -> PathBuf {
263 self.var_dir().join("state.db")
264 }
265
266 #[must_use]
268 pub fn run_dir(&self) -> PathBuf {
269 self.root.join("run")
270 }
271
272 #[must_use]
274 pub fn socket_path(&self) -> PathBuf {
275 self.run_dir().join("system.sock")
276 }
277
278 #[must_use]
280 pub fn token_path(&self) -> PathBuf {
281 self.run_dir().join("system.token")
282 }
283
284 #[must_use]
290 pub fn ready_path(&self) -> PathBuf {
291 self.run_dir().join("system.ready")
292 }
293
294 #[must_use]
296 pub fn deferred_db_path(&self) -> PathBuf {
297 self.run_dir().join("deferred.db")
298 }
299
300 #[must_use]
302 pub fn log_dir(&self) -> PathBuf {
303 self.root.join("log")
304 }
305
306 #[must_use]
314 pub fn secrets_dir(&self) -> PathBuf {
315 self.root.join("secrets")
316 }
317
318 #[must_use]
320 pub fn keys_dir(&self) -> PathBuf {
321 self.root.join("keys")
322 }
323
324 #[must_use]
326 pub fn runtime_key_path(&self) -> PathBuf {
327 self.keys_dir().join("runtime.key")
328 }
329
330 #[must_use]
332 pub fn bin_dir(&self) -> PathBuf {
333 self.root.join("bin")
334 }
335
336 #[must_use]
342 pub fn wit_dir(&self) -> PathBuf {
343 self.root.join("wit")
344 }
345
346 #[must_use]
351 pub fn lib_dir(&self) -> PathBuf {
352 self.root.join("lib")
353 }
354
355 #[must_use]
357 pub fn home_dir(&self) -> PathBuf {
358 self.root.join("home")
359 }
360
361 #[must_use]
363 pub fn principal_home(&self, id: &PrincipalId) -> PrincipalHome {
364 PrincipalHome {
365 root: self.home_dir().join(id.as_str()),
366 }
367 }
368}
369
370#[derive(Debug, Clone)]
377pub struct PrincipalHome {
378 root: PathBuf,
379}
380
381impl PrincipalHome {
382 #[must_use]
384 pub fn from_path(root: impl Into<PathBuf>) -> Self {
385 Self { root: root.into() }
386 }
387
388 pub fn ensure(&self) -> io::Result<()> {
394 let dirs = [
395 self.capsules_dir(),
396 self.kv_dir(),
397 self.log_dir(),
398 self.audit_dir(),
399 self.tokens_dir(),
400 self.tmp_dir(),
401 self.env_dir(),
402 ];
403 for dir in &dirs {
404 std::fs::create_dir_all(dir)?;
405 }
406 #[cfg(unix)]
407 {
408 use std::os::unix::fs::PermissionsExt;
409 let perms = std::fs::Permissions::from_mode(0o700);
410 std::fs::set_permissions(&self.root, perms.clone())?;
411 std::fs::set_permissions(self.root.join(".local"), perms.clone())?;
413 std::fs::set_permissions(self.root.join(".config"), perms)?;
414 }
415 Ok(())
416 }
417
418 #[must_use]
422 pub fn root(&self) -> &Path {
423 &self.root
424 }
425
426 #[must_use]
428 pub fn capsules_dir(&self) -> PathBuf {
429 self.root.join(".local").join("capsules")
430 }
431
432 #[must_use]
434 pub fn kv_dir(&self) -> PathBuf {
435 self.root.join(".local").join("kv")
436 }
437
438 #[must_use]
440 pub fn log_dir(&self) -> PathBuf {
441 self.root.join(".local").join("log")
442 }
443
444 #[must_use]
446 pub fn audit_dir(&self) -> PathBuf {
447 self.root.join(".local").join("audit")
448 }
449
450 #[must_use]
452 pub fn tokens_dir(&self) -> PathBuf {
453 self.root.join(".local").join("tokens")
454 }
455
456 #[must_use]
458 pub fn tmp_dir(&self) -> PathBuf {
459 self.root.join(".local").join("tmp")
460 }
461
462 #[must_use]
464 pub fn config_dir(&self) -> PathBuf {
465 self.root.join(".config")
466 }
467
468 #[must_use]
470 pub fn env_dir(&self) -> PathBuf {
471 self.root.join(".config").join("env")
472 }
473}
474
475#[derive(Debug, Clone)]
482pub struct WorkspaceDir {
483 project_root: PathBuf,
485}
486
487impl WorkspaceDir {
488 #[must_use]
496 pub fn detect(start_dir: &Path) -> Self {
497 let start = if start_dir.is_absolute() {
498 start_dir.to_path_buf()
499 } else {
500 std::env::current_dir().unwrap_or_default().join(start_dir)
501 };
502
503 let mut current = start.as_path();
504
505 loop {
506 if current.join(".astrid").is_dir() {
507 return Self {
508 project_root: current.to_path_buf(),
509 };
510 }
511 if current.join(".git").exists() {
512 return Self {
513 project_root: current.to_path_buf(),
514 };
515 }
516 if current.join("ASTRID.md").exists() {
517 return Self {
518 project_root: current.to_path_buf(),
519 };
520 }
521 match current.parent() {
522 Some(parent) if parent != current => current = parent,
523 _ => break,
524 }
525 }
526
527 Self {
528 project_root: start,
529 }
530 }
531
532 #[must_use]
534 pub fn from_path(project_root: impl Into<PathBuf>) -> Self {
535 Self {
536 project_root: project_root.into(),
537 }
538 }
539
540 pub fn ensure(&self) -> io::Result<()> {
547 std::fs::create_dir_all(self.dot_astrid())?;
548 let _ = self.workspace_id()?;
549 Ok(())
550 }
551
552 #[must_use]
554 pub fn root(&self) -> &Path {
555 &self.project_root
556 }
557
558 #[must_use]
560 pub fn dot_astrid(&self) -> PathBuf {
561 self.project_root.join(".astrid")
562 }
563
564 #[must_use]
566 pub fn capsules_dir(&self) -> PathBuf {
567 self.dot_astrid().join("capsules")
568 }
569
570 #[must_use]
572 pub fn workspace_id_path(&self) -> PathBuf {
573 self.dot_astrid().join("workspace-id")
574 }
575
576 pub fn workspace_id(&self) -> io::Result<Uuid> {
585 let path = self.workspace_id_path();
586 if let Ok(content) = std::fs::read_to_string(&path) {
587 let trimmed = content.trim();
588 if let Ok(id) = Uuid::parse_str(trimmed) {
589 return Ok(id);
590 }
591 }
592 std::fs::create_dir_all(self.dot_astrid())?;
593 let id = Uuid::new_v4();
594 std::fs::write(&path, id.to_string())?;
595 Ok(id)
596 }
597
598 #[must_use]
600 pub fn instructions_path(&self) -> PathBuf {
601 self.dot_astrid().join("ASTRID.md")
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
612 fn test_astrid_home_resolve_with_env() {
613 let dir = tempfile::tempdir().unwrap();
614 let path = dir.path().to_path_buf();
615 let path_str = path.to_string_lossy().to_string();
616
617 let home = AstridHome::resolve_with_env(Some(path_str), None).unwrap();
618 assert_eq!(home.root(), path);
619 }
620
621 #[test]
622 fn test_astrid_home_resolve_default() {
623 let home_val = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
624 let home = AstridHome::resolve_with_env(None, Some(home_val.clone())).unwrap();
625 let expected = PathBuf::from(home_val).join(".astrid");
626 assert_eq!(home.root(), expected);
627 }
628
629 #[test]
630 fn test_astrid_home_rejects_traversal_in_astrid_home() {
631 let result = AstridHome::resolve_with_env(Some("/tmp/../etc".to_string()), None);
632 assert!(result.is_err());
633 let err = result.unwrap_err();
634 assert!(
635 err.to_string().contains("'..'"),
636 "expected path traversal error, got: {err}"
637 );
638 }
639
640 #[test]
641 fn test_astrid_home_rejects_traversal_in_home() {
642 let result = AstridHome::resolve_with_env(None, Some("/tmp/../etc".to_string()));
643 assert!(result.is_err());
644 let err = result.unwrap_err();
645 assert!(
646 err.to_string().contains("'..'"),
647 "expected path traversal error, got: {err}"
648 );
649 }
650
651 #[test]
652 fn test_astrid_home_rejects_relative_env() {
653 let result = AstridHome::resolve_with_env(Some("relative/path".to_string()), None);
654 assert!(result.is_err());
655 assert!(result.unwrap_err().to_string().contains("absolute"));
656 }
657
658 #[test]
659 fn test_astrid_home_rejects_empty_env() {
660 let result = AstridHome::resolve_with_env(Some(String::new()), None);
661 assert!(result.is_err());
662 }
663
664 #[test]
665 fn test_astrid_home_rejects_relative_home() {
666 let result = AstridHome::resolve_with_env(None, Some("relative/path".to_string()));
667 assert!(result.is_err());
668 assert!(result.unwrap_err().to_string().contains("absolute"));
669 }
670
671 #[test]
674 fn test_astrid_home_ensure_creates_dirs() {
675 let dir = tempfile::tempdir().unwrap();
676 let home = AstridHome::from_path(dir.path());
677 home.ensure().unwrap();
678
679 assert!(home.etc_dir().exists());
680 assert!(home.hooks_dir().exists());
681 assert!(home.var_dir().exists());
682 assert!(home.run_dir().exists());
683 assert!(home.log_dir().exists());
684 assert!(home.keys_dir().exists());
685 assert!(home.bin_dir().exists());
686 assert!(home.home_dir().exists());
687 }
688
689 #[test]
690 fn test_astrid_home_ensure_writes_layout_version() {
691 let dir = tempfile::tempdir().unwrap();
692 let home = AstridHome::from_path(dir.path());
693 home.ensure().unwrap();
694
695 let version_path = home.etc_dir().join("layout-version");
696 assert!(version_path.exists());
697 let content = std::fs::read_to_string(&version_path).unwrap();
698 assert_eq!(content, LAYOUT_VERSION);
699 }
700
701 #[test]
702 fn test_astrid_home_ensure_idempotent() {
703 let dir = tempfile::tempdir().unwrap();
704 let home = AstridHome::from_path(dir.path());
705 home.ensure().unwrap();
706 home.ensure().unwrap(); }
708
709 #[cfg(unix)]
710 #[test]
711 fn test_astrid_home_ensure_sets_permissions() {
712 use std::os::unix::fs::PermissionsExt;
713
714 let dir = tempfile::tempdir().unwrap();
715 let home = AstridHome::from_path(dir.path());
716 home.ensure().unwrap();
717
718 let root_perms = std::fs::metadata(home.root()).unwrap().permissions();
719 assert_eq!(root_perms.mode() & 0o777, 0o700);
720
721 let keys_perms = std::fs::metadata(home.keys_dir()).unwrap().permissions();
722 assert_eq!(keys_perms.mode() & 0o777, 0o700);
723 }
724
725 #[test]
728 fn test_astrid_home_fhs_paths() {
729 let home = AstridHome::from_path("/tmp/test-astrid");
730 let r = "/tmp/test-astrid";
731
732 assert_eq!(home.root(), Path::new(r));
733 assert_eq!(home.etc_dir(), PathBuf::from(format!("{r}/etc")));
734 assert_eq!(
735 home.config_path(),
736 PathBuf::from(format!("{r}/etc/config.toml"))
737 );
738 assert_eq!(
739 home.servers_config_path(),
740 PathBuf::from(format!("{r}/etc/servers.toml"))
741 );
742 assert_eq!(
743 home.gateway_config_path(),
744 PathBuf::from(format!("{r}/etc/gateway.toml"))
745 );
746 assert_eq!(home.hooks_dir(), PathBuf::from(format!("{r}/etc/hooks")));
747 assert_eq!(home.var_dir(), PathBuf::from(format!("{r}/var")));
748 assert_eq!(
749 home.state_db_path(),
750 PathBuf::from(format!("{r}/var/state.db"))
751 );
752 assert_eq!(home.run_dir(), PathBuf::from(format!("{r}/run")));
753 assert_eq!(
754 home.socket_path(),
755 PathBuf::from(format!("{r}/run/system.sock"))
756 );
757 assert_eq!(
758 home.token_path(),
759 PathBuf::from(format!("{r}/run/system.token"))
760 );
761 assert_eq!(
762 home.ready_path(),
763 PathBuf::from(format!("{r}/run/system.ready"))
764 );
765 assert_eq!(
766 home.deferred_db_path(),
767 PathBuf::from(format!("{r}/run/deferred.db"))
768 );
769 assert_eq!(home.log_dir(), PathBuf::from(format!("{r}/log")));
770 assert_eq!(home.keys_dir(), PathBuf::from(format!("{r}/keys")));
771 assert_eq!(
772 home.runtime_key_path(),
773 PathBuf::from(format!("{r}/keys/runtime.key"))
774 );
775 assert_eq!(home.bin_dir(), PathBuf::from(format!("{r}/bin")));
776 assert_eq!(home.home_dir(), PathBuf::from(format!("{r}/home")));
777 }
778
779 #[test]
782 fn test_principal_home_from_astrid_home() {
783 let home = AstridHome::from_path("/tmp/test-astrid");
784 let principal = PrincipalId::default();
785 let ph = home.principal_home(&principal);
786 assert_eq!(ph.root(), Path::new("/tmp/test-astrid/home/default"));
787 }
788
789 #[test]
790 fn test_principal_home_paths() {
791 let ph = PrincipalHome::from_path("/tmp/test-astrid/home/alice");
792 let r = "/tmp/test-astrid/home/alice";
793
794 assert_eq!(ph.root(), Path::new(r));
795 assert_eq!(
796 ph.capsules_dir(),
797 PathBuf::from(format!("{r}/.local/capsules"))
798 );
799 assert_eq!(ph.kv_dir(), PathBuf::from(format!("{r}/.local/kv")));
800 assert_eq!(ph.log_dir(), PathBuf::from(format!("{r}/.local/log")));
801 assert_eq!(ph.audit_dir(), PathBuf::from(format!("{r}/.local/audit")));
802 assert_eq!(ph.tokens_dir(), PathBuf::from(format!("{r}/.local/tokens")));
803 assert_eq!(ph.tmp_dir(), PathBuf::from(format!("{r}/.local/tmp")));
804 assert_eq!(ph.config_dir(), PathBuf::from(format!("{r}/.config")));
805 assert_eq!(ph.env_dir(), PathBuf::from(format!("{r}/.config/env")));
806 }
807
808 #[test]
809 fn test_principal_home_ensure_creates_dirs() {
810 let dir = tempfile::tempdir().unwrap();
811 let ph = PrincipalHome::from_path(dir.path().join("alice"));
812 ph.ensure().unwrap();
813
814 assert!(ph.capsules_dir().exists());
815 assert!(ph.kv_dir().exists());
816 assert!(ph.log_dir().exists());
817 assert!(ph.audit_dir().exists());
818 assert!(ph.tokens_dir().exists());
819 assert!(ph.tmp_dir().exists());
820 assert!(ph.env_dir().exists());
821 }
822
823 #[cfg(unix)]
824 #[test]
825 fn test_principal_home_ensure_sets_permissions() {
826 use std::os::unix::fs::PermissionsExt;
827
828 let dir = tempfile::tempdir().unwrap();
829 let ph = PrincipalHome::from_path(dir.path().join("bob"));
830 ph.ensure().unwrap();
831
832 let root_perms = std::fs::metadata(ph.root()).unwrap().permissions();
833 assert_eq!(root_perms.mode() & 0o777, 0o700);
834
835 let local_perms = std::fs::metadata(ph.root().join(".local"))
836 .unwrap()
837 .permissions();
838 assert_eq!(local_perms.mode() & 0o777, 0o700);
839
840 let config_perms = std::fs::metadata(ph.root().join(".config"))
841 .unwrap()
842 .permissions();
843 assert_eq!(config_perms.mode() & 0o777, 0o700);
844 }
845
846 #[test]
847 fn test_principal_home_ensure_idempotent() {
848 let dir = tempfile::tempdir().unwrap();
849 let ph = PrincipalHome::from_path(dir.path().join("charlie"));
850 ph.ensure().unwrap();
851 ph.ensure().unwrap(); }
853
854 #[test]
857 fn test_workspace_detect_with_dot_astrid() {
858 let dir = tempfile::tempdir().unwrap();
859 let astrid_dir = dir.path().join(".astrid");
860 std::fs::create_dir(&astrid_dir).unwrap();
861
862 let sub = dir.path().join("src").join("deep");
863 std::fs::create_dir_all(&sub).unwrap();
864
865 let ws = WorkspaceDir::detect(&sub);
866 assert_eq!(ws.root(), dir.path());
867 }
868
869 #[test]
870 fn test_workspace_detect_with_git() {
871 let dir = tempfile::tempdir().unwrap();
872 let git_dir = dir.path().join(".git");
873 std::fs::create_dir(&git_dir).unwrap();
874
875 let sub = dir.path().join("src");
876 std::fs::create_dir_all(&sub).unwrap();
877
878 let ws = WorkspaceDir::detect(&sub);
879 assert_eq!(ws.root(), dir.path());
880 }
881
882 #[test]
883 fn test_workspace_detect_with_astrid_md() {
884 let dir = tempfile::tempdir().unwrap();
885 std::fs::write(dir.path().join("ASTRID.md"), "# Project").unwrap();
886
887 let sub = dir.path().join("src");
888 std::fs::create_dir_all(&sub).unwrap();
889
890 let ws = WorkspaceDir::detect(&sub);
891 assert_eq!(ws.root(), dir.path());
892 }
893
894 #[test]
895 fn test_workspace_detect_fallback() {
896 let dir = tempfile::tempdir().unwrap();
897 let isolated = dir.path().join("isolated");
898 std::fs::create_dir_all(&isolated).unwrap();
899
900 let ws = WorkspaceDir::from_path(&isolated);
901 assert_eq!(ws.root(), isolated);
902 }
903
904 #[test]
905 fn test_workspace_detect_prefers_dot_astrid_over_git() {
906 let dir = tempfile::tempdir().unwrap();
907 std::fs::create_dir(dir.path().join(".astrid")).unwrap();
908 std::fs::create_dir(dir.path().join(".git")).unwrap();
909
910 let sub = dir.path().join("src");
911 std::fs::create_dir_all(&sub).unwrap();
912
913 let ws = WorkspaceDir::detect(&sub);
914 assert_eq!(ws.root(), dir.path());
915 }
916
917 #[test]
918 fn test_workspace_ensure_creates_dirs_and_id() {
919 let dir = tempfile::tempdir().unwrap();
920 let ws = WorkspaceDir::from_path(dir.path());
921 ws.ensure().unwrap();
922
923 assert!(ws.dot_astrid().exists());
924 assert!(ws.workspace_id_path().exists());
925
926 let content = std::fs::read_to_string(ws.workspace_id_path()).unwrap();
927 uuid::Uuid::parse_str(content.trim()).expect("workspace-id should be a valid UUID");
928 }
929
930 #[test]
931 fn test_workspace_id_adopts_existing() {
932 let dir = tempfile::tempdir().unwrap();
933 let ws = WorkspaceDir::from_path(dir.path());
934
935 std::fs::create_dir_all(ws.dot_astrid()).unwrap();
936 let pre_id = uuid::Uuid::new_v4();
937 std::fs::write(ws.workspace_id_path(), pre_id.to_string()).unwrap();
938
939 let id = ws.workspace_id().unwrap();
940 assert_eq!(id, pre_id);
941 }
942
943 #[test]
944 fn test_workspace_id_stable_across_calls() {
945 let dir = tempfile::tempdir().unwrap();
946 let ws = WorkspaceDir::from_path(dir.path());
947 let id1 = ws.workspace_id().unwrap();
948 let id2 = ws.workspace_id().unwrap();
949 assert_eq!(id1, id2);
950 }
951
952 #[test]
953 fn test_workspace_path_accessors() {
954 let ws = WorkspaceDir::from_path("/home/user/project");
955 assert_eq!(ws.root(), Path::new("/home/user/project"));
956 assert_eq!(ws.dot_astrid(), PathBuf::from("/home/user/project/.astrid"));
957 assert_eq!(
958 ws.capsules_dir(),
959 PathBuf::from("/home/user/project/.astrid/capsules")
960 );
961 assert_eq!(
962 ws.workspace_id_path(),
963 PathBuf::from("/home/user/project/.astrid/workspace-id")
964 );
965 assert_eq!(
966 ws.instructions_path(),
967 PathBuf::from("/home/user/project/.astrid/ASTRID.md")
968 );
969 }
970}