1use crate::db::{create_pool, run_migrations};
2use crate::error::{IntentError, Result};
3use serde::{Deserialize, Serialize};
4use sqlx::SqlitePool;
5use std::path::PathBuf;
6
7const INTENT_DIR: &str = ".intent-engine";
8const DB_FILE: &str = "project.db";
9
10const PROJECT_ROOT_MARKERS: &[&str] = &[
13 ".git", ".hg", "package.json", "Cargo.toml", "pyproject.toml", "go.mod", "pom.xml", "build.gradle", ];
22
23#[derive(Debug)]
24pub struct ProjectContext {
25 pub root: PathBuf,
26 pub db_path: PathBuf,
27 pub pool: SqlitePool,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DirectoryTraversalInfo {
33 pub path: String,
34 pub has_intent_engine: bool,
35 pub is_selected: bool,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
40pub struct DatabasePathInfo {
41 pub current_working_directory: String,
42 pub env_var_set: bool,
43 pub env_var_path: Option<String>,
44 pub env_var_valid: Option<bool>,
45 pub directories_checked: Vec<DirectoryTraversalInfo>,
46 pub home_directory: Option<String>,
47 pub home_has_intent_engine: bool,
48 pub final_database_path: Option<String>,
49 pub resolution_method: Option<String>,
50}
51
52impl ProjectContext {
53 pub fn get_database_path_info() -> DatabasePathInfo {
58 let cwd = std::env::current_dir()
59 .ok()
60 .map(|p| p.display().to_string())
61 .unwrap_or_else(|| "<unable to determine>".to_string());
62
63 let mut info = DatabasePathInfo {
64 current_working_directory: cwd.clone(),
65 env_var_set: false,
66 env_var_path: None,
67 env_var_valid: None,
68 directories_checked: Vec::new(),
69 home_directory: None,
70 home_has_intent_engine: false,
71 final_database_path: None,
72 resolution_method: None,
73 };
74
75 if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
77 info.env_var_set = true;
78 info.env_var_path = Some(env_path.clone());
79
80 let path = PathBuf::from(&env_path);
81 let intent_dir = path.join(INTENT_DIR);
82 let has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
83 info.env_var_valid = Some(has_intent_engine);
84
85 if has_intent_engine {
86 let db_path = intent_dir.join(DB_FILE);
87 info.final_database_path = Some(db_path.display().to_string());
88 info.resolution_method =
89 Some("Environment Variable (INTENT_ENGINE_PROJECT_DIR)".to_string());
90 return info;
91 }
92 }
93
94 if let Ok(mut current) = std::env::current_dir() {
96 loop {
97 let intent_dir = current.join(INTENT_DIR);
98 let has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
99
100 let is_selected = has_intent_engine && info.final_database_path.is_none();
101
102 info.directories_checked.push(DirectoryTraversalInfo {
103 path: current.display().to_string(),
104 has_intent_engine,
105 is_selected,
106 });
107
108 if has_intent_engine && info.final_database_path.is_none() {
109 let db_path = intent_dir.join(DB_FILE);
110 info.final_database_path = Some(db_path.display().to_string());
111 info.resolution_method = Some("Upward Directory Traversal".to_string());
112 }
114
115 if !current.pop() {
116 break;
117 }
118 }
119 }
120
121 #[cfg(not(target_os = "windows"))]
123 let home_path = std::env::var("HOME").ok().map(PathBuf::from);
124
125 #[cfg(target_os = "windows")]
126 let home_path = std::env::var("HOME")
127 .ok()
128 .map(PathBuf::from)
129 .or_else(|| std::env::var("USERPROFILE").ok().map(PathBuf::from));
130
131 if let Some(home) = home_path {
132 info.home_directory = Some(home.display().to_string());
133 let intent_dir = home.join(INTENT_DIR);
134 info.home_has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
135
136 if info.home_has_intent_engine && info.final_database_path.is_none() {
137 let db_path = intent_dir.join(DB_FILE);
138 info.final_database_path = Some(db_path.display().to_string());
139 info.resolution_method = Some("Home Directory Fallback".to_string());
140 }
141 }
142
143 info
144 }
145
146 pub fn find_project_root() -> Option<PathBuf> {
158 if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
160 let path = PathBuf::from(env_path);
161 let intent_dir = path.join(INTENT_DIR);
162 if intent_dir.exists() && intent_dir.is_dir() {
163 eprintln!(
164 "✓ Using project from INTENT_ENGINE_PROJECT_DIR: {}",
165 path.display()
166 );
167 return Some(path);
168 } else {
169 eprintln!(
170 "⚠ INTENT_ENGINE_PROJECT_DIR set but no .intent-engine found: {}",
171 path.display()
172 );
173 }
174 }
175
176 if let Ok(current_dir) = std::env::current_dir() {
179 let start_dir = current_dir.clone();
180
181 let project_boundary = Self::infer_project_root();
184
185 let mut current = start_dir.clone();
186 loop {
187 let intent_dir = current.join(INTENT_DIR);
188 if intent_dir.exists() && intent_dir.is_dir() {
189 if let Some(ref boundary) = project_boundary {
194 if !current.starts_with(boundary) && current != *boundary {
197 break;
200 }
201 }
202
203 if current != start_dir {
204 eprintln!("✓ Found project: {}", current.display());
205 }
206 return Some(current);
207 }
208
209 if let Some(ref boundary) = project_boundary {
212 if current == *boundary {
213 break;
216 }
217 }
218
219 if !current.pop() {
220 break;
221 }
222 }
223 }
224
225 if let Ok(home) = std::env::var("HOME") {
227 let home_path = PathBuf::from(home);
228 let intent_dir = home_path.join(INTENT_DIR);
229 if intent_dir.exists() && intent_dir.is_dir() {
230 eprintln!("✓ Using home project: {}", home_path.display());
231 return Some(home_path);
232 }
233 }
234
235 #[cfg(target_os = "windows")]
237 if let Ok(userprofile) = std::env::var("USERPROFILE") {
238 let home_path = PathBuf::from(userprofile);
239 let intent_dir = home_path.join(INTENT_DIR);
240 if intent_dir.exists() && intent_dir.is_dir() {
241 eprintln!("✓ Using home project: {}", home_path.display());
242 return Some(home_path);
243 }
244 }
245
246 None
247 }
248
249 fn infer_project_root_from(start_path: &std::path::Path) -> Option<PathBuf> {
261 let mut current = start_path.to_path_buf();
262
263 loop {
264 for marker in PROJECT_ROOT_MARKERS {
266 let marker_path = current.join(marker);
267 if marker_path.exists() {
268 return Some(current);
269 }
270 }
271
272 if !current.pop() {
274 break;
276 }
277 }
278
279 None
280 }
281
282 fn infer_project_root() -> Option<PathBuf> {
290 let cwd = std::env::current_dir().ok()?;
291 Self::infer_project_root_from(&cwd)
292 }
293
294 pub async fn initialize_project() -> Result<Self> {
301 let cwd = std::env::current_dir()?;
302
303 let root = match Self::infer_project_root() {
305 Some(inferred_root) => {
306 inferred_root
308 },
309 None => {
310 eprintln!(
313 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
314 Initialized Intent-Engine in the current directory '{}'.\n\
315 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
316 cwd.display()
317 );
318 cwd
319 },
320 };
321
322 let intent_dir = root.join(INTENT_DIR);
323 let db_path = intent_dir.join(DB_FILE);
324
325 if !intent_dir.exists() {
327 std::fs::create_dir_all(&intent_dir)?;
328 }
329
330 let pool = create_pool(&db_path).await?;
332
333 run_migrations(&pool).await?;
335
336 Ok(ProjectContext {
337 root,
338 db_path,
339 pool,
340 })
341 }
342
343 pub async fn initialize_project_at(project_dir: PathBuf) -> Result<Self> {
372 let root = match Self::infer_project_root_from(&project_dir) {
374 Some(inferred_root) => {
375 inferred_root
377 },
378 None => {
379 project_dir
383 },
384 };
385
386 let intent_dir = root.join(INTENT_DIR);
387 let db_path = intent_dir.join(DB_FILE);
388
389 if !intent_dir.exists() {
391 std::fs::create_dir_all(&intent_dir)?;
392 }
393
394 let pool = create_pool(&db_path).await?;
396
397 run_migrations(&pool).await?;
399
400 Ok(ProjectContext {
401 root,
402 db_path,
403 pool,
404 })
405 }
406
407 pub async fn load() -> Result<Self> {
409 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
410 let db_path = root.join(INTENT_DIR).join(DB_FILE);
411
412 let pool = create_pool(&db_path).await?;
413
414 Ok(ProjectContext {
415 root,
416 db_path,
417 pool,
418 })
419 }
420
421 pub async fn load_or_init() -> Result<Self> {
423 match Self::load().await {
424 Ok(ctx) => Ok(ctx),
425 Err(IntentError::NotAProject) => Self::initialize_project().await,
426 Err(e) => Err(e),
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
440 fn test_constants() {
441 assert_eq!(INTENT_DIR, ".intent-engine");
442 assert_eq!(DB_FILE, "project.db");
443 }
444
445 #[test]
446 fn test_project_context_debug() {
447 let _type_check = |ctx: ProjectContext| {
450 let _ = format!("{:?}", ctx);
451 };
452 }
453
454 #[test]
455 fn test_project_root_markers_list() {
456 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
458 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
459 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
460 }
461
462 #[test]
463 fn test_project_root_markers_priority() {
464 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
466 }
467
468 #[test]
471 fn test_infer_project_root_with_git() {
472 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
476 }
477
478 #[test]
480 fn test_all_major_project_types_covered() {
481 let markers = PROJECT_ROOT_MARKERS;
482
483 assert!(markers.contains(&".git"));
485 assert!(markers.contains(&".hg"));
486
487 assert!(markers.contains(&"Cargo.toml")); assert!(markers.contains(&"package.json")); assert!(markers.contains(&"pyproject.toml")); assert!(markers.contains(&"go.mod")); assert!(markers.contains(&"pom.xml")); assert!(markers.contains(&"build.gradle")); }
495
496 #[test]
498 fn test_directory_traversal_info_creation() {
499 let info = DirectoryTraversalInfo {
500 path: "/test/path".to_string(),
501 has_intent_engine: true,
502 is_selected: false,
503 };
504
505 assert_eq!(info.path, "/test/path");
506 assert!(info.has_intent_engine);
507 assert!(!info.is_selected);
508 }
509
510 #[test]
512 fn test_directory_traversal_info_clone() {
513 let info = DirectoryTraversalInfo {
514 path: "/test/path".to_string(),
515 has_intent_engine: true,
516 is_selected: true,
517 };
518
519 let cloned = info.clone();
520 assert_eq!(cloned.path, info.path);
521 assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
522 assert_eq!(cloned.is_selected, info.is_selected);
523 }
524
525 #[test]
527 fn test_directory_traversal_info_debug() {
528 let info = DirectoryTraversalInfo {
529 path: "/test/path".to_string(),
530 has_intent_engine: false,
531 is_selected: true,
532 };
533
534 let debug_str = format!("{:?}", info);
535 assert!(debug_str.contains("DirectoryTraversalInfo"));
536 assert!(debug_str.contains("/test/path"));
537 }
538
539 #[test]
541 fn test_directory_traversal_info_serialization() {
542 let info = DirectoryTraversalInfo {
543 path: "/test/path".to_string(),
544 has_intent_engine: true,
545 is_selected: false,
546 };
547
548 let json = serde_json::to_string(&info).unwrap();
549 assert!(json.contains("path"));
550 assert!(json.contains("has_intent_engine"));
551 assert!(json.contains("is_selected"));
552 assert!(json.contains("/test/path"));
553 }
554
555 #[test]
557 fn test_directory_traversal_info_deserialization() {
558 let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
559 let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
560
561 assert_eq!(info.path, "/test/path");
562 assert!(info.has_intent_engine);
563 assert!(!info.is_selected);
564 }
565
566 #[test]
568 fn test_database_path_info_creation() {
569 let info = DatabasePathInfo {
570 current_working_directory: "/test/cwd".to_string(),
571 env_var_set: false,
572 env_var_path: None,
573 env_var_valid: None,
574 directories_checked: vec![],
575 home_directory: Some("/home/user".to_string()),
576 home_has_intent_engine: false,
577 final_database_path: Some("/test/db.db".to_string()),
578 resolution_method: Some("Test Method".to_string()),
579 };
580
581 assert_eq!(info.current_working_directory, "/test/cwd");
582 assert!(!info.env_var_set);
583 assert_eq!(info.env_var_path, None);
584 assert_eq!(info.home_directory, Some("/home/user".to_string()));
585 assert!(!info.home_has_intent_engine);
586 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
587 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
588 }
589
590 #[test]
592 fn test_database_path_info_with_env_var() {
593 let info = DatabasePathInfo {
594 current_working_directory: "/test/cwd".to_string(),
595 env_var_set: true,
596 env_var_path: Some("/env/path".to_string()),
597 env_var_valid: Some(true),
598 directories_checked: vec![],
599 home_directory: Some("/home/user".to_string()),
600 home_has_intent_engine: false,
601 final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
602 resolution_method: Some("Environment Variable".to_string()),
603 };
604
605 assert!(info.env_var_set);
606 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
607 assert_eq!(info.env_var_valid, Some(true));
608 assert_eq!(
609 info.resolution_method,
610 Some("Environment Variable".to_string())
611 );
612 }
613
614 #[test]
616 fn test_database_path_info_with_directories() {
617 let dirs = vec![
618 DirectoryTraversalInfo {
619 path: "/test/path1".to_string(),
620 has_intent_engine: false,
621 is_selected: false,
622 },
623 DirectoryTraversalInfo {
624 path: "/test/path2".to_string(),
625 has_intent_engine: true,
626 is_selected: true,
627 },
628 ];
629
630 let info = DatabasePathInfo {
631 current_working_directory: "/test/path1".to_string(),
632 env_var_set: false,
633 env_var_path: None,
634 env_var_valid: None,
635 directories_checked: dirs.clone(),
636 home_directory: Some("/home/user".to_string()),
637 home_has_intent_engine: false,
638 final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
639 resolution_method: Some("Upward Directory Traversal".to_string()),
640 };
641
642 assert_eq!(info.directories_checked.len(), 2);
643 assert!(!info.directories_checked[0].has_intent_engine);
644 assert!(info.directories_checked[1].has_intent_engine);
645 assert!(info.directories_checked[1].is_selected);
646 }
647
648 #[test]
650 fn test_database_path_info_debug() {
651 let info = DatabasePathInfo {
652 current_working_directory: "/test/cwd".to_string(),
653 env_var_set: false,
654 env_var_path: None,
655 env_var_valid: None,
656 directories_checked: vec![],
657 home_directory: Some("/home/user".to_string()),
658 home_has_intent_engine: false,
659 final_database_path: Some("/test/db.db".to_string()),
660 resolution_method: Some("Test".to_string()),
661 };
662
663 let debug_str = format!("{:?}", info);
664 assert!(debug_str.contains("DatabasePathInfo"));
665 assert!(debug_str.contains("/test/cwd"));
666 }
667
668 #[test]
670 fn test_database_path_info_serialization() {
671 let info = DatabasePathInfo {
672 current_working_directory: "/test/cwd".to_string(),
673 env_var_set: true,
674 env_var_path: Some("/env/path".to_string()),
675 env_var_valid: Some(true),
676 directories_checked: vec![],
677 home_directory: Some("/home/user".to_string()),
678 home_has_intent_engine: false,
679 final_database_path: Some("/test/db.db".to_string()),
680 resolution_method: Some("Test Method".to_string()),
681 };
682
683 let json = serde_json::to_string(&info).unwrap();
684 assert!(json.contains("current_working_directory"));
685 assert!(json.contains("env_var_set"));
686 assert!(json.contains("env_var_path"));
687 assert!(json.contains("final_database_path"));
688 assert!(json.contains("/test/cwd"));
689 assert!(json.contains("/env/path"));
690 }
691
692 #[test]
694 fn test_database_path_info_deserialization() {
695 let json = r#"{
696 "current_working_directory": "/test/cwd",
697 "env_var_set": true,
698 "env_var_path": "/env/path",
699 "env_var_valid": true,
700 "directories_checked": [],
701 "home_directory": "/home/user",
702 "home_has_intent_engine": false,
703 "final_database_path": "/test/db.db",
704 "resolution_method": "Test Method"
705 }"#;
706
707 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
708 assert_eq!(info.current_working_directory, "/test/cwd");
709 assert!(info.env_var_set);
710 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
711 assert_eq!(info.env_var_valid, Some(true));
712 assert_eq!(info.home_directory, Some("/home/user".to_string()));
713 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
714 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
715 }
716
717 #[test]
719 fn test_database_path_info_complete_structure() {
720 let dirs = vec![
721 DirectoryTraversalInfo {
722 path: "/home/user/project/src".to_string(),
723 has_intent_engine: false,
724 is_selected: false,
725 },
726 DirectoryTraversalInfo {
727 path: "/home/user/project".to_string(),
728 has_intent_engine: true,
729 is_selected: true,
730 },
731 DirectoryTraversalInfo {
732 path: "/home/user".to_string(),
733 has_intent_engine: false,
734 is_selected: false,
735 },
736 ];
737
738 let info = DatabasePathInfo {
739 current_working_directory: "/home/user/project/src".to_string(),
740 env_var_set: false,
741 env_var_path: None,
742 env_var_valid: None,
743 directories_checked: dirs,
744 home_directory: Some("/home/user".to_string()),
745 home_has_intent_engine: false,
746 final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
747 resolution_method: Some("Upward Directory Traversal".to_string()),
748 };
749
750 assert_eq!(info.directories_checked.len(), 3);
752 assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
753 assert_eq!(info.directories_checked[1].path, "/home/user/project");
754 assert_eq!(info.directories_checked[2].path, "/home/user");
755
756 assert!(!info.directories_checked[0].is_selected);
758 assert!(info.directories_checked[1].is_selected);
759 assert!(!info.directories_checked[2].is_selected);
760
761 assert!(!info.directories_checked[0].has_intent_engine);
763 assert!(info.directories_checked[1].has_intent_engine);
764 assert!(!info.directories_checked[2].has_intent_engine);
765 }
766
767 #[test]
769 fn test_get_database_path_info_structure() {
770 let info = ProjectContext::get_database_path_info();
771
772 assert!(!info.current_working_directory.is_empty());
774
775 let has_data = !info.directories_checked.is_empty()
777 || info.home_directory.is_some()
778 || info.env_var_set;
779
780 assert!(
781 has_data,
782 "get_database_path_info should return some directory information"
783 );
784 }
785
786 #[test]
788 fn test_get_database_path_info_checks_current_dir() {
789 let info = ProjectContext::get_database_path_info();
790
791 assert!(!info.current_working_directory.is_empty());
793
794 if !info.env_var_set || info.env_var_valid != Some(true) {
796 assert!(
797 !info.directories_checked.is_empty(),
798 "Should check at least the current directory"
799 );
800 }
801 }
802
803 #[test]
805 fn test_get_database_path_info_includes_cwd() {
806 let info = ProjectContext::get_database_path_info();
807
808 if !info.env_var_set || info.env_var_valid != Some(true) {
810 assert!(!info.directories_checked.is_empty());
811
812 let cwd = &info.current_working_directory;
814 let first_checked = &info.directories_checked[0].path;
815
816 assert!(
817 cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
818 "First checked directory should be related to CWD"
819 );
820 }
821 }
822
823 #[test]
825 fn test_get_database_path_info_resolution_method() {
826 let info = ProjectContext::get_database_path_info();
827
828 if info.final_database_path.is_some() {
830 assert!(
831 info.resolution_method.is_some(),
832 "Resolution method should be set when database path is found"
833 );
834
835 let method = info.resolution_method.unwrap();
836 assert!(
837 method.contains("Environment Variable")
838 || method.contains("Upward Directory Traversal")
839 || method.contains("Home Directory"),
840 "Resolution method should be one of the known strategies"
841 );
842 }
843 }
844
845 #[test]
847 fn test_get_database_path_info_selected_directory() {
848 let info = ProjectContext::get_database_path_info();
849
850 if (!info.env_var_set || info.env_var_valid != Some(true))
852 && !info.directories_checked.is_empty()
853 && info.final_database_path.is_some()
854 {
855 let selected_count = info
857 .directories_checked
858 .iter()
859 .filter(|d| d.is_selected)
860 .count();
861
862 assert!(
863 selected_count <= 1,
864 "At most one directory should be marked as selected"
865 );
866
867 if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
869 assert!(
870 selected.has_intent_engine,
871 "Selected directory should have .intent-engine"
872 );
873 }
874 }
875 }
876
877 #[test]
879 fn test_database_path_info_no_database_found() {
880 let info = DatabasePathInfo {
881 current_working_directory: "/test/path".to_string(),
882 env_var_set: false,
883 env_var_path: None,
884 env_var_valid: None,
885 directories_checked: vec![
886 DirectoryTraversalInfo {
887 path: "/test/path".to_string(),
888 has_intent_engine: false,
889 is_selected: false,
890 },
891 DirectoryTraversalInfo {
892 path: "/test".to_string(),
893 has_intent_engine: false,
894 is_selected: false,
895 },
896 ],
897 home_directory: Some("/home/user".to_string()),
898 home_has_intent_engine: false,
899 final_database_path: None,
900 resolution_method: None,
901 };
902
903 assert!(info.final_database_path.is_none());
904 assert!(info.resolution_method.is_none());
905 assert_eq!(info.directories_checked.len(), 2);
906 assert!(!info.home_has_intent_engine);
907 }
908
909 #[test]
911 fn test_database_path_info_env_var_invalid() {
912 let info = DatabasePathInfo {
913 current_working_directory: "/test/cwd".to_string(),
914 env_var_set: true,
915 env_var_path: Some("/invalid/path".to_string()),
916 env_var_valid: Some(false),
917 directories_checked: vec![DirectoryTraversalInfo {
918 path: "/test/cwd".to_string(),
919 has_intent_engine: true,
920 is_selected: true,
921 }],
922 home_directory: Some("/home/user".to_string()),
923 home_has_intent_engine: false,
924 final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
925 resolution_method: Some("Upward Directory Traversal".to_string()),
926 };
927
928 assert!(info.env_var_set);
929 assert_eq!(info.env_var_valid, Some(false));
930 assert!(info.final_database_path.is_some());
931 assert!(info.resolution_method.unwrap().contains("Upward Directory"));
933 }
934
935 #[test]
937 fn test_database_path_info_home_directory_used() {
938 let info = DatabasePathInfo {
939 current_working_directory: "/tmp/work".to_string(),
940 env_var_set: false,
941 env_var_path: None,
942 env_var_valid: None,
943 directories_checked: vec![
944 DirectoryTraversalInfo {
945 path: "/tmp/work".to_string(),
946 has_intent_engine: false,
947 is_selected: false,
948 },
949 DirectoryTraversalInfo {
950 path: "/tmp".to_string(),
951 has_intent_engine: false,
952 is_selected: false,
953 },
954 ],
955 home_directory: Some("/home/user".to_string()),
956 home_has_intent_engine: true,
957 final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
958 resolution_method: Some("Home Directory Fallback".to_string()),
959 };
960
961 assert!(info.home_has_intent_engine);
962 assert_eq!(
963 info.final_database_path,
964 Some("/home/user/.intent-engine/project.db".to_string())
965 );
966 assert_eq!(
967 info.resolution_method,
968 Some("Home Directory Fallback".to_string())
969 );
970 }
971
972 #[test]
974 fn test_database_path_info_full_roundtrip() {
975 let original = DatabasePathInfo {
976 current_working_directory: "/test/cwd".to_string(),
977 env_var_set: true,
978 env_var_path: Some("/env/path".to_string()),
979 env_var_valid: Some(false),
980 directories_checked: vec![
981 DirectoryTraversalInfo {
982 path: "/test/cwd".to_string(),
983 has_intent_engine: false,
984 is_selected: false,
985 },
986 DirectoryTraversalInfo {
987 path: "/test".to_string(),
988 has_intent_engine: true,
989 is_selected: true,
990 },
991 ],
992 home_directory: Some("/home/user".to_string()),
993 home_has_intent_engine: false,
994 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
995 resolution_method: Some("Upward Directory Traversal".to_string()),
996 };
997
998 let json = serde_json::to_string(&original).unwrap();
1000
1001 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1003
1004 assert_eq!(
1006 deserialized.current_working_directory,
1007 original.current_working_directory
1008 );
1009 assert_eq!(deserialized.env_var_set, original.env_var_set);
1010 assert_eq!(deserialized.env_var_path, original.env_var_path);
1011 assert_eq!(deserialized.env_var_valid, original.env_var_valid);
1012 assert_eq!(
1013 deserialized.directories_checked.len(),
1014 original.directories_checked.len()
1015 );
1016 assert_eq!(deserialized.home_directory, original.home_directory);
1017 assert_eq!(
1018 deserialized.home_has_intent_engine,
1019 original.home_has_intent_engine
1020 );
1021 assert_eq!(
1022 deserialized.final_database_path,
1023 original.final_database_path
1024 );
1025 assert_eq!(deserialized.resolution_method, original.resolution_method);
1026 }
1027
1028 #[test]
1030 fn test_directory_traversal_info_all_combinations() {
1031 let combinations = [(false, false), (false, true), (true, false), (true, true)];
1033
1034 for (has_ie, is_sel) in combinations.iter() {
1035 let info = DirectoryTraversalInfo {
1036 path: format!("/test/path/{}_{}", has_ie, is_sel),
1037 has_intent_engine: *has_ie,
1038 is_selected: *is_sel,
1039 };
1040
1041 assert_eq!(info.has_intent_engine, *has_ie);
1042 assert_eq!(info.is_selected, *is_sel);
1043 }
1044 }
1045
1046 #[test]
1048 fn test_directory_traversal_info_exact_serialization() {
1049 let info = DirectoryTraversalInfo {
1050 path: "/exact/path/with/special-chars_123".to_string(),
1051 has_intent_engine: true,
1052 is_selected: false,
1053 };
1054
1055 let json = serde_json::to_string(&info).unwrap();
1056 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1057
1058 assert_eq!(info.path, deserialized.path);
1059 assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
1060 assert_eq!(info.is_selected, deserialized.is_selected);
1061 }
1062
1063 #[test]
1065 fn test_database_path_info_all_none() {
1066 let info = DatabasePathInfo {
1067 current_working_directory: "/test".to_string(),
1068 env_var_set: false,
1069 env_var_path: None,
1070 env_var_valid: None,
1071 directories_checked: vec![],
1072 home_directory: None,
1073 home_has_intent_engine: false,
1074 final_database_path: None,
1075 resolution_method: None,
1076 };
1077
1078 assert!(!info.env_var_set);
1079 assert!(info.env_var_path.is_none());
1080 assert!(info.env_var_valid.is_none());
1081 assert!(info.directories_checked.is_empty());
1082 assert!(info.home_directory.is_none());
1083 assert!(info.final_database_path.is_none());
1084 assert!(info.resolution_method.is_none());
1085 }
1086
1087 #[test]
1089 fn test_database_path_info_all_some() {
1090 let info = DatabasePathInfo {
1091 current_working_directory: "/test".to_string(),
1092 env_var_set: true,
1093 env_var_path: Some("/env".to_string()),
1094 env_var_valid: Some(true),
1095 directories_checked: vec![DirectoryTraversalInfo {
1096 path: "/test".to_string(),
1097 has_intent_engine: true,
1098 is_selected: true,
1099 }],
1100 home_directory: Some("/home".to_string()),
1101 home_has_intent_engine: true,
1102 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1103 resolution_method: Some("Test Method".to_string()),
1104 };
1105
1106 assert!(info.env_var_set);
1107 assert!(info.env_var_path.is_some());
1108 assert!(info.env_var_valid.is_some());
1109 assert!(!info.directories_checked.is_empty());
1110 assert!(info.home_directory.is_some());
1111 assert!(info.final_database_path.is_some());
1112 assert!(info.resolution_method.is_some());
1113 }
1114
1115 #[test]
1117 fn test_get_database_path_info_home_directory() {
1118 let info = ProjectContext::get_database_path_info();
1119
1120 if std::env::var("HOME").is_ok() {
1123 assert!(
1124 info.home_directory.is_some(),
1125 "HOME env var is set, so home_directory should be Some"
1126 );
1127 }
1128 }
1129
1130 #[test]
1132 fn test_get_database_path_info_no_panic() {
1133 let info = ProjectContext::get_database_path_info();
1136
1137 assert!(!info.current_working_directory.is_empty());
1139
1140 if info.final_database_path.is_none() {
1143 let has_diagnostic_info = !info.directories_checked.is_empty()
1145 || info.env_var_set
1146 || info.home_directory.is_some();
1147
1148 assert!(
1149 has_diagnostic_info,
1150 "Even without finding a database, should provide diagnostic information"
1151 );
1152 }
1153 }
1154
1155 #[test]
1157 fn test_get_database_path_info_prefers_first_match() {
1158 let info = ProjectContext::get_database_path_info();
1159
1160 if info
1162 .resolution_method
1163 .as_ref()
1164 .is_some_and(|m| m.contains("Upward Directory"))
1165 && info.directories_checked.len() > 1
1166 {
1167 let with_ie: Vec<_> = info
1169 .directories_checked
1170 .iter()
1171 .filter(|d| d.has_intent_engine)
1172 .collect();
1173
1174 if with_ie.len() > 1 {
1175 let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1177 assert!(
1178 selected.len() <= 1,
1179 "Only the first .intent-engine found should be selected"
1180 );
1181 }
1182 }
1183 }
1184
1185 #[test]
1187 fn test_database_path_info_partial_deserialization() {
1188 let json = r#"{
1190 "current_working_directory": "/test",
1191 "env_var_set": false,
1192 "env_var_path": null,
1193 "env_var_valid": null,
1194 "directories_checked": [],
1195 "home_directory": null,
1196 "home_has_intent_engine": false,
1197 "final_database_path": null,
1198 "resolution_method": null
1199 }"#;
1200
1201 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1202 assert_eq!(info.current_working_directory, "/test");
1203 assert!(!info.env_var_set);
1204 }
1205
1206 #[test]
1208 fn test_database_path_info_json_schema() {
1209 let info = DatabasePathInfo {
1210 current_working_directory: "/test".to_string(),
1211 env_var_set: true,
1212 env_var_path: Some("/env".to_string()),
1213 env_var_valid: Some(true),
1214 directories_checked: vec![],
1215 home_directory: Some("/home".to_string()),
1216 home_has_intent_engine: false,
1217 final_database_path: Some("/db".to_string()),
1218 resolution_method: Some("Test".to_string()),
1219 };
1220
1221 let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1222
1223 assert!(json_value.get("current_working_directory").is_some());
1225 assert!(json_value.get("env_var_set").is_some());
1226 assert!(json_value.get("env_var_path").is_some());
1227 assert!(json_value.get("env_var_valid").is_some());
1228 assert!(json_value.get("directories_checked").is_some());
1229 assert!(json_value.get("home_directory").is_some());
1230 assert!(json_value.get("home_has_intent_engine").is_some());
1231 assert!(json_value.get("final_database_path").is_some());
1232 assert!(json_value.get("resolution_method").is_some());
1233 }
1234
1235 #[test]
1237 fn test_directory_traversal_info_empty_path() {
1238 let info = DirectoryTraversalInfo {
1239 path: "".to_string(),
1240 has_intent_engine: false,
1241 is_selected: false,
1242 };
1243
1244 assert_eq!(info.path, "");
1245 let json = serde_json::to_string(&info).unwrap();
1246 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1247 assert_eq!(deserialized.path, "");
1248 }
1249
1250 #[test]
1252 fn test_directory_traversal_info_unicode_path() {
1253 let info = DirectoryTraversalInfo {
1254 path: "/test/路径/مسار/путь".to_string(),
1255 has_intent_engine: true,
1256 is_selected: false,
1257 };
1258
1259 let json = serde_json::to_string(&info).unwrap();
1260 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1261 assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1262 }
1263
1264 #[test]
1266 fn test_database_path_info_long_paths() {
1267 let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1268 let info = DatabasePathInfo {
1269 current_working_directory: long_path.clone(),
1270 env_var_set: false,
1271 env_var_path: None,
1272 env_var_valid: None,
1273 directories_checked: vec![],
1274 home_directory: Some(long_path.clone()),
1275 home_has_intent_engine: false,
1276 final_database_path: Some(long_path.clone()),
1277 resolution_method: Some("Test".to_string()),
1278 };
1279
1280 let json = serde_json::to_string(&info).unwrap();
1281 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1282 assert_eq!(deserialized.current_working_directory, long_path);
1283 }
1284
1285 #[test]
1287 fn test_get_database_path_info_env_var_detection() {
1288 let info = ProjectContext::get_database_path_info();
1289
1290 if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1292 assert!(
1293 info.env_var_set,
1294 "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1295 );
1296 assert!(
1297 info.env_var_path.is_some(),
1298 "env_var_path should contain the path when env var is set"
1299 );
1300 assert!(
1301 info.env_var_valid.is_some(),
1302 "env_var_valid should be set when env var is present"
1303 );
1304 } else {
1305 assert!(
1306 !info.env_var_set,
1307 "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1308 );
1309 assert!(
1310 info.env_var_path.is_none(),
1311 "env_var_path should be None when env var is not set"
1312 );
1313 assert!(
1314 info.env_var_valid.is_none(),
1315 "env_var_valid should be None when env var is not set"
1316 );
1317 }
1318 }
1319}