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