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