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 }
88
89 if let Ok(mut current) = std::env::current_dir() {
91 loop {
92 let intent_dir = current.join(INTENT_DIR);
93 let has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
94
95 let is_selected = has_intent_engine && info.final_database_path.is_none();
96
97 info.directories_checked.push(DirectoryTraversalInfo {
98 path: current.display().to_string(),
99 has_intent_engine,
100 is_selected,
101 });
102
103 if has_intent_engine && info.final_database_path.is_none() {
104 let db_path = intent_dir.join(DB_FILE);
105 info.final_database_path = Some(db_path.display().to_string());
106 info.resolution_method = Some("Upward Directory Traversal".to_string());
107 }
109
110 if !current.pop() {
111 break;
112 }
113 }
114 }
115
116 #[cfg(not(target_os = "windows"))]
118 let home_path = std::env::var("HOME").ok().map(PathBuf::from);
119
120 #[cfg(target_os = "windows")]
121 let home_path = std::env::var("HOME")
122 .ok()
123 .map(PathBuf::from)
124 .or_else(|| std::env::var("USERPROFILE").ok().map(PathBuf::from));
125
126 if let Some(home) = home_path {
127 info.home_directory = Some(home.display().to_string());
128 let intent_dir = home.join(INTENT_DIR);
129 info.home_has_intent_engine = intent_dir.exists() && intent_dir.is_dir();
130
131 if info.home_has_intent_engine && info.final_database_path.is_none() {
132 let db_path = intent_dir.join(DB_FILE);
133 info.final_database_path = Some(db_path.display().to_string());
134 info.resolution_method = Some("Home Directory Fallback".to_string());
135 }
136 }
137
138 info
139 }
140
141 pub fn find_project_root() -> Option<PathBuf> {
152 if let Ok(current_dir) = std::env::current_dir() {
156 let start_dir = current_dir.clone();
157
158 let project_boundary = Self::infer_project_root();
161
162 let mut current = start_dir.clone();
163 loop {
164 let intent_dir = current.join(INTENT_DIR);
165 if intent_dir.exists() && intent_dir.is_dir() {
166 if let Some(ref boundary) = project_boundary {
172 if !current.starts_with(boundary) && current != *boundary {
175 break;
178 }
179 }
180
181 if current != start_dir {
182 eprintln!("✓ Found project: {}", current.display());
183 }
184 return Some(current);
185 }
186
187 if let Some(ref boundary) = project_boundary {
190 if current == *boundary {
191 eprintln!("✓ Detected project root: {}", boundary.display());
194 return Some(boundary.clone());
195 }
196 }
197
198 if !current.pop() {
199 break;
200 }
201 }
202 }
203
204 if let Ok(home) = std::env::var("HOME") {
207 let home_path = PathBuf::from(home);
208 let intent_dir = home_path.join(INTENT_DIR);
209 if intent_dir.exists() && intent_dir.is_dir() {
210 eprintln!("✓ Using home project: {}", home_path.display());
211 return Some(home_path);
212 }
213 }
214
215 #[cfg(target_os = "windows")]
217 if let Ok(userprofile) = std::env::var("USERPROFILE") {
218 let home_path = PathBuf::from(userprofile);
219 let intent_dir = home_path.join(INTENT_DIR);
220 if intent_dir.exists() && intent_dir.is_dir() {
221 eprintln!("✓ Using home project: {}", home_path.display());
222 return Some(home_path);
223 }
224 }
225
226 None
227 }
228
229 fn infer_project_root_from(start_path: &std::path::Path) -> Option<PathBuf> {
241 let mut current = start_path.to_path_buf();
242
243 loop {
244 for marker in PROJECT_ROOT_MARKERS {
246 let marker_path = current.join(marker);
247 if marker_path.exists() {
248 return Some(current);
249 }
250 }
251
252 if !current.pop() {
254 break;
256 }
257 }
258
259 None
260 }
261
262 fn infer_project_root() -> Option<PathBuf> {
270 let cwd = std::env::current_dir().ok()?;
271 Self::infer_project_root_from(&cwd)
272 }
273
274 pub async fn initialize_project() -> Result<Self> {
281 let cwd = std::env::current_dir()?;
282
283 let root = match Self::infer_project_root() {
285 Some(inferred_root) => {
286 inferred_root
288 },
289 None => {
290 eprintln!(
293 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
294 Initialized Intent-Engine in the current directory '{}'.\n\
295 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
296 cwd.display()
297 );
298 cwd
299 },
300 };
301
302 let intent_dir = root.join(INTENT_DIR);
303 let db_path = intent_dir.join(DB_FILE);
304
305 if !intent_dir.exists() {
307 std::fs::create_dir_all(&intent_dir)?;
308 }
309
310 let pool = create_pool(&db_path).await?;
312
313 run_migrations(&pool).await?;
315
316 Ok(ProjectContext {
317 root,
318 db_path,
319 pool,
320 })
321 }
322
323 pub async fn initialize_project_at(project_dir: PathBuf) -> Result<Self> {
354 let root = project_dir;
357
358 let intent_dir = root.join(INTENT_DIR);
359 let db_path = intent_dir.join(DB_FILE);
360
361 if !intent_dir.exists() {
363 std::fs::create_dir_all(&intent_dir)?;
364 }
365
366 let pool = create_pool(&db_path).await?;
368
369 run_migrations(&pool).await?;
371
372 Ok(ProjectContext {
373 root,
374 db_path,
375 pool,
376 })
377 }
378
379 pub async fn load() -> Result<Self> {
381 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
382 let intent_dir = root.join(INTENT_DIR);
383
384 if !intent_dir.exists() || !intent_dir.is_dir() {
387 return Err(IntentError::NotAProject);
388 }
389
390 let db_path = intent_dir.join(DB_FILE);
391
392 let pool = create_pool(&db_path).await?;
393
394 run_migrations(&pool).await?;
397
398 Ok(ProjectContext {
399 root,
400 db_path,
401 pool,
402 })
403 }
404
405 pub async fn load_or_init() -> Result<Self> {
407 match Self::load().await {
408 Ok(ctx) => Ok(ctx),
409 Err(IntentError::NotAProject) => Self::initialize_project().await,
410 Err(e) => Err(e),
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
424 fn test_constants() {
425 assert_eq!(INTENT_DIR, ".intent-engine");
426 assert_eq!(DB_FILE, "project.db");
427 }
428
429 #[test]
430 fn test_project_context_debug() {
431 let _type_check = |ctx: ProjectContext| {
434 let _ = format!("{:?}", ctx);
435 };
436 }
437
438 #[test]
439 fn test_project_root_markers_list() {
440 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
442 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
443 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
444 }
445
446 #[test]
447 fn test_project_root_markers_priority() {
448 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
450 }
451
452 #[test]
455 fn test_infer_project_root_with_git() {
456 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
460 }
461
462 #[test]
464 fn test_all_major_project_types_covered() {
465 let markers = PROJECT_ROOT_MARKERS;
466
467 assert!(markers.contains(&".git"));
469 assert!(markers.contains(&".hg"));
470
471 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")); }
479
480 #[test]
482 fn test_directory_traversal_info_creation() {
483 let info = DirectoryTraversalInfo {
484 path: "/test/path".to_string(),
485 has_intent_engine: true,
486 is_selected: false,
487 };
488
489 assert_eq!(info.path, "/test/path");
490 assert!(info.has_intent_engine);
491 assert!(!info.is_selected);
492 }
493
494 #[test]
496 fn test_directory_traversal_info_clone() {
497 let info = DirectoryTraversalInfo {
498 path: "/test/path".to_string(),
499 has_intent_engine: true,
500 is_selected: true,
501 };
502
503 let cloned = info.clone();
504 assert_eq!(cloned.path, info.path);
505 assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
506 assert_eq!(cloned.is_selected, info.is_selected);
507 }
508
509 #[test]
511 fn test_directory_traversal_info_debug() {
512 let info = DirectoryTraversalInfo {
513 path: "/test/path".to_string(),
514 has_intent_engine: false,
515 is_selected: true,
516 };
517
518 let debug_str = format!("{:?}", info);
519 assert!(debug_str.contains("DirectoryTraversalInfo"));
520 assert!(debug_str.contains("/test/path"));
521 }
522
523 #[test]
525 fn test_directory_traversal_info_serialization() {
526 let info = DirectoryTraversalInfo {
527 path: "/test/path".to_string(),
528 has_intent_engine: true,
529 is_selected: false,
530 };
531
532 let json = serde_json::to_string(&info).unwrap();
533 assert!(json.contains("path"));
534 assert!(json.contains("has_intent_engine"));
535 assert!(json.contains("is_selected"));
536 assert!(json.contains("/test/path"));
537 }
538
539 #[test]
541 fn test_directory_traversal_info_deserialization() {
542 let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
543 let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
544
545 assert_eq!(info.path, "/test/path");
546 assert!(info.has_intent_engine);
547 assert!(!info.is_selected);
548 }
549
550 #[test]
552 fn test_database_path_info_creation() {
553 let info = DatabasePathInfo {
554 current_working_directory: "/test/cwd".to_string(),
555 env_var_set: false,
556 env_var_path: None,
557 env_var_valid: None,
558 directories_checked: vec![],
559 home_directory: Some("/home/user".to_string()),
560 home_has_intent_engine: false,
561 final_database_path: Some("/test/db.db".to_string()),
562 resolution_method: Some("Test Method".to_string()),
563 };
564
565 assert_eq!(info.current_working_directory, "/test/cwd");
566 assert!(!info.env_var_set);
567 assert_eq!(info.env_var_path, None);
568 assert_eq!(info.home_directory, Some("/home/user".to_string()));
569 assert!(!info.home_has_intent_engine);
570 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
571 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
572 }
573
574 #[test]
576 fn test_database_path_info_with_env_var() {
577 let info = DatabasePathInfo {
578 current_working_directory: "/test/cwd".to_string(),
579 env_var_set: true,
580 env_var_path: Some("/env/path".to_string()),
581 env_var_valid: Some(true),
582 directories_checked: vec![],
583 home_directory: Some("/home/user".to_string()),
584 home_has_intent_engine: false,
585 final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
586 resolution_method: Some("Environment Variable".to_string()),
587 };
588
589 assert!(info.env_var_set);
590 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
591 assert_eq!(info.env_var_valid, Some(true));
592 assert_eq!(
593 info.resolution_method,
594 Some("Environment Variable".to_string())
595 );
596 }
597
598 #[test]
600 fn test_database_path_info_with_directories() {
601 let dirs = vec![
602 DirectoryTraversalInfo {
603 path: "/test/path1".to_string(),
604 has_intent_engine: false,
605 is_selected: false,
606 },
607 DirectoryTraversalInfo {
608 path: "/test/path2".to_string(),
609 has_intent_engine: true,
610 is_selected: true,
611 },
612 ];
613
614 let info = DatabasePathInfo {
615 current_working_directory: "/test/path1".to_string(),
616 env_var_set: false,
617 env_var_path: None,
618 env_var_valid: None,
619 directories_checked: dirs.clone(),
620 home_directory: Some("/home/user".to_string()),
621 home_has_intent_engine: false,
622 final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
623 resolution_method: Some("Upward Directory Traversal".to_string()),
624 };
625
626 assert_eq!(info.directories_checked.len(), 2);
627 assert!(!info.directories_checked[0].has_intent_engine);
628 assert!(info.directories_checked[1].has_intent_engine);
629 assert!(info.directories_checked[1].is_selected);
630 }
631
632 #[test]
634 fn test_database_path_info_debug() {
635 let info = DatabasePathInfo {
636 current_working_directory: "/test/cwd".to_string(),
637 env_var_set: false,
638 env_var_path: None,
639 env_var_valid: None,
640 directories_checked: vec![],
641 home_directory: Some("/home/user".to_string()),
642 home_has_intent_engine: false,
643 final_database_path: Some("/test/db.db".to_string()),
644 resolution_method: Some("Test".to_string()),
645 };
646
647 let debug_str = format!("{:?}", info);
648 assert!(debug_str.contains("DatabasePathInfo"));
649 assert!(debug_str.contains("/test/cwd"));
650 }
651
652 #[test]
654 fn test_database_path_info_serialization() {
655 let info = DatabasePathInfo {
656 current_working_directory: "/test/cwd".to_string(),
657 env_var_set: true,
658 env_var_path: Some("/env/path".to_string()),
659 env_var_valid: Some(true),
660 directories_checked: vec![],
661 home_directory: Some("/home/user".to_string()),
662 home_has_intent_engine: false,
663 final_database_path: Some("/test/db.db".to_string()),
664 resolution_method: Some("Test Method".to_string()),
665 };
666
667 let json = serde_json::to_string(&info).unwrap();
668 assert!(json.contains("current_working_directory"));
669 assert!(json.contains("env_var_set"));
670 assert!(json.contains("env_var_path"));
671 assert!(json.contains("final_database_path"));
672 assert!(json.contains("/test/cwd"));
673 assert!(json.contains("/env/path"));
674 }
675
676 #[test]
678 fn test_database_path_info_deserialization() {
679 let json = r#"{
680 "current_working_directory": "/test/cwd",
681 "env_var_set": true,
682 "env_var_path": "/env/path",
683 "env_var_valid": true,
684 "directories_checked": [],
685 "home_directory": "/home/user",
686 "home_has_intent_engine": false,
687 "final_database_path": "/test/db.db",
688 "resolution_method": "Test Method"
689 }"#;
690
691 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
692 assert_eq!(info.current_working_directory, "/test/cwd");
693 assert!(info.env_var_set);
694 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
695 assert_eq!(info.env_var_valid, Some(true));
696 assert_eq!(info.home_directory, Some("/home/user".to_string()));
697 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
698 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
699 }
700
701 #[test]
703 fn test_database_path_info_complete_structure() {
704 let dirs = vec![
705 DirectoryTraversalInfo {
706 path: "/home/user/project/src".to_string(),
707 has_intent_engine: false,
708 is_selected: false,
709 },
710 DirectoryTraversalInfo {
711 path: "/home/user/project".to_string(),
712 has_intent_engine: true,
713 is_selected: true,
714 },
715 DirectoryTraversalInfo {
716 path: "/home/user".to_string(),
717 has_intent_engine: false,
718 is_selected: false,
719 },
720 ];
721
722 let info = DatabasePathInfo {
723 current_working_directory: "/home/user/project/src".to_string(),
724 env_var_set: false,
725 env_var_path: None,
726 env_var_valid: None,
727 directories_checked: dirs,
728 home_directory: Some("/home/user".to_string()),
729 home_has_intent_engine: false,
730 final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
731 resolution_method: Some("Upward Directory Traversal".to_string()),
732 };
733
734 assert_eq!(info.directories_checked.len(), 3);
736 assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
737 assert_eq!(info.directories_checked[1].path, "/home/user/project");
738 assert_eq!(info.directories_checked[2].path, "/home/user");
739
740 assert!(!info.directories_checked[0].is_selected);
742 assert!(info.directories_checked[1].is_selected);
743 assert!(!info.directories_checked[2].is_selected);
744
745 assert!(!info.directories_checked[0].has_intent_engine);
747 assert!(info.directories_checked[1].has_intent_engine);
748 assert!(!info.directories_checked[2].has_intent_engine);
749 }
750
751 #[test]
753 fn test_get_database_path_info_structure() {
754 let info = ProjectContext::get_database_path_info();
755
756 assert!(!info.current_working_directory.is_empty());
758
759 let has_data = !info.directories_checked.is_empty()
761 || info.home_directory.is_some()
762 || info.env_var_set;
763
764 assert!(
765 has_data,
766 "get_database_path_info should return some directory information"
767 );
768 }
769
770 #[test]
772 fn test_get_database_path_info_checks_current_dir() {
773 let info = ProjectContext::get_database_path_info();
774
775 assert!(!info.current_working_directory.is_empty());
777
778 if !info.env_var_set || info.env_var_valid != Some(true) {
780 assert!(
781 !info.directories_checked.is_empty(),
782 "Should check at least the current directory"
783 );
784 }
785 }
786
787 #[test]
789 fn test_get_database_path_info_includes_cwd() {
790 let info = ProjectContext::get_database_path_info();
791
792 if !info.env_var_set || info.env_var_valid != Some(true) {
794 assert!(!info.directories_checked.is_empty());
795
796 let cwd = &info.current_working_directory;
798 let first_checked = &info.directories_checked[0].path;
799
800 assert!(
801 cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
802 "First checked directory should be related to CWD"
803 );
804 }
805 }
806
807 #[test]
809 fn test_get_database_path_info_resolution_method() {
810 let info = ProjectContext::get_database_path_info();
811
812 if info.final_database_path.is_some() {
814 assert!(
815 info.resolution_method.is_some(),
816 "Resolution method should be set when database path is found"
817 );
818
819 let method = info.resolution_method.unwrap();
820 assert!(
821 method.contains("Environment Variable")
822 || method.contains("Upward Directory Traversal")
823 || method.contains("Home Directory"),
824 "Resolution method should be one of the known strategies"
825 );
826 }
827 }
828
829 #[test]
831 fn test_get_database_path_info_selected_directory() {
832 let info = ProjectContext::get_database_path_info();
833
834 if (!info.env_var_set || info.env_var_valid != Some(true))
836 && !info.directories_checked.is_empty()
837 && info.final_database_path.is_some()
838 {
839 let selected_count = info
841 .directories_checked
842 .iter()
843 .filter(|d| d.is_selected)
844 .count();
845
846 assert!(
847 selected_count <= 1,
848 "At most one directory should be marked as selected"
849 );
850
851 if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
853 assert!(
854 selected.has_intent_engine,
855 "Selected directory should have .intent-engine"
856 );
857 }
858 }
859 }
860
861 #[test]
863 fn test_database_path_info_no_database_found() {
864 let info = DatabasePathInfo {
865 current_working_directory: "/test/path".to_string(),
866 env_var_set: false,
867 env_var_path: None,
868 env_var_valid: None,
869 directories_checked: vec![
870 DirectoryTraversalInfo {
871 path: "/test/path".to_string(),
872 has_intent_engine: false,
873 is_selected: false,
874 },
875 DirectoryTraversalInfo {
876 path: "/test".to_string(),
877 has_intent_engine: false,
878 is_selected: false,
879 },
880 ],
881 home_directory: Some("/home/user".to_string()),
882 home_has_intent_engine: false,
883 final_database_path: None,
884 resolution_method: None,
885 };
886
887 assert!(info.final_database_path.is_none());
888 assert!(info.resolution_method.is_none());
889 assert_eq!(info.directories_checked.len(), 2);
890 assert!(!info.home_has_intent_engine);
891 }
892
893 #[test]
895 fn test_database_path_info_env_var_invalid() {
896 let info = DatabasePathInfo {
897 current_working_directory: "/test/cwd".to_string(),
898 env_var_set: true,
899 env_var_path: Some("/invalid/path".to_string()),
900 env_var_valid: Some(false),
901 directories_checked: vec![DirectoryTraversalInfo {
902 path: "/test/cwd".to_string(),
903 has_intent_engine: true,
904 is_selected: true,
905 }],
906 home_directory: Some("/home/user".to_string()),
907 home_has_intent_engine: false,
908 final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
909 resolution_method: Some("Upward Directory Traversal".to_string()),
910 };
911
912 assert!(info.env_var_set);
913 assert_eq!(info.env_var_valid, Some(false));
914 assert!(info.final_database_path.is_some());
915 assert!(info.resolution_method.unwrap().contains("Upward Directory"));
917 }
918
919 #[test]
921 fn test_database_path_info_home_directory_used() {
922 let info = DatabasePathInfo {
923 current_working_directory: "/tmp/work".to_string(),
924 env_var_set: false,
925 env_var_path: None,
926 env_var_valid: None,
927 directories_checked: vec![
928 DirectoryTraversalInfo {
929 path: "/tmp/work".to_string(),
930 has_intent_engine: false,
931 is_selected: false,
932 },
933 DirectoryTraversalInfo {
934 path: "/tmp".to_string(),
935 has_intent_engine: false,
936 is_selected: false,
937 },
938 ],
939 home_directory: Some("/home/user".to_string()),
940 home_has_intent_engine: true,
941 final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
942 resolution_method: Some("Home Directory Fallback".to_string()),
943 };
944
945 assert!(info.home_has_intent_engine);
946 assert_eq!(
947 info.final_database_path,
948 Some("/home/user/.intent-engine/project.db".to_string())
949 );
950 assert_eq!(
951 info.resolution_method,
952 Some("Home Directory Fallback".to_string())
953 );
954 }
955
956 #[test]
958 fn test_database_path_info_full_roundtrip() {
959 let original = DatabasePathInfo {
960 current_working_directory: "/test/cwd".to_string(),
961 env_var_set: true,
962 env_var_path: Some("/env/path".to_string()),
963 env_var_valid: Some(false),
964 directories_checked: vec![
965 DirectoryTraversalInfo {
966 path: "/test/cwd".to_string(),
967 has_intent_engine: false,
968 is_selected: false,
969 },
970 DirectoryTraversalInfo {
971 path: "/test".to_string(),
972 has_intent_engine: true,
973 is_selected: true,
974 },
975 ],
976 home_directory: Some("/home/user".to_string()),
977 home_has_intent_engine: false,
978 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
979 resolution_method: Some("Upward Directory Traversal".to_string()),
980 };
981
982 let json = serde_json::to_string(&original).unwrap();
984
985 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
987
988 assert_eq!(
990 deserialized.current_working_directory,
991 original.current_working_directory
992 );
993 assert_eq!(deserialized.env_var_set, original.env_var_set);
994 assert_eq!(deserialized.env_var_path, original.env_var_path);
995 assert_eq!(deserialized.env_var_valid, original.env_var_valid);
996 assert_eq!(
997 deserialized.directories_checked.len(),
998 original.directories_checked.len()
999 );
1000 assert_eq!(deserialized.home_directory, original.home_directory);
1001 assert_eq!(
1002 deserialized.home_has_intent_engine,
1003 original.home_has_intent_engine
1004 );
1005 assert_eq!(
1006 deserialized.final_database_path,
1007 original.final_database_path
1008 );
1009 assert_eq!(deserialized.resolution_method, original.resolution_method);
1010 }
1011
1012 #[test]
1014 fn test_directory_traversal_info_all_combinations() {
1015 let combinations = [(false, false), (false, true), (true, false), (true, true)];
1017
1018 for (has_ie, is_sel) in combinations.iter() {
1019 let info = DirectoryTraversalInfo {
1020 path: format!("/test/path/{}_{}", has_ie, is_sel),
1021 has_intent_engine: *has_ie,
1022 is_selected: *is_sel,
1023 };
1024
1025 assert_eq!(info.has_intent_engine, *has_ie);
1026 assert_eq!(info.is_selected, *is_sel);
1027 }
1028 }
1029
1030 #[test]
1032 fn test_directory_traversal_info_exact_serialization() {
1033 let info = DirectoryTraversalInfo {
1034 path: "/exact/path/with/special-chars_123".to_string(),
1035 has_intent_engine: true,
1036 is_selected: false,
1037 };
1038
1039 let json = serde_json::to_string(&info).unwrap();
1040 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1041
1042 assert_eq!(info.path, deserialized.path);
1043 assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
1044 assert_eq!(info.is_selected, deserialized.is_selected);
1045 }
1046
1047 #[test]
1049 fn test_database_path_info_all_none() {
1050 let info = DatabasePathInfo {
1051 current_working_directory: "/test".to_string(),
1052 env_var_set: false,
1053 env_var_path: None,
1054 env_var_valid: None,
1055 directories_checked: vec![],
1056 home_directory: None,
1057 home_has_intent_engine: false,
1058 final_database_path: None,
1059 resolution_method: None,
1060 };
1061
1062 assert!(!info.env_var_set);
1063 assert!(info.env_var_path.is_none());
1064 assert!(info.env_var_valid.is_none());
1065 assert!(info.directories_checked.is_empty());
1066 assert!(info.home_directory.is_none());
1067 assert!(info.final_database_path.is_none());
1068 assert!(info.resolution_method.is_none());
1069 }
1070
1071 #[test]
1073 fn test_database_path_info_all_some() {
1074 let info = DatabasePathInfo {
1075 current_working_directory: "/test".to_string(),
1076 env_var_set: true,
1077 env_var_path: Some("/env".to_string()),
1078 env_var_valid: Some(true),
1079 directories_checked: vec![DirectoryTraversalInfo {
1080 path: "/test".to_string(),
1081 has_intent_engine: true,
1082 is_selected: true,
1083 }],
1084 home_directory: Some("/home".to_string()),
1085 home_has_intent_engine: true,
1086 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1087 resolution_method: Some("Test Method".to_string()),
1088 };
1089
1090 assert!(info.env_var_set);
1091 assert!(info.env_var_path.is_some());
1092 assert!(info.env_var_valid.is_some());
1093 assert!(!info.directories_checked.is_empty());
1094 assert!(info.home_directory.is_some());
1095 assert!(info.final_database_path.is_some());
1096 assert!(info.resolution_method.is_some());
1097 }
1098
1099 #[test]
1101 fn test_get_database_path_info_home_directory() {
1102 let home_val = std::env::var("HOME");
1104 let userprofile_val = std::env::var("USERPROFILE");
1105 let has_home = home_val.is_ok();
1106 let has_userprofile = userprofile_val.is_ok();
1107 let info = ProjectContext::get_database_path_info();
1108
1109 if (has_home || has_userprofile) && !info.env_var_set && info.directories_checked.is_empty()
1115 {
1116 assert!(
1118 info.home_directory.is_some(),
1119 "HOME or USERPROFILE env var is set (HOME={:?}, USERPROFILE={:?}), but home_directory is None. Full info: {:?}",
1120 home_val.as_ref().map(|s| s.as_str()),
1121 userprofile_val.as_ref().map(|s| s.as_str()),
1122 info
1123 );
1124 }
1125 }
1126
1127 #[test]
1129 fn test_get_database_path_info_no_panic() {
1130 let info = ProjectContext::get_database_path_info();
1133
1134 assert!(!info.current_working_directory.is_empty());
1136
1137 if info.final_database_path.is_none() {
1140 let has_diagnostic_info = !info.directories_checked.is_empty()
1142 || info.env_var_set
1143 || info.home_directory.is_some();
1144
1145 assert!(
1146 has_diagnostic_info,
1147 "Even without finding a database, should provide diagnostic information"
1148 );
1149 }
1150 }
1151
1152 #[test]
1154 fn test_get_database_path_info_prefers_first_match() {
1155 let info = ProjectContext::get_database_path_info();
1156
1157 if info
1159 .resolution_method
1160 .as_ref()
1161 .is_some_and(|m| m.contains("Upward Directory"))
1162 && info.directories_checked.len() > 1
1163 {
1164 let with_ie: Vec<_> = info
1166 .directories_checked
1167 .iter()
1168 .filter(|d| d.has_intent_engine)
1169 .collect();
1170
1171 if with_ie.len() > 1 {
1172 let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1174 assert!(
1175 selected.len() <= 1,
1176 "Only the first .intent-engine found should be selected"
1177 );
1178 }
1179 }
1180 }
1181
1182 #[test]
1184 fn test_database_path_info_partial_deserialization() {
1185 let json = r#"{
1187 "current_working_directory": "/test",
1188 "env_var_set": false,
1189 "env_var_path": null,
1190 "env_var_valid": null,
1191 "directories_checked": [],
1192 "home_directory": null,
1193 "home_has_intent_engine": false,
1194 "final_database_path": null,
1195 "resolution_method": null
1196 }"#;
1197
1198 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1199 assert_eq!(info.current_working_directory, "/test");
1200 assert!(!info.env_var_set);
1201 }
1202
1203 #[test]
1205 fn test_database_path_info_json_schema() {
1206 let info = DatabasePathInfo {
1207 current_working_directory: "/test".to_string(),
1208 env_var_set: true,
1209 env_var_path: Some("/env".to_string()),
1210 env_var_valid: Some(true),
1211 directories_checked: vec![],
1212 home_directory: Some("/home".to_string()),
1213 home_has_intent_engine: false,
1214 final_database_path: Some("/db".to_string()),
1215 resolution_method: Some("Test".to_string()),
1216 };
1217
1218 let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1219
1220 assert!(json_value.get("current_working_directory").is_some());
1222 assert!(json_value.get("env_var_set").is_some());
1223 assert!(json_value.get("env_var_path").is_some());
1224 assert!(json_value.get("env_var_valid").is_some());
1225 assert!(json_value.get("directories_checked").is_some());
1226 assert!(json_value.get("home_directory").is_some());
1227 assert!(json_value.get("home_has_intent_engine").is_some());
1228 assert!(json_value.get("final_database_path").is_some());
1229 assert!(json_value.get("resolution_method").is_some());
1230 }
1231
1232 #[test]
1234 fn test_directory_traversal_info_empty_path() {
1235 let info = DirectoryTraversalInfo {
1236 path: "".to_string(),
1237 has_intent_engine: false,
1238 is_selected: false,
1239 };
1240
1241 assert_eq!(info.path, "");
1242 let json = serde_json::to_string(&info).unwrap();
1243 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1244 assert_eq!(deserialized.path, "");
1245 }
1246
1247 #[test]
1249 fn test_directory_traversal_info_unicode_path() {
1250 let info = DirectoryTraversalInfo {
1251 path: "/test/路径/مسار/путь".to_string(),
1252 has_intent_engine: true,
1253 is_selected: false,
1254 };
1255
1256 let json = serde_json::to_string(&info).unwrap();
1257 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1258 assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1259 }
1260
1261 #[test]
1263 fn test_database_path_info_long_paths() {
1264 let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1265 let info = DatabasePathInfo {
1266 current_working_directory: long_path.clone(),
1267 env_var_set: false,
1268 env_var_path: None,
1269 env_var_valid: None,
1270 directories_checked: vec![],
1271 home_directory: Some(long_path.clone()),
1272 home_has_intent_engine: false,
1273 final_database_path: Some(long_path.clone()),
1274 resolution_method: Some("Test".to_string()),
1275 };
1276
1277 let json = serde_json::to_string(&info).unwrap();
1278 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1279 assert_eq!(deserialized.current_working_directory, long_path);
1280 }
1281
1282 #[test]
1284 fn test_get_database_path_info_env_var_detection() {
1285 let info = ProjectContext::get_database_path_info();
1286
1287 if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1289 assert!(
1290 info.env_var_set,
1291 "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1292 );
1293 assert!(
1294 info.env_var_path.is_some(),
1295 "env_var_path should contain the path when env var is set"
1296 );
1297 assert!(
1298 info.env_var_valid.is_some(),
1299 "env_var_valid should be set when env var is present"
1300 );
1301 } else {
1302 assert!(
1303 !info.env_var_set,
1304 "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1305 );
1306 assert!(
1307 info.env_var_path.is_none(),
1308 "env_var_path should be None when env var is not set"
1309 );
1310 assert!(
1311 info.env_var_valid.is_none(),
1312 "env_var_valid should be None when env var is not set"
1313 );
1314 }
1315 }
1316}