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() {
180 let start_dir = current_dir.clone();
181
182 let project_boundary = Self::infer_project_root();
185
186 let mut current = start_dir.clone();
187 loop {
188 let intent_dir = current.join(INTENT_DIR);
189 if intent_dir.exists() && intent_dir.is_dir() {
190 if let Some(ref boundary) = project_boundary {
196 if !current.starts_with(boundary) && current != *boundary {
199 break;
202 }
203 }
204
205 if current != start_dir {
206 eprintln!("✓ Found project: {}", current.display());
207 }
208 return Some(current);
209 }
210
211 if let Some(ref boundary) = project_boundary {
214 if current == *boundary {
215 eprintln!("✓ Detected project root: {}", boundary.display());
218 return Some(boundary.clone());
219 }
220 }
221
222 if !current.pop() {
223 break;
224 }
225 }
226 }
227
228 if let Ok(home) = std::env::var("HOME") {
231 let home_path = PathBuf::from(home);
232 let intent_dir = home_path.join(INTENT_DIR);
233 if intent_dir.exists() && intent_dir.is_dir() {
234 eprintln!("✓ Using home project: {}", home_path.display());
235 return Some(home_path);
236 }
237 }
238
239 #[cfg(target_os = "windows")]
241 if let Ok(userprofile) = std::env::var("USERPROFILE") {
242 let home_path = PathBuf::from(userprofile);
243 let intent_dir = home_path.join(INTENT_DIR);
244 if intent_dir.exists() && intent_dir.is_dir() {
245 eprintln!("✓ Using home project: {}", home_path.display());
246 return Some(home_path);
247 }
248 }
249
250 None
251 }
252
253 fn infer_project_root_from(start_path: &std::path::Path) -> Option<PathBuf> {
265 let mut current = start_path.to_path_buf();
266
267 loop {
268 for marker in PROJECT_ROOT_MARKERS {
270 let marker_path = current.join(marker);
271 if marker_path.exists() {
272 return Some(current);
273 }
274 }
275
276 if !current.pop() {
278 break;
280 }
281 }
282
283 None
284 }
285
286 fn infer_project_root() -> Option<PathBuf> {
294 let cwd = std::env::current_dir().ok()?;
295 Self::infer_project_root_from(&cwd)
296 }
297
298 pub async fn initialize_project() -> Result<Self> {
305 let cwd = std::env::current_dir()?;
306
307 let root = match Self::infer_project_root() {
309 Some(inferred_root) => {
310 inferred_root
312 },
313 None => {
314 eprintln!(
317 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
318 Initialized Intent-Engine in the current directory '{}'.\n\
319 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
320 cwd.display()
321 );
322 cwd
323 },
324 };
325
326 let intent_dir = root.join(INTENT_DIR);
327 let db_path = intent_dir.join(DB_FILE);
328
329 if !intent_dir.exists() {
331 std::fs::create_dir_all(&intent_dir)?;
332 }
333
334 let pool = create_pool(&db_path).await?;
336
337 run_migrations(&pool).await?;
339
340 Ok(ProjectContext {
341 root,
342 db_path,
343 pool,
344 })
345 }
346
347 pub async fn initialize_project_at(project_dir: PathBuf) -> Result<Self> {
378 let root = project_dir;
381
382 let intent_dir = root.join(INTENT_DIR);
383 let db_path = intent_dir.join(DB_FILE);
384
385 if !intent_dir.exists() {
387 std::fs::create_dir_all(&intent_dir)?;
388 }
389
390 let pool = create_pool(&db_path).await?;
392
393 run_migrations(&pool).await?;
395
396 Ok(ProjectContext {
397 root,
398 db_path,
399 pool,
400 })
401 }
402
403 pub async fn load() -> Result<Self> {
405 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
406 let intent_dir = root.join(INTENT_DIR);
407
408 if !intent_dir.exists() || !intent_dir.is_dir() {
411 return Err(IntentError::NotAProject);
412 }
413
414 let db_path = intent_dir.join(DB_FILE);
415
416 let pool = create_pool(&db_path).await?;
417
418 run_migrations(&pool).await?;
421
422 Ok(ProjectContext {
423 root,
424 db_path,
425 pool,
426 })
427 }
428
429 pub async fn load_or_init() -> Result<Self> {
431 match Self::load().await {
432 Ok(ctx) => Ok(ctx),
433 Err(IntentError::NotAProject) => Self::initialize_project().await,
434 Err(e) => Err(e),
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
448 fn test_constants() {
449 assert_eq!(INTENT_DIR, ".intent-engine");
450 assert_eq!(DB_FILE, "project.db");
451 }
452
453 #[test]
454 fn test_project_context_debug() {
455 let _type_check = |ctx: ProjectContext| {
458 let _ = format!("{:?}", ctx);
459 };
460 }
461
462 #[test]
463 fn test_project_root_markers_list() {
464 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
466 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
467 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
468 }
469
470 #[test]
471 fn test_project_root_markers_priority() {
472 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
474 }
475
476 #[test]
479 fn test_infer_project_root_with_git() {
480 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
484 }
485
486 #[test]
488 fn test_all_major_project_types_covered() {
489 let markers = PROJECT_ROOT_MARKERS;
490
491 assert!(markers.contains(&".git"));
493 assert!(markers.contains(&".hg"));
494
495 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")); }
503
504 #[test]
506 fn test_directory_traversal_info_creation() {
507 let info = DirectoryTraversalInfo {
508 path: "/test/path".to_string(),
509 has_intent_engine: true,
510 is_selected: false,
511 };
512
513 assert_eq!(info.path, "/test/path");
514 assert!(info.has_intent_engine);
515 assert!(!info.is_selected);
516 }
517
518 #[test]
520 fn test_directory_traversal_info_clone() {
521 let info = DirectoryTraversalInfo {
522 path: "/test/path".to_string(),
523 has_intent_engine: true,
524 is_selected: true,
525 };
526
527 let cloned = info.clone();
528 assert_eq!(cloned.path, info.path);
529 assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
530 assert_eq!(cloned.is_selected, info.is_selected);
531 }
532
533 #[test]
535 fn test_directory_traversal_info_debug() {
536 let info = DirectoryTraversalInfo {
537 path: "/test/path".to_string(),
538 has_intent_engine: false,
539 is_selected: true,
540 };
541
542 let debug_str = format!("{:?}", info);
543 assert!(debug_str.contains("DirectoryTraversalInfo"));
544 assert!(debug_str.contains("/test/path"));
545 }
546
547 #[test]
549 fn test_directory_traversal_info_serialization() {
550 let info = DirectoryTraversalInfo {
551 path: "/test/path".to_string(),
552 has_intent_engine: true,
553 is_selected: false,
554 };
555
556 let json = serde_json::to_string(&info).unwrap();
557 assert!(json.contains("path"));
558 assert!(json.contains("has_intent_engine"));
559 assert!(json.contains("is_selected"));
560 assert!(json.contains("/test/path"));
561 }
562
563 #[test]
565 fn test_directory_traversal_info_deserialization() {
566 let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
567 let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
568
569 assert_eq!(info.path, "/test/path");
570 assert!(info.has_intent_engine);
571 assert!(!info.is_selected);
572 }
573
574 #[test]
576 fn test_database_path_info_creation() {
577 let info = DatabasePathInfo {
578 current_working_directory: "/test/cwd".to_string(),
579 env_var_set: false,
580 env_var_path: None,
581 env_var_valid: None,
582 directories_checked: vec![],
583 home_directory: Some("/home/user".to_string()),
584 home_has_intent_engine: false,
585 final_database_path: Some("/test/db.db".to_string()),
586 resolution_method: Some("Test Method".to_string()),
587 };
588
589 assert_eq!(info.current_working_directory, "/test/cwd");
590 assert!(!info.env_var_set);
591 assert_eq!(info.env_var_path, None);
592 assert_eq!(info.home_directory, Some("/home/user".to_string()));
593 assert!(!info.home_has_intent_engine);
594 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
595 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
596 }
597
598 #[test]
600 fn test_database_path_info_with_env_var() {
601 let info = DatabasePathInfo {
602 current_working_directory: "/test/cwd".to_string(),
603 env_var_set: true,
604 env_var_path: Some("/env/path".to_string()),
605 env_var_valid: Some(true),
606 directories_checked: vec![],
607 home_directory: Some("/home/user".to_string()),
608 home_has_intent_engine: false,
609 final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
610 resolution_method: Some("Environment Variable".to_string()),
611 };
612
613 assert!(info.env_var_set);
614 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
615 assert_eq!(info.env_var_valid, Some(true));
616 assert_eq!(
617 info.resolution_method,
618 Some("Environment Variable".to_string())
619 );
620 }
621
622 #[test]
624 fn test_database_path_info_with_directories() {
625 let dirs = vec![
626 DirectoryTraversalInfo {
627 path: "/test/path1".to_string(),
628 has_intent_engine: false,
629 is_selected: false,
630 },
631 DirectoryTraversalInfo {
632 path: "/test/path2".to_string(),
633 has_intent_engine: true,
634 is_selected: true,
635 },
636 ];
637
638 let info = DatabasePathInfo {
639 current_working_directory: "/test/path1".to_string(),
640 env_var_set: false,
641 env_var_path: None,
642 env_var_valid: None,
643 directories_checked: dirs.clone(),
644 home_directory: Some("/home/user".to_string()),
645 home_has_intent_engine: false,
646 final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
647 resolution_method: Some("Upward Directory Traversal".to_string()),
648 };
649
650 assert_eq!(info.directories_checked.len(), 2);
651 assert!(!info.directories_checked[0].has_intent_engine);
652 assert!(info.directories_checked[1].has_intent_engine);
653 assert!(info.directories_checked[1].is_selected);
654 }
655
656 #[test]
658 fn test_database_path_info_debug() {
659 let info = DatabasePathInfo {
660 current_working_directory: "/test/cwd".to_string(),
661 env_var_set: false,
662 env_var_path: None,
663 env_var_valid: None,
664 directories_checked: vec![],
665 home_directory: Some("/home/user".to_string()),
666 home_has_intent_engine: false,
667 final_database_path: Some("/test/db.db".to_string()),
668 resolution_method: Some("Test".to_string()),
669 };
670
671 let debug_str = format!("{:?}", info);
672 assert!(debug_str.contains("DatabasePathInfo"));
673 assert!(debug_str.contains("/test/cwd"));
674 }
675
676 #[test]
678 fn test_database_path_info_serialization() {
679 let info = DatabasePathInfo {
680 current_working_directory: "/test/cwd".to_string(),
681 env_var_set: true,
682 env_var_path: Some("/env/path".to_string()),
683 env_var_valid: Some(true),
684 directories_checked: vec![],
685 home_directory: Some("/home/user".to_string()),
686 home_has_intent_engine: false,
687 final_database_path: Some("/test/db.db".to_string()),
688 resolution_method: Some("Test Method".to_string()),
689 };
690
691 let json = serde_json::to_string(&info).unwrap();
692 assert!(json.contains("current_working_directory"));
693 assert!(json.contains("env_var_set"));
694 assert!(json.contains("env_var_path"));
695 assert!(json.contains("final_database_path"));
696 assert!(json.contains("/test/cwd"));
697 assert!(json.contains("/env/path"));
698 }
699
700 #[test]
702 fn test_database_path_info_deserialization() {
703 let json = r#"{
704 "current_working_directory": "/test/cwd",
705 "env_var_set": true,
706 "env_var_path": "/env/path",
707 "env_var_valid": true,
708 "directories_checked": [],
709 "home_directory": "/home/user",
710 "home_has_intent_engine": false,
711 "final_database_path": "/test/db.db",
712 "resolution_method": "Test Method"
713 }"#;
714
715 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
716 assert_eq!(info.current_working_directory, "/test/cwd");
717 assert!(info.env_var_set);
718 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
719 assert_eq!(info.env_var_valid, Some(true));
720 assert_eq!(info.home_directory, Some("/home/user".to_string()));
721 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
722 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
723 }
724
725 #[test]
727 fn test_database_path_info_complete_structure() {
728 let dirs = vec![
729 DirectoryTraversalInfo {
730 path: "/home/user/project/src".to_string(),
731 has_intent_engine: false,
732 is_selected: false,
733 },
734 DirectoryTraversalInfo {
735 path: "/home/user/project".to_string(),
736 has_intent_engine: true,
737 is_selected: true,
738 },
739 DirectoryTraversalInfo {
740 path: "/home/user".to_string(),
741 has_intent_engine: false,
742 is_selected: false,
743 },
744 ];
745
746 let info = DatabasePathInfo {
747 current_working_directory: "/home/user/project/src".to_string(),
748 env_var_set: false,
749 env_var_path: None,
750 env_var_valid: None,
751 directories_checked: dirs,
752 home_directory: Some("/home/user".to_string()),
753 home_has_intent_engine: false,
754 final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
755 resolution_method: Some("Upward Directory Traversal".to_string()),
756 };
757
758 assert_eq!(info.directories_checked.len(), 3);
760 assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
761 assert_eq!(info.directories_checked[1].path, "/home/user/project");
762 assert_eq!(info.directories_checked[2].path, "/home/user");
763
764 assert!(!info.directories_checked[0].is_selected);
766 assert!(info.directories_checked[1].is_selected);
767 assert!(!info.directories_checked[2].is_selected);
768
769 assert!(!info.directories_checked[0].has_intent_engine);
771 assert!(info.directories_checked[1].has_intent_engine);
772 assert!(!info.directories_checked[2].has_intent_engine);
773 }
774
775 #[test]
777 fn test_get_database_path_info_structure() {
778 let info = ProjectContext::get_database_path_info();
779
780 assert!(!info.current_working_directory.is_empty());
782
783 let has_data = !info.directories_checked.is_empty()
785 || info.home_directory.is_some()
786 || info.env_var_set;
787
788 assert!(
789 has_data,
790 "get_database_path_info should return some directory information"
791 );
792 }
793
794 #[test]
796 fn test_get_database_path_info_checks_current_dir() {
797 let info = ProjectContext::get_database_path_info();
798
799 assert!(!info.current_working_directory.is_empty());
801
802 if !info.env_var_set || info.env_var_valid != Some(true) {
804 assert!(
805 !info.directories_checked.is_empty(),
806 "Should check at least the current directory"
807 );
808 }
809 }
810
811 #[test]
813 fn test_get_database_path_info_includes_cwd() {
814 let info = ProjectContext::get_database_path_info();
815
816 if !info.env_var_set || info.env_var_valid != Some(true) {
818 assert!(!info.directories_checked.is_empty());
819
820 let cwd = &info.current_working_directory;
822 let first_checked = &info.directories_checked[0].path;
823
824 assert!(
825 cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
826 "First checked directory should be related to CWD"
827 );
828 }
829 }
830
831 #[test]
833 fn test_get_database_path_info_resolution_method() {
834 let info = ProjectContext::get_database_path_info();
835
836 if info.final_database_path.is_some() {
838 assert!(
839 info.resolution_method.is_some(),
840 "Resolution method should be set when database path is found"
841 );
842
843 let method = info.resolution_method.unwrap();
844 assert!(
845 method.contains("Environment Variable")
846 || method.contains("Upward Directory Traversal")
847 || method.contains("Home Directory"),
848 "Resolution method should be one of the known strategies"
849 );
850 }
851 }
852
853 #[test]
855 fn test_get_database_path_info_selected_directory() {
856 let info = ProjectContext::get_database_path_info();
857
858 if (!info.env_var_set || info.env_var_valid != Some(true))
860 && !info.directories_checked.is_empty()
861 && info.final_database_path.is_some()
862 {
863 let selected_count = info
865 .directories_checked
866 .iter()
867 .filter(|d| d.is_selected)
868 .count();
869
870 assert!(
871 selected_count <= 1,
872 "At most one directory should be marked as selected"
873 );
874
875 if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
877 assert!(
878 selected.has_intent_engine,
879 "Selected directory should have .intent-engine"
880 );
881 }
882 }
883 }
884
885 #[test]
887 fn test_database_path_info_no_database_found() {
888 let info = DatabasePathInfo {
889 current_working_directory: "/test/path".to_string(),
890 env_var_set: false,
891 env_var_path: None,
892 env_var_valid: None,
893 directories_checked: vec![
894 DirectoryTraversalInfo {
895 path: "/test/path".to_string(),
896 has_intent_engine: false,
897 is_selected: false,
898 },
899 DirectoryTraversalInfo {
900 path: "/test".to_string(),
901 has_intent_engine: false,
902 is_selected: false,
903 },
904 ],
905 home_directory: Some("/home/user".to_string()),
906 home_has_intent_engine: false,
907 final_database_path: None,
908 resolution_method: None,
909 };
910
911 assert!(info.final_database_path.is_none());
912 assert!(info.resolution_method.is_none());
913 assert_eq!(info.directories_checked.len(), 2);
914 assert!(!info.home_has_intent_engine);
915 }
916
917 #[test]
919 fn test_database_path_info_env_var_invalid() {
920 let info = DatabasePathInfo {
921 current_working_directory: "/test/cwd".to_string(),
922 env_var_set: true,
923 env_var_path: Some("/invalid/path".to_string()),
924 env_var_valid: Some(false),
925 directories_checked: vec![DirectoryTraversalInfo {
926 path: "/test/cwd".to_string(),
927 has_intent_engine: true,
928 is_selected: true,
929 }],
930 home_directory: Some("/home/user".to_string()),
931 home_has_intent_engine: false,
932 final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
933 resolution_method: Some("Upward Directory Traversal".to_string()),
934 };
935
936 assert!(info.env_var_set);
937 assert_eq!(info.env_var_valid, Some(false));
938 assert!(info.final_database_path.is_some());
939 assert!(info.resolution_method.unwrap().contains("Upward Directory"));
941 }
942
943 #[test]
945 fn test_database_path_info_home_directory_used() {
946 let info = DatabasePathInfo {
947 current_working_directory: "/tmp/work".to_string(),
948 env_var_set: false,
949 env_var_path: None,
950 env_var_valid: None,
951 directories_checked: vec![
952 DirectoryTraversalInfo {
953 path: "/tmp/work".to_string(),
954 has_intent_engine: false,
955 is_selected: false,
956 },
957 DirectoryTraversalInfo {
958 path: "/tmp".to_string(),
959 has_intent_engine: false,
960 is_selected: false,
961 },
962 ],
963 home_directory: Some("/home/user".to_string()),
964 home_has_intent_engine: true,
965 final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
966 resolution_method: Some("Home Directory Fallback".to_string()),
967 };
968
969 assert!(info.home_has_intent_engine);
970 assert_eq!(
971 info.final_database_path,
972 Some("/home/user/.intent-engine/project.db".to_string())
973 );
974 assert_eq!(
975 info.resolution_method,
976 Some("Home Directory Fallback".to_string())
977 );
978 }
979
980 #[test]
982 fn test_database_path_info_full_roundtrip() {
983 let original = DatabasePathInfo {
984 current_working_directory: "/test/cwd".to_string(),
985 env_var_set: true,
986 env_var_path: Some("/env/path".to_string()),
987 env_var_valid: Some(false),
988 directories_checked: vec![
989 DirectoryTraversalInfo {
990 path: "/test/cwd".to_string(),
991 has_intent_engine: false,
992 is_selected: false,
993 },
994 DirectoryTraversalInfo {
995 path: "/test".to_string(),
996 has_intent_engine: true,
997 is_selected: true,
998 },
999 ],
1000 home_directory: Some("/home/user".to_string()),
1001 home_has_intent_engine: false,
1002 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1003 resolution_method: Some("Upward Directory Traversal".to_string()),
1004 };
1005
1006 let json = serde_json::to_string(&original).unwrap();
1008
1009 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1011
1012 assert_eq!(
1014 deserialized.current_working_directory,
1015 original.current_working_directory
1016 );
1017 assert_eq!(deserialized.env_var_set, original.env_var_set);
1018 assert_eq!(deserialized.env_var_path, original.env_var_path);
1019 assert_eq!(deserialized.env_var_valid, original.env_var_valid);
1020 assert_eq!(
1021 deserialized.directories_checked.len(),
1022 original.directories_checked.len()
1023 );
1024 assert_eq!(deserialized.home_directory, original.home_directory);
1025 assert_eq!(
1026 deserialized.home_has_intent_engine,
1027 original.home_has_intent_engine
1028 );
1029 assert_eq!(
1030 deserialized.final_database_path,
1031 original.final_database_path
1032 );
1033 assert_eq!(deserialized.resolution_method, original.resolution_method);
1034 }
1035
1036 #[test]
1038 fn test_directory_traversal_info_all_combinations() {
1039 let combinations = [(false, false), (false, true), (true, false), (true, true)];
1041
1042 for (has_ie, is_sel) in combinations.iter() {
1043 let info = DirectoryTraversalInfo {
1044 path: format!("/test/path/{}_{}", has_ie, is_sel),
1045 has_intent_engine: *has_ie,
1046 is_selected: *is_sel,
1047 };
1048
1049 assert_eq!(info.has_intent_engine, *has_ie);
1050 assert_eq!(info.is_selected, *is_sel);
1051 }
1052 }
1053
1054 #[test]
1056 fn test_directory_traversal_info_exact_serialization() {
1057 let info = DirectoryTraversalInfo {
1058 path: "/exact/path/with/special-chars_123".to_string(),
1059 has_intent_engine: true,
1060 is_selected: false,
1061 };
1062
1063 let json = serde_json::to_string(&info).unwrap();
1064 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1065
1066 assert_eq!(info.path, deserialized.path);
1067 assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
1068 assert_eq!(info.is_selected, deserialized.is_selected);
1069 }
1070
1071 #[test]
1073 fn test_database_path_info_all_none() {
1074 let info = DatabasePathInfo {
1075 current_working_directory: "/test".to_string(),
1076 env_var_set: false,
1077 env_var_path: None,
1078 env_var_valid: None,
1079 directories_checked: vec![],
1080 home_directory: None,
1081 home_has_intent_engine: false,
1082 final_database_path: None,
1083 resolution_method: None,
1084 };
1085
1086 assert!(!info.env_var_set);
1087 assert!(info.env_var_path.is_none());
1088 assert!(info.env_var_valid.is_none());
1089 assert!(info.directories_checked.is_empty());
1090 assert!(info.home_directory.is_none());
1091 assert!(info.final_database_path.is_none());
1092 assert!(info.resolution_method.is_none());
1093 }
1094
1095 #[test]
1097 fn test_database_path_info_all_some() {
1098 let info = DatabasePathInfo {
1099 current_working_directory: "/test".to_string(),
1100 env_var_set: true,
1101 env_var_path: Some("/env".to_string()),
1102 env_var_valid: Some(true),
1103 directories_checked: vec![DirectoryTraversalInfo {
1104 path: "/test".to_string(),
1105 has_intent_engine: true,
1106 is_selected: true,
1107 }],
1108 home_directory: Some("/home".to_string()),
1109 home_has_intent_engine: true,
1110 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1111 resolution_method: Some("Test Method".to_string()),
1112 };
1113
1114 assert!(info.env_var_set);
1115 assert!(info.env_var_path.is_some());
1116 assert!(info.env_var_valid.is_some());
1117 assert!(!info.directories_checked.is_empty());
1118 assert!(info.home_directory.is_some());
1119 assert!(info.final_database_path.is_some());
1120 assert!(info.resolution_method.is_some());
1121 }
1122
1123 #[test]
1125 fn test_get_database_path_info_home_directory() {
1126 let home_val = std::env::var("HOME");
1128 let userprofile_val = std::env::var("USERPROFILE");
1129 let has_home = home_val.is_ok();
1130 let has_userprofile = userprofile_val.is_ok();
1131 let info = ProjectContext::get_database_path_info();
1132
1133 if (has_home || has_userprofile) && !info.env_var_set && info.directories_checked.is_empty()
1139 {
1140 assert!(
1142 info.home_directory.is_some(),
1143 "HOME or USERPROFILE env var is set (HOME={:?}, USERPROFILE={:?}), but home_directory is None. Full info: {:?}",
1144 home_val.as_ref().map(|s| s.as_str()),
1145 userprofile_val.as_ref().map(|s| s.as_str()),
1146 info
1147 );
1148 }
1149 }
1150
1151 #[test]
1153 fn test_get_database_path_info_no_panic() {
1154 let info = ProjectContext::get_database_path_info();
1157
1158 assert!(!info.current_working_directory.is_empty());
1160
1161 if info.final_database_path.is_none() {
1164 let has_diagnostic_info = !info.directories_checked.is_empty()
1166 || info.env_var_set
1167 || info.home_directory.is_some();
1168
1169 assert!(
1170 has_diagnostic_info,
1171 "Even without finding a database, should provide diagnostic information"
1172 );
1173 }
1174 }
1175
1176 #[test]
1178 fn test_get_database_path_info_prefers_first_match() {
1179 let info = ProjectContext::get_database_path_info();
1180
1181 if info
1183 .resolution_method
1184 .as_ref()
1185 .is_some_and(|m| m.contains("Upward Directory"))
1186 && info.directories_checked.len() > 1
1187 {
1188 let with_ie: Vec<_> = info
1190 .directories_checked
1191 .iter()
1192 .filter(|d| d.has_intent_engine)
1193 .collect();
1194
1195 if with_ie.len() > 1 {
1196 let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1198 assert!(
1199 selected.len() <= 1,
1200 "Only the first .intent-engine found should be selected"
1201 );
1202 }
1203 }
1204 }
1205
1206 #[test]
1208 fn test_database_path_info_partial_deserialization() {
1209 let json = r#"{
1211 "current_working_directory": "/test",
1212 "env_var_set": false,
1213 "env_var_path": null,
1214 "env_var_valid": null,
1215 "directories_checked": [],
1216 "home_directory": null,
1217 "home_has_intent_engine": false,
1218 "final_database_path": null,
1219 "resolution_method": null
1220 }"#;
1221
1222 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1223 assert_eq!(info.current_working_directory, "/test");
1224 assert!(!info.env_var_set);
1225 }
1226
1227 #[test]
1229 fn test_database_path_info_json_schema() {
1230 let info = DatabasePathInfo {
1231 current_working_directory: "/test".to_string(),
1232 env_var_set: true,
1233 env_var_path: Some("/env".to_string()),
1234 env_var_valid: Some(true),
1235 directories_checked: vec![],
1236 home_directory: Some("/home".to_string()),
1237 home_has_intent_engine: false,
1238 final_database_path: Some("/db".to_string()),
1239 resolution_method: Some("Test".to_string()),
1240 };
1241
1242 let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1243
1244 assert!(json_value.get("current_working_directory").is_some());
1246 assert!(json_value.get("env_var_set").is_some());
1247 assert!(json_value.get("env_var_path").is_some());
1248 assert!(json_value.get("env_var_valid").is_some());
1249 assert!(json_value.get("directories_checked").is_some());
1250 assert!(json_value.get("home_directory").is_some());
1251 assert!(json_value.get("home_has_intent_engine").is_some());
1252 assert!(json_value.get("final_database_path").is_some());
1253 assert!(json_value.get("resolution_method").is_some());
1254 }
1255
1256 #[test]
1258 fn test_directory_traversal_info_empty_path() {
1259 let info = DirectoryTraversalInfo {
1260 path: "".to_string(),
1261 has_intent_engine: false,
1262 is_selected: false,
1263 };
1264
1265 assert_eq!(info.path, "");
1266 let json = serde_json::to_string(&info).unwrap();
1267 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1268 assert_eq!(deserialized.path, "");
1269 }
1270
1271 #[test]
1273 fn test_directory_traversal_info_unicode_path() {
1274 let info = DirectoryTraversalInfo {
1275 path: "/test/路径/مسار/путь".to_string(),
1276 has_intent_engine: true,
1277 is_selected: false,
1278 };
1279
1280 let json = serde_json::to_string(&info).unwrap();
1281 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1282 assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1283 }
1284
1285 #[test]
1287 fn test_database_path_info_long_paths() {
1288 let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1289 let info = DatabasePathInfo {
1290 current_working_directory: long_path.clone(),
1291 env_var_set: false,
1292 env_var_path: None,
1293 env_var_valid: None,
1294 directories_checked: vec![],
1295 home_directory: Some(long_path.clone()),
1296 home_has_intent_engine: false,
1297 final_database_path: Some(long_path.clone()),
1298 resolution_method: Some("Test".to_string()),
1299 };
1300
1301 let json = serde_json::to_string(&info).unwrap();
1302 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1303 assert_eq!(deserialized.current_working_directory, long_path);
1304 }
1305
1306 #[test]
1308 fn test_get_database_path_info_env_var_detection() {
1309 let info = ProjectContext::get_database_path_info();
1310
1311 if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1313 assert!(
1314 info.env_var_set,
1315 "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1316 );
1317 assert!(
1318 info.env_var_path.is_some(),
1319 "env_var_path should contain the path when env var is set"
1320 );
1321 assert!(
1322 info.env_var_valid.is_some(),
1323 "env_var_valid should be set when env var is present"
1324 );
1325 } else {
1326 assert!(
1327 !info.env_var_set,
1328 "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1329 );
1330 assert!(
1331 info.env_var_path.is_none(),
1332 "env_var_path should be None when env var is not set"
1333 );
1334 assert!(
1335 info.env_var_valid.is_none(),
1336 "env_var_valid should be None when env var is not set"
1337 );
1338 }
1339 }
1340}