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> {
376 let root = match Self::infer_project_root_from(&project_dir) {
378 Some(inferred_root) => {
379 inferred_root
381 },
382 None => {
383 project_dir
387 },
388 };
389
390 let intent_dir = root.join(INTENT_DIR);
391 let db_path = intent_dir.join(DB_FILE);
392
393 if !intent_dir.exists() {
395 std::fs::create_dir_all(&intent_dir)?;
396 }
397
398 let pool = create_pool(&db_path).await?;
400
401 run_migrations(&pool).await?;
403
404 Ok(ProjectContext {
405 root,
406 db_path,
407 pool,
408 })
409 }
410
411 pub async fn load() -> Result<Self> {
413 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
414 let intent_dir = root.join(INTENT_DIR);
415
416 if !intent_dir.exists() || !intent_dir.is_dir() {
419 return Err(IntentError::NotAProject);
420 }
421
422 let db_path = intent_dir.join(DB_FILE);
423
424 let pool = create_pool(&db_path).await?;
425
426 run_migrations(&pool).await?;
429
430 Ok(ProjectContext {
431 root,
432 db_path,
433 pool,
434 })
435 }
436
437 pub async fn load_or_init() -> Result<Self> {
439 match Self::load().await {
440 Ok(ctx) => Ok(ctx),
441 Err(IntentError::NotAProject) => Self::initialize_project().await,
442 Err(e) => Err(e),
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
456 fn test_constants() {
457 assert_eq!(INTENT_DIR, ".intent-engine");
458 assert_eq!(DB_FILE, "project.db");
459 }
460
461 #[test]
462 fn test_project_context_debug() {
463 let _type_check = |ctx: ProjectContext| {
466 let _ = format!("{:?}", ctx);
467 };
468 }
469
470 #[test]
471 fn test_project_root_markers_list() {
472 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
474 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
475 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
476 }
477
478 #[test]
479 fn test_project_root_markers_priority() {
480 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
482 }
483
484 #[test]
487 fn test_infer_project_root_with_git() {
488 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
492 }
493
494 #[test]
496 fn test_all_major_project_types_covered() {
497 let markers = PROJECT_ROOT_MARKERS;
498
499 assert!(markers.contains(&".git"));
501 assert!(markers.contains(&".hg"));
502
503 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")); }
511
512 #[test]
514 fn test_directory_traversal_info_creation() {
515 let info = DirectoryTraversalInfo {
516 path: "/test/path".to_string(),
517 has_intent_engine: true,
518 is_selected: false,
519 };
520
521 assert_eq!(info.path, "/test/path");
522 assert!(info.has_intent_engine);
523 assert!(!info.is_selected);
524 }
525
526 #[test]
528 fn test_directory_traversal_info_clone() {
529 let info = DirectoryTraversalInfo {
530 path: "/test/path".to_string(),
531 has_intent_engine: true,
532 is_selected: true,
533 };
534
535 let cloned = info.clone();
536 assert_eq!(cloned.path, info.path);
537 assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
538 assert_eq!(cloned.is_selected, info.is_selected);
539 }
540
541 #[test]
543 fn test_directory_traversal_info_debug() {
544 let info = DirectoryTraversalInfo {
545 path: "/test/path".to_string(),
546 has_intent_engine: false,
547 is_selected: true,
548 };
549
550 let debug_str = format!("{:?}", info);
551 assert!(debug_str.contains("DirectoryTraversalInfo"));
552 assert!(debug_str.contains("/test/path"));
553 }
554
555 #[test]
557 fn test_directory_traversal_info_serialization() {
558 let info = DirectoryTraversalInfo {
559 path: "/test/path".to_string(),
560 has_intent_engine: true,
561 is_selected: false,
562 };
563
564 let json = serde_json::to_string(&info).unwrap();
565 assert!(json.contains("path"));
566 assert!(json.contains("has_intent_engine"));
567 assert!(json.contains("is_selected"));
568 assert!(json.contains("/test/path"));
569 }
570
571 #[test]
573 fn test_directory_traversal_info_deserialization() {
574 let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
575 let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
576
577 assert_eq!(info.path, "/test/path");
578 assert!(info.has_intent_engine);
579 assert!(!info.is_selected);
580 }
581
582 #[test]
584 fn test_database_path_info_creation() {
585 let info = DatabasePathInfo {
586 current_working_directory: "/test/cwd".to_string(),
587 env_var_set: false,
588 env_var_path: None,
589 env_var_valid: None,
590 directories_checked: vec![],
591 home_directory: Some("/home/user".to_string()),
592 home_has_intent_engine: false,
593 final_database_path: Some("/test/db.db".to_string()),
594 resolution_method: Some("Test Method".to_string()),
595 };
596
597 assert_eq!(info.current_working_directory, "/test/cwd");
598 assert!(!info.env_var_set);
599 assert_eq!(info.env_var_path, None);
600 assert_eq!(info.home_directory, Some("/home/user".to_string()));
601 assert!(!info.home_has_intent_engine);
602 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
603 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
604 }
605
606 #[test]
608 fn test_database_path_info_with_env_var() {
609 let info = DatabasePathInfo {
610 current_working_directory: "/test/cwd".to_string(),
611 env_var_set: true,
612 env_var_path: Some("/env/path".to_string()),
613 env_var_valid: Some(true),
614 directories_checked: vec![],
615 home_directory: Some("/home/user".to_string()),
616 home_has_intent_engine: false,
617 final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
618 resolution_method: Some("Environment Variable".to_string()),
619 };
620
621 assert!(info.env_var_set);
622 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
623 assert_eq!(info.env_var_valid, Some(true));
624 assert_eq!(
625 info.resolution_method,
626 Some("Environment Variable".to_string())
627 );
628 }
629
630 #[test]
632 fn test_database_path_info_with_directories() {
633 let dirs = vec![
634 DirectoryTraversalInfo {
635 path: "/test/path1".to_string(),
636 has_intent_engine: false,
637 is_selected: false,
638 },
639 DirectoryTraversalInfo {
640 path: "/test/path2".to_string(),
641 has_intent_engine: true,
642 is_selected: true,
643 },
644 ];
645
646 let info = DatabasePathInfo {
647 current_working_directory: "/test/path1".to_string(),
648 env_var_set: false,
649 env_var_path: None,
650 env_var_valid: None,
651 directories_checked: dirs.clone(),
652 home_directory: Some("/home/user".to_string()),
653 home_has_intent_engine: false,
654 final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
655 resolution_method: Some("Upward Directory Traversal".to_string()),
656 };
657
658 assert_eq!(info.directories_checked.len(), 2);
659 assert!(!info.directories_checked[0].has_intent_engine);
660 assert!(info.directories_checked[1].has_intent_engine);
661 assert!(info.directories_checked[1].is_selected);
662 }
663
664 #[test]
666 fn test_database_path_info_debug() {
667 let info = DatabasePathInfo {
668 current_working_directory: "/test/cwd".to_string(),
669 env_var_set: false,
670 env_var_path: None,
671 env_var_valid: None,
672 directories_checked: vec![],
673 home_directory: Some("/home/user".to_string()),
674 home_has_intent_engine: false,
675 final_database_path: Some("/test/db.db".to_string()),
676 resolution_method: Some("Test".to_string()),
677 };
678
679 let debug_str = format!("{:?}", info);
680 assert!(debug_str.contains("DatabasePathInfo"));
681 assert!(debug_str.contains("/test/cwd"));
682 }
683
684 #[test]
686 fn test_database_path_info_serialization() {
687 let info = DatabasePathInfo {
688 current_working_directory: "/test/cwd".to_string(),
689 env_var_set: true,
690 env_var_path: Some("/env/path".to_string()),
691 env_var_valid: Some(true),
692 directories_checked: vec![],
693 home_directory: Some("/home/user".to_string()),
694 home_has_intent_engine: false,
695 final_database_path: Some("/test/db.db".to_string()),
696 resolution_method: Some("Test Method".to_string()),
697 };
698
699 let json = serde_json::to_string(&info).unwrap();
700 assert!(json.contains("current_working_directory"));
701 assert!(json.contains("env_var_set"));
702 assert!(json.contains("env_var_path"));
703 assert!(json.contains("final_database_path"));
704 assert!(json.contains("/test/cwd"));
705 assert!(json.contains("/env/path"));
706 }
707
708 #[test]
710 fn test_database_path_info_deserialization() {
711 let json = r#"{
712 "current_working_directory": "/test/cwd",
713 "env_var_set": true,
714 "env_var_path": "/env/path",
715 "env_var_valid": true,
716 "directories_checked": [],
717 "home_directory": "/home/user",
718 "home_has_intent_engine": false,
719 "final_database_path": "/test/db.db",
720 "resolution_method": "Test Method"
721 }"#;
722
723 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
724 assert_eq!(info.current_working_directory, "/test/cwd");
725 assert!(info.env_var_set);
726 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
727 assert_eq!(info.env_var_valid, Some(true));
728 assert_eq!(info.home_directory, Some("/home/user".to_string()));
729 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
730 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
731 }
732
733 #[test]
735 fn test_database_path_info_complete_structure() {
736 let dirs = vec![
737 DirectoryTraversalInfo {
738 path: "/home/user/project/src".to_string(),
739 has_intent_engine: false,
740 is_selected: false,
741 },
742 DirectoryTraversalInfo {
743 path: "/home/user/project".to_string(),
744 has_intent_engine: true,
745 is_selected: true,
746 },
747 DirectoryTraversalInfo {
748 path: "/home/user".to_string(),
749 has_intent_engine: false,
750 is_selected: false,
751 },
752 ];
753
754 let info = DatabasePathInfo {
755 current_working_directory: "/home/user/project/src".to_string(),
756 env_var_set: false,
757 env_var_path: None,
758 env_var_valid: None,
759 directories_checked: dirs,
760 home_directory: Some("/home/user".to_string()),
761 home_has_intent_engine: false,
762 final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
763 resolution_method: Some("Upward Directory Traversal".to_string()),
764 };
765
766 assert_eq!(info.directories_checked.len(), 3);
768 assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
769 assert_eq!(info.directories_checked[1].path, "/home/user/project");
770 assert_eq!(info.directories_checked[2].path, "/home/user");
771
772 assert!(!info.directories_checked[0].is_selected);
774 assert!(info.directories_checked[1].is_selected);
775 assert!(!info.directories_checked[2].is_selected);
776
777 assert!(!info.directories_checked[0].has_intent_engine);
779 assert!(info.directories_checked[1].has_intent_engine);
780 assert!(!info.directories_checked[2].has_intent_engine);
781 }
782
783 #[test]
785 fn test_get_database_path_info_structure() {
786 let info = ProjectContext::get_database_path_info();
787
788 assert!(!info.current_working_directory.is_empty());
790
791 let has_data = !info.directories_checked.is_empty()
793 || info.home_directory.is_some()
794 || info.env_var_set;
795
796 assert!(
797 has_data,
798 "get_database_path_info should return some directory information"
799 );
800 }
801
802 #[test]
804 fn test_get_database_path_info_checks_current_dir() {
805 let info = ProjectContext::get_database_path_info();
806
807 assert!(!info.current_working_directory.is_empty());
809
810 if !info.env_var_set || info.env_var_valid != Some(true) {
812 assert!(
813 !info.directories_checked.is_empty(),
814 "Should check at least the current directory"
815 );
816 }
817 }
818
819 #[test]
821 fn test_get_database_path_info_includes_cwd() {
822 let info = ProjectContext::get_database_path_info();
823
824 if !info.env_var_set || info.env_var_valid != Some(true) {
826 assert!(!info.directories_checked.is_empty());
827
828 let cwd = &info.current_working_directory;
830 let first_checked = &info.directories_checked[0].path;
831
832 assert!(
833 cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
834 "First checked directory should be related to CWD"
835 );
836 }
837 }
838
839 #[test]
841 fn test_get_database_path_info_resolution_method() {
842 let info = ProjectContext::get_database_path_info();
843
844 if info.final_database_path.is_some() {
846 assert!(
847 info.resolution_method.is_some(),
848 "Resolution method should be set when database path is found"
849 );
850
851 let method = info.resolution_method.unwrap();
852 assert!(
853 method.contains("Environment Variable")
854 || method.contains("Upward Directory Traversal")
855 || method.contains("Home Directory"),
856 "Resolution method should be one of the known strategies"
857 );
858 }
859 }
860
861 #[test]
863 fn test_get_database_path_info_selected_directory() {
864 let info = ProjectContext::get_database_path_info();
865
866 if (!info.env_var_set || info.env_var_valid != Some(true))
868 && !info.directories_checked.is_empty()
869 && info.final_database_path.is_some()
870 {
871 let selected_count = info
873 .directories_checked
874 .iter()
875 .filter(|d| d.is_selected)
876 .count();
877
878 assert!(
879 selected_count <= 1,
880 "At most one directory should be marked as selected"
881 );
882
883 if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
885 assert!(
886 selected.has_intent_engine,
887 "Selected directory should have .intent-engine"
888 );
889 }
890 }
891 }
892
893 #[test]
895 fn test_database_path_info_no_database_found() {
896 let info = DatabasePathInfo {
897 current_working_directory: "/test/path".to_string(),
898 env_var_set: false,
899 env_var_path: None,
900 env_var_valid: None,
901 directories_checked: vec![
902 DirectoryTraversalInfo {
903 path: "/test/path".to_string(),
904 has_intent_engine: false,
905 is_selected: false,
906 },
907 DirectoryTraversalInfo {
908 path: "/test".to_string(),
909 has_intent_engine: false,
910 is_selected: false,
911 },
912 ],
913 home_directory: Some("/home/user".to_string()),
914 home_has_intent_engine: false,
915 final_database_path: None,
916 resolution_method: None,
917 };
918
919 assert!(info.final_database_path.is_none());
920 assert!(info.resolution_method.is_none());
921 assert_eq!(info.directories_checked.len(), 2);
922 assert!(!info.home_has_intent_engine);
923 }
924
925 #[test]
927 fn test_database_path_info_env_var_invalid() {
928 let info = DatabasePathInfo {
929 current_working_directory: "/test/cwd".to_string(),
930 env_var_set: true,
931 env_var_path: Some("/invalid/path".to_string()),
932 env_var_valid: Some(false),
933 directories_checked: vec![DirectoryTraversalInfo {
934 path: "/test/cwd".to_string(),
935 has_intent_engine: true,
936 is_selected: true,
937 }],
938 home_directory: Some("/home/user".to_string()),
939 home_has_intent_engine: false,
940 final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
941 resolution_method: Some("Upward Directory Traversal".to_string()),
942 };
943
944 assert!(info.env_var_set);
945 assert_eq!(info.env_var_valid, Some(false));
946 assert!(info.final_database_path.is_some());
947 assert!(info.resolution_method.unwrap().contains("Upward Directory"));
949 }
950
951 #[test]
953 fn test_database_path_info_home_directory_used() {
954 let info = DatabasePathInfo {
955 current_working_directory: "/tmp/work".to_string(),
956 env_var_set: false,
957 env_var_path: None,
958 env_var_valid: None,
959 directories_checked: vec![
960 DirectoryTraversalInfo {
961 path: "/tmp/work".to_string(),
962 has_intent_engine: false,
963 is_selected: false,
964 },
965 DirectoryTraversalInfo {
966 path: "/tmp".to_string(),
967 has_intent_engine: false,
968 is_selected: false,
969 },
970 ],
971 home_directory: Some("/home/user".to_string()),
972 home_has_intent_engine: true,
973 final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
974 resolution_method: Some("Home Directory Fallback".to_string()),
975 };
976
977 assert!(info.home_has_intent_engine);
978 assert_eq!(
979 info.final_database_path,
980 Some("/home/user/.intent-engine/project.db".to_string())
981 );
982 assert_eq!(
983 info.resolution_method,
984 Some("Home Directory Fallback".to_string())
985 );
986 }
987
988 #[test]
990 fn test_database_path_info_full_roundtrip() {
991 let original = DatabasePathInfo {
992 current_working_directory: "/test/cwd".to_string(),
993 env_var_set: true,
994 env_var_path: Some("/env/path".to_string()),
995 env_var_valid: Some(false),
996 directories_checked: vec![
997 DirectoryTraversalInfo {
998 path: "/test/cwd".to_string(),
999 has_intent_engine: false,
1000 is_selected: false,
1001 },
1002 DirectoryTraversalInfo {
1003 path: "/test".to_string(),
1004 has_intent_engine: true,
1005 is_selected: true,
1006 },
1007 ],
1008 home_directory: Some("/home/user".to_string()),
1009 home_has_intent_engine: false,
1010 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1011 resolution_method: Some("Upward Directory Traversal".to_string()),
1012 };
1013
1014 let json = serde_json::to_string(&original).unwrap();
1016
1017 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1019
1020 assert_eq!(
1022 deserialized.current_working_directory,
1023 original.current_working_directory
1024 );
1025 assert_eq!(deserialized.env_var_set, original.env_var_set);
1026 assert_eq!(deserialized.env_var_path, original.env_var_path);
1027 assert_eq!(deserialized.env_var_valid, original.env_var_valid);
1028 assert_eq!(
1029 deserialized.directories_checked.len(),
1030 original.directories_checked.len()
1031 );
1032 assert_eq!(deserialized.home_directory, original.home_directory);
1033 assert_eq!(
1034 deserialized.home_has_intent_engine,
1035 original.home_has_intent_engine
1036 );
1037 assert_eq!(
1038 deserialized.final_database_path,
1039 original.final_database_path
1040 );
1041 assert_eq!(deserialized.resolution_method, original.resolution_method);
1042 }
1043
1044 #[test]
1046 fn test_directory_traversal_info_all_combinations() {
1047 let combinations = [(false, false), (false, true), (true, false), (true, true)];
1049
1050 for (has_ie, is_sel) in combinations.iter() {
1051 let info = DirectoryTraversalInfo {
1052 path: format!("/test/path/{}_{}", has_ie, is_sel),
1053 has_intent_engine: *has_ie,
1054 is_selected: *is_sel,
1055 };
1056
1057 assert_eq!(info.has_intent_engine, *has_ie);
1058 assert_eq!(info.is_selected, *is_sel);
1059 }
1060 }
1061
1062 #[test]
1064 fn test_directory_traversal_info_exact_serialization() {
1065 let info = DirectoryTraversalInfo {
1066 path: "/exact/path/with/special-chars_123".to_string(),
1067 has_intent_engine: true,
1068 is_selected: false,
1069 };
1070
1071 let json = serde_json::to_string(&info).unwrap();
1072 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1073
1074 assert_eq!(info.path, deserialized.path);
1075 assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
1076 assert_eq!(info.is_selected, deserialized.is_selected);
1077 }
1078
1079 #[test]
1081 fn test_database_path_info_all_none() {
1082 let info = DatabasePathInfo {
1083 current_working_directory: "/test".to_string(),
1084 env_var_set: false,
1085 env_var_path: None,
1086 env_var_valid: None,
1087 directories_checked: vec![],
1088 home_directory: None,
1089 home_has_intent_engine: false,
1090 final_database_path: None,
1091 resolution_method: None,
1092 };
1093
1094 assert!(!info.env_var_set);
1095 assert!(info.env_var_path.is_none());
1096 assert!(info.env_var_valid.is_none());
1097 assert!(info.directories_checked.is_empty());
1098 assert!(info.home_directory.is_none());
1099 assert!(info.final_database_path.is_none());
1100 assert!(info.resolution_method.is_none());
1101 }
1102
1103 #[test]
1105 fn test_database_path_info_all_some() {
1106 let info = DatabasePathInfo {
1107 current_working_directory: "/test".to_string(),
1108 env_var_set: true,
1109 env_var_path: Some("/env".to_string()),
1110 env_var_valid: Some(true),
1111 directories_checked: vec![DirectoryTraversalInfo {
1112 path: "/test".to_string(),
1113 has_intent_engine: true,
1114 is_selected: true,
1115 }],
1116 home_directory: Some("/home".to_string()),
1117 home_has_intent_engine: true,
1118 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1119 resolution_method: Some("Test Method".to_string()),
1120 };
1121
1122 assert!(info.env_var_set);
1123 assert!(info.env_var_path.is_some());
1124 assert!(info.env_var_valid.is_some());
1125 assert!(!info.directories_checked.is_empty());
1126 assert!(info.home_directory.is_some());
1127 assert!(info.final_database_path.is_some());
1128 assert!(info.resolution_method.is_some());
1129 }
1130
1131 #[test]
1133 fn test_get_database_path_info_home_directory() {
1134 let info = ProjectContext::get_database_path_info();
1135
1136 if std::env::var("HOME").is_ok() {
1139 assert!(
1140 info.home_directory.is_some(),
1141 "HOME env var is set, so home_directory should be Some"
1142 );
1143 }
1144 }
1145
1146 #[test]
1148 fn test_get_database_path_info_no_panic() {
1149 let info = ProjectContext::get_database_path_info();
1152
1153 assert!(!info.current_working_directory.is_empty());
1155
1156 if info.final_database_path.is_none() {
1159 let has_diagnostic_info = !info.directories_checked.is_empty()
1161 || info.env_var_set
1162 || info.home_directory.is_some();
1163
1164 assert!(
1165 has_diagnostic_info,
1166 "Even without finding a database, should provide diagnostic information"
1167 );
1168 }
1169 }
1170
1171 #[test]
1173 fn test_get_database_path_info_prefers_first_match() {
1174 let info = ProjectContext::get_database_path_info();
1175
1176 if info
1178 .resolution_method
1179 .as_ref()
1180 .is_some_and(|m| m.contains("Upward Directory"))
1181 && info.directories_checked.len() > 1
1182 {
1183 let with_ie: Vec<_> = info
1185 .directories_checked
1186 .iter()
1187 .filter(|d| d.has_intent_engine)
1188 .collect();
1189
1190 if with_ie.len() > 1 {
1191 let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1193 assert!(
1194 selected.len() <= 1,
1195 "Only the first .intent-engine found should be selected"
1196 );
1197 }
1198 }
1199 }
1200
1201 #[test]
1203 fn test_database_path_info_partial_deserialization() {
1204 let json = r#"{
1206 "current_working_directory": "/test",
1207 "env_var_set": false,
1208 "env_var_path": null,
1209 "env_var_valid": null,
1210 "directories_checked": [],
1211 "home_directory": null,
1212 "home_has_intent_engine": false,
1213 "final_database_path": null,
1214 "resolution_method": null
1215 }"#;
1216
1217 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1218 assert_eq!(info.current_working_directory, "/test");
1219 assert!(!info.env_var_set);
1220 }
1221
1222 #[test]
1224 fn test_database_path_info_json_schema() {
1225 let info = DatabasePathInfo {
1226 current_working_directory: "/test".to_string(),
1227 env_var_set: true,
1228 env_var_path: Some("/env".to_string()),
1229 env_var_valid: Some(true),
1230 directories_checked: vec![],
1231 home_directory: Some("/home".to_string()),
1232 home_has_intent_engine: false,
1233 final_database_path: Some("/db".to_string()),
1234 resolution_method: Some("Test".to_string()),
1235 };
1236
1237 let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1238
1239 assert!(json_value.get("current_working_directory").is_some());
1241 assert!(json_value.get("env_var_set").is_some());
1242 assert!(json_value.get("env_var_path").is_some());
1243 assert!(json_value.get("env_var_valid").is_some());
1244 assert!(json_value.get("directories_checked").is_some());
1245 assert!(json_value.get("home_directory").is_some());
1246 assert!(json_value.get("home_has_intent_engine").is_some());
1247 assert!(json_value.get("final_database_path").is_some());
1248 assert!(json_value.get("resolution_method").is_some());
1249 }
1250
1251 #[test]
1253 fn test_directory_traversal_info_empty_path() {
1254 let info = DirectoryTraversalInfo {
1255 path: "".to_string(),
1256 has_intent_engine: false,
1257 is_selected: false,
1258 };
1259
1260 assert_eq!(info.path, "");
1261 let json = serde_json::to_string(&info).unwrap();
1262 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1263 assert_eq!(deserialized.path, "");
1264 }
1265
1266 #[test]
1268 fn test_directory_traversal_info_unicode_path() {
1269 let info = DirectoryTraversalInfo {
1270 path: "/test/路径/مسار/путь".to_string(),
1271 has_intent_engine: true,
1272 is_selected: false,
1273 };
1274
1275 let json = serde_json::to_string(&info).unwrap();
1276 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1277 assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1278 }
1279
1280 #[test]
1282 fn test_database_path_info_long_paths() {
1283 let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1284 let info = DatabasePathInfo {
1285 current_working_directory: long_path.clone(),
1286 env_var_set: false,
1287 env_var_path: None,
1288 env_var_valid: None,
1289 directories_checked: vec![],
1290 home_directory: Some(long_path.clone()),
1291 home_has_intent_engine: false,
1292 final_database_path: Some(long_path.clone()),
1293 resolution_method: Some("Test".to_string()),
1294 };
1295
1296 let json = serde_json::to_string(&info).unwrap();
1297 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1298 assert_eq!(deserialized.current_working_directory, long_path);
1299 }
1300
1301 #[test]
1303 fn test_get_database_path_info_env_var_detection() {
1304 let info = ProjectContext::get_database_path_info();
1305
1306 if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1308 assert!(
1309 info.env_var_set,
1310 "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1311 );
1312 assert!(
1313 info.env_var_path.is_some(),
1314 "env_var_path should contain the path when env var is set"
1315 );
1316 assert!(
1317 info.env_var_valid.is_some(),
1318 "env_var_valid should be set when env var is present"
1319 );
1320 } else {
1321 assert!(
1322 !info.env_var_set,
1323 "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1324 );
1325 assert!(
1326 info.env_var_path.is_none(),
1327 "env_var_path should be None when env var is not set"
1328 );
1329 assert!(
1330 info.env_var_valid.is_none(),
1331 "env_var_valid should be None when env var is not set"
1332 );
1333 }
1334 }
1335}