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> {
153 if let Ok(env_path) = std::env::var("INTENT_ENGINE_PROJECT_DIR") {
155 let path = PathBuf::from(env_path);
156 let intent_dir = path.join(INTENT_DIR);
157 if intent_dir.exists() && intent_dir.is_dir() {
158 eprintln!(
159 "✓ Using project from INTENT_ENGINE_PROJECT_DIR: {}",
160 path.display()
161 );
162 return Some(path);
163 } else {
164 eprintln!(
165 "⚠ INTENT_ENGINE_PROJECT_DIR set but no .intent-engine found: {}",
166 path.display()
167 );
168 }
169 }
170
171 if let Ok(mut current) = std::env::current_dir() {
173 let start_dir = current.clone();
174 loop {
175 let intent_dir = current.join(INTENT_DIR);
176 if intent_dir.exists() && intent_dir.is_dir() {
177 if current != start_dir {
178 eprintln!("✓ Found project: {}", current.display());
179 }
180 return Some(current);
181 }
182
183 if !current.pop() {
184 break;
185 }
186 }
187 }
188
189 if let Ok(home) = std::env::var("HOME") {
191 let home_path = PathBuf::from(home);
192 let intent_dir = home_path.join(INTENT_DIR);
193 if intent_dir.exists() && intent_dir.is_dir() {
194 eprintln!("✓ Using home project: {}", home_path.display());
195 return Some(home_path);
196 }
197 }
198
199 #[cfg(target_os = "windows")]
201 if let Ok(userprofile) = std::env::var("USERPROFILE") {
202 let home_path = PathBuf::from(userprofile);
203 let intent_dir = home_path.join(INTENT_DIR);
204 if intent_dir.exists() && intent_dir.is_dir() {
205 eprintln!("✓ Using home project: {}", home_path.display());
206 return Some(home_path);
207 }
208 }
209
210 None
211 }
212
213 fn infer_project_root() -> Option<PathBuf> {
221 let cwd = std::env::current_dir().ok()?;
222 let mut current = cwd.clone();
223
224 loop {
225 for marker in PROJECT_ROOT_MARKERS {
227 let marker_path = current.join(marker);
228 if marker_path.exists() {
229 return Some(current);
230 }
231 }
232
233 if !current.pop() {
235 break;
237 }
238 }
239
240 None
241 }
242
243 pub async fn initialize_project() -> Result<Self> {
250 let cwd = std::env::current_dir()?;
251
252 let root = match Self::infer_project_root() {
254 Some(inferred_root) => {
255 inferred_root
257 },
258 None => {
259 eprintln!(
262 "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
263 Initialized Intent-Engine in the current directory '{}'.\n\
264 For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
265 cwd.display()
266 );
267 cwd
268 },
269 };
270
271 let intent_dir = root.join(INTENT_DIR);
272 let db_path = intent_dir.join(DB_FILE);
273
274 if !intent_dir.exists() {
276 std::fs::create_dir_all(&intent_dir)?;
277 }
278
279 let pool = create_pool(&db_path).await?;
281
282 run_migrations(&pool).await?;
284
285 Ok(ProjectContext {
286 root,
287 db_path,
288 pool,
289 })
290 }
291
292 pub async fn load() -> Result<Self> {
294 let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
295 let db_path = root.join(INTENT_DIR).join(DB_FILE);
296
297 let pool = create_pool(&db_path).await?;
298
299 Ok(ProjectContext {
300 root,
301 db_path,
302 pool,
303 })
304 }
305
306 pub async fn load_or_init() -> Result<Self> {
308 match Self::load().await {
309 Ok(ctx) => Ok(ctx),
310 Err(IntentError::NotAProject) => Self::initialize_project().await,
311 Err(e) => Err(e),
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
325 fn test_constants() {
326 assert_eq!(INTENT_DIR, ".intent-engine");
327 assert_eq!(DB_FILE, "project.db");
328 }
329
330 #[test]
331 fn test_project_context_debug() {
332 let _type_check = |ctx: ProjectContext| {
335 let _ = format!("{:?}", ctx);
336 };
337 }
338
339 #[test]
340 fn test_project_root_markers_list() {
341 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
343 assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
344 assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
345 }
346
347 #[test]
348 fn test_project_root_markers_priority() {
349 assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
351 }
352
353 #[test]
356 fn test_infer_project_root_with_git() {
357 assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
361 }
362
363 #[test]
365 fn test_all_major_project_types_covered() {
366 let markers = PROJECT_ROOT_MARKERS;
367
368 assert!(markers.contains(&".git"));
370 assert!(markers.contains(&".hg"));
371
372 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")); }
380
381 #[test]
383 fn test_directory_traversal_info_creation() {
384 let info = DirectoryTraversalInfo {
385 path: "/test/path".to_string(),
386 has_intent_engine: true,
387 is_selected: false,
388 };
389
390 assert_eq!(info.path, "/test/path");
391 assert!(info.has_intent_engine);
392 assert!(!info.is_selected);
393 }
394
395 #[test]
397 fn test_directory_traversal_info_clone() {
398 let info = DirectoryTraversalInfo {
399 path: "/test/path".to_string(),
400 has_intent_engine: true,
401 is_selected: true,
402 };
403
404 let cloned = info.clone();
405 assert_eq!(cloned.path, info.path);
406 assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
407 assert_eq!(cloned.is_selected, info.is_selected);
408 }
409
410 #[test]
412 fn test_directory_traversal_info_debug() {
413 let info = DirectoryTraversalInfo {
414 path: "/test/path".to_string(),
415 has_intent_engine: false,
416 is_selected: true,
417 };
418
419 let debug_str = format!("{:?}", info);
420 assert!(debug_str.contains("DirectoryTraversalInfo"));
421 assert!(debug_str.contains("/test/path"));
422 }
423
424 #[test]
426 fn test_directory_traversal_info_serialization() {
427 let info = DirectoryTraversalInfo {
428 path: "/test/path".to_string(),
429 has_intent_engine: true,
430 is_selected: false,
431 };
432
433 let json = serde_json::to_string(&info).unwrap();
434 assert!(json.contains("path"));
435 assert!(json.contains("has_intent_engine"));
436 assert!(json.contains("is_selected"));
437 assert!(json.contains("/test/path"));
438 }
439
440 #[test]
442 fn test_directory_traversal_info_deserialization() {
443 let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
444 let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
445
446 assert_eq!(info.path, "/test/path");
447 assert!(info.has_intent_engine);
448 assert!(!info.is_selected);
449 }
450
451 #[test]
453 fn test_database_path_info_creation() {
454 let info = DatabasePathInfo {
455 current_working_directory: "/test/cwd".to_string(),
456 env_var_set: false,
457 env_var_path: None,
458 env_var_valid: None,
459 directories_checked: vec![],
460 home_directory: Some("/home/user".to_string()),
461 home_has_intent_engine: false,
462 final_database_path: Some("/test/db.db".to_string()),
463 resolution_method: Some("Test Method".to_string()),
464 };
465
466 assert_eq!(info.current_working_directory, "/test/cwd");
467 assert!(!info.env_var_set);
468 assert_eq!(info.env_var_path, None);
469 assert_eq!(info.home_directory, Some("/home/user".to_string()));
470 assert!(!info.home_has_intent_engine);
471 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
472 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
473 }
474
475 #[test]
477 fn test_database_path_info_with_env_var() {
478 let info = DatabasePathInfo {
479 current_working_directory: "/test/cwd".to_string(),
480 env_var_set: true,
481 env_var_path: Some("/env/path".to_string()),
482 env_var_valid: Some(true),
483 directories_checked: vec![],
484 home_directory: Some("/home/user".to_string()),
485 home_has_intent_engine: false,
486 final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
487 resolution_method: Some("Environment Variable".to_string()),
488 };
489
490 assert!(info.env_var_set);
491 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
492 assert_eq!(info.env_var_valid, Some(true));
493 assert_eq!(
494 info.resolution_method,
495 Some("Environment Variable".to_string())
496 );
497 }
498
499 #[test]
501 fn test_database_path_info_with_directories() {
502 let dirs = vec![
503 DirectoryTraversalInfo {
504 path: "/test/path1".to_string(),
505 has_intent_engine: false,
506 is_selected: false,
507 },
508 DirectoryTraversalInfo {
509 path: "/test/path2".to_string(),
510 has_intent_engine: true,
511 is_selected: true,
512 },
513 ];
514
515 let info = DatabasePathInfo {
516 current_working_directory: "/test/path1".to_string(),
517 env_var_set: false,
518 env_var_path: None,
519 env_var_valid: None,
520 directories_checked: dirs.clone(),
521 home_directory: Some("/home/user".to_string()),
522 home_has_intent_engine: false,
523 final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
524 resolution_method: Some("Upward Directory Traversal".to_string()),
525 };
526
527 assert_eq!(info.directories_checked.len(), 2);
528 assert!(!info.directories_checked[0].has_intent_engine);
529 assert!(info.directories_checked[1].has_intent_engine);
530 assert!(info.directories_checked[1].is_selected);
531 }
532
533 #[test]
535 fn test_database_path_info_debug() {
536 let info = DatabasePathInfo {
537 current_working_directory: "/test/cwd".to_string(),
538 env_var_set: false,
539 env_var_path: None,
540 env_var_valid: None,
541 directories_checked: vec![],
542 home_directory: Some("/home/user".to_string()),
543 home_has_intent_engine: false,
544 final_database_path: Some("/test/db.db".to_string()),
545 resolution_method: Some("Test".to_string()),
546 };
547
548 let debug_str = format!("{:?}", info);
549 assert!(debug_str.contains("DatabasePathInfo"));
550 assert!(debug_str.contains("/test/cwd"));
551 }
552
553 #[test]
555 fn test_database_path_info_serialization() {
556 let info = DatabasePathInfo {
557 current_working_directory: "/test/cwd".to_string(),
558 env_var_set: true,
559 env_var_path: Some("/env/path".to_string()),
560 env_var_valid: Some(true),
561 directories_checked: vec![],
562 home_directory: Some("/home/user".to_string()),
563 home_has_intent_engine: false,
564 final_database_path: Some("/test/db.db".to_string()),
565 resolution_method: Some("Test Method".to_string()),
566 };
567
568 let json = serde_json::to_string(&info).unwrap();
569 assert!(json.contains("current_working_directory"));
570 assert!(json.contains("env_var_set"));
571 assert!(json.contains("env_var_path"));
572 assert!(json.contains("final_database_path"));
573 assert!(json.contains("/test/cwd"));
574 assert!(json.contains("/env/path"));
575 }
576
577 #[test]
579 fn test_database_path_info_deserialization() {
580 let json = r#"{
581 "current_working_directory": "/test/cwd",
582 "env_var_set": true,
583 "env_var_path": "/env/path",
584 "env_var_valid": true,
585 "directories_checked": [],
586 "home_directory": "/home/user",
587 "home_has_intent_engine": false,
588 "final_database_path": "/test/db.db",
589 "resolution_method": "Test Method"
590 }"#;
591
592 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
593 assert_eq!(info.current_working_directory, "/test/cwd");
594 assert!(info.env_var_set);
595 assert_eq!(info.env_var_path, Some("/env/path".to_string()));
596 assert_eq!(info.env_var_valid, Some(true));
597 assert_eq!(info.home_directory, Some("/home/user".to_string()));
598 assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
599 assert_eq!(info.resolution_method, Some("Test Method".to_string()));
600 }
601
602 #[test]
604 fn test_database_path_info_complete_structure() {
605 let dirs = vec![
606 DirectoryTraversalInfo {
607 path: "/home/user/project/src".to_string(),
608 has_intent_engine: false,
609 is_selected: false,
610 },
611 DirectoryTraversalInfo {
612 path: "/home/user/project".to_string(),
613 has_intent_engine: true,
614 is_selected: true,
615 },
616 DirectoryTraversalInfo {
617 path: "/home/user".to_string(),
618 has_intent_engine: false,
619 is_selected: false,
620 },
621 ];
622
623 let info = DatabasePathInfo {
624 current_working_directory: "/home/user/project/src".to_string(),
625 env_var_set: false,
626 env_var_path: None,
627 env_var_valid: None,
628 directories_checked: dirs,
629 home_directory: Some("/home/user".to_string()),
630 home_has_intent_engine: false,
631 final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
632 resolution_method: Some("Upward Directory Traversal".to_string()),
633 };
634
635 assert_eq!(info.directories_checked.len(), 3);
637 assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
638 assert_eq!(info.directories_checked[1].path, "/home/user/project");
639 assert_eq!(info.directories_checked[2].path, "/home/user");
640
641 assert!(!info.directories_checked[0].is_selected);
643 assert!(info.directories_checked[1].is_selected);
644 assert!(!info.directories_checked[2].is_selected);
645
646 assert!(!info.directories_checked[0].has_intent_engine);
648 assert!(info.directories_checked[1].has_intent_engine);
649 assert!(!info.directories_checked[2].has_intent_engine);
650 }
651
652 #[test]
654 fn test_get_database_path_info_structure() {
655 let info = ProjectContext::get_database_path_info();
656
657 assert!(!info.current_working_directory.is_empty());
659
660 let has_data = !info.directories_checked.is_empty()
662 || info.home_directory.is_some()
663 || info.env_var_set;
664
665 assert!(
666 has_data,
667 "get_database_path_info should return some directory information"
668 );
669 }
670
671 #[test]
673 fn test_get_database_path_info_checks_current_dir() {
674 let info = ProjectContext::get_database_path_info();
675
676 assert!(!info.current_working_directory.is_empty());
678
679 if !info.env_var_set || info.env_var_valid != Some(true) {
681 assert!(
682 !info.directories_checked.is_empty(),
683 "Should check at least the current directory"
684 );
685 }
686 }
687
688 #[test]
690 fn test_get_database_path_info_includes_cwd() {
691 let info = ProjectContext::get_database_path_info();
692
693 if !info.env_var_set || info.env_var_valid != Some(true) {
695 assert!(!info.directories_checked.is_empty());
696
697 let cwd = &info.current_working_directory;
699 let first_checked = &info.directories_checked[0].path;
700
701 assert!(
702 cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
703 "First checked directory should be related to CWD"
704 );
705 }
706 }
707
708 #[test]
710 fn test_get_database_path_info_resolution_method() {
711 let info = ProjectContext::get_database_path_info();
712
713 if info.final_database_path.is_some() {
715 assert!(
716 info.resolution_method.is_some(),
717 "Resolution method should be set when database path is found"
718 );
719
720 let method = info.resolution_method.unwrap();
721 assert!(
722 method.contains("Environment Variable")
723 || method.contains("Upward Directory Traversal")
724 || method.contains("Home Directory"),
725 "Resolution method should be one of the known strategies"
726 );
727 }
728 }
729
730 #[test]
732 fn test_get_database_path_info_selected_directory() {
733 let info = ProjectContext::get_database_path_info();
734
735 if (!info.env_var_set || info.env_var_valid != Some(true))
737 && !info.directories_checked.is_empty()
738 && info.final_database_path.is_some()
739 {
740 let selected_count = info
742 .directories_checked
743 .iter()
744 .filter(|d| d.is_selected)
745 .count();
746
747 assert!(
748 selected_count <= 1,
749 "At most one directory should be marked as selected"
750 );
751
752 if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
754 assert!(
755 selected.has_intent_engine,
756 "Selected directory should have .intent-engine"
757 );
758 }
759 }
760 }
761
762 #[test]
764 fn test_database_path_info_no_database_found() {
765 let info = DatabasePathInfo {
766 current_working_directory: "/test/path".to_string(),
767 env_var_set: false,
768 env_var_path: None,
769 env_var_valid: None,
770 directories_checked: vec![
771 DirectoryTraversalInfo {
772 path: "/test/path".to_string(),
773 has_intent_engine: false,
774 is_selected: false,
775 },
776 DirectoryTraversalInfo {
777 path: "/test".to_string(),
778 has_intent_engine: false,
779 is_selected: false,
780 },
781 ],
782 home_directory: Some("/home/user".to_string()),
783 home_has_intent_engine: false,
784 final_database_path: None,
785 resolution_method: None,
786 };
787
788 assert!(info.final_database_path.is_none());
789 assert!(info.resolution_method.is_none());
790 assert_eq!(info.directories_checked.len(), 2);
791 assert!(!info.home_has_intent_engine);
792 }
793
794 #[test]
796 fn test_database_path_info_env_var_invalid() {
797 let info = DatabasePathInfo {
798 current_working_directory: "/test/cwd".to_string(),
799 env_var_set: true,
800 env_var_path: Some("/invalid/path".to_string()),
801 env_var_valid: Some(false),
802 directories_checked: vec![DirectoryTraversalInfo {
803 path: "/test/cwd".to_string(),
804 has_intent_engine: true,
805 is_selected: true,
806 }],
807 home_directory: Some("/home/user".to_string()),
808 home_has_intent_engine: false,
809 final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
810 resolution_method: Some("Upward Directory Traversal".to_string()),
811 };
812
813 assert!(info.env_var_set);
814 assert_eq!(info.env_var_valid, Some(false));
815 assert!(info.final_database_path.is_some());
816 assert!(info.resolution_method.unwrap().contains("Upward Directory"));
818 }
819
820 #[test]
822 fn test_database_path_info_home_directory_used() {
823 let info = DatabasePathInfo {
824 current_working_directory: "/tmp/work".to_string(),
825 env_var_set: false,
826 env_var_path: None,
827 env_var_valid: None,
828 directories_checked: vec![
829 DirectoryTraversalInfo {
830 path: "/tmp/work".to_string(),
831 has_intent_engine: false,
832 is_selected: false,
833 },
834 DirectoryTraversalInfo {
835 path: "/tmp".to_string(),
836 has_intent_engine: false,
837 is_selected: false,
838 },
839 ],
840 home_directory: Some("/home/user".to_string()),
841 home_has_intent_engine: true,
842 final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
843 resolution_method: Some("Home Directory Fallback".to_string()),
844 };
845
846 assert!(info.home_has_intent_engine);
847 assert_eq!(
848 info.final_database_path,
849 Some("/home/user/.intent-engine/project.db".to_string())
850 );
851 assert_eq!(
852 info.resolution_method,
853 Some("Home Directory Fallback".to_string())
854 );
855 }
856
857 #[test]
859 fn test_database_path_info_full_roundtrip() {
860 let original = DatabasePathInfo {
861 current_working_directory: "/test/cwd".to_string(),
862 env_var_set: true,
863 env_var_path: Some("/env/path".to_string()),
864 env_var_valid: Some(false),
865 directories_checked: vec![
866 DirectoryTraversalInfo {
867 path: "/test/cwd".to_string(),
868 has_intent_engine: false,
869 is_selected: false,
870 },
871 DirectoryTraversalInfo {
872 path: "/test".to_string(),
873 has_intent_engine: true,
874 is_selected: true,
875 },
876 ],
877 home_directory: Some("/home/user".to_string()),
878 home_has_intent_engine: false,
879 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
880 resolution_method: Some("Upward Directory Traversal".to_string()),
881 };
882
883 let json = serde_json::to_string(&original).unwrap();
885
886 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
888
889 assert_eq!(
891 deserialized.current_working_directory,
892 original.current_working_directory
893 );
894 assert_eq!(deserialized.env_var_set, original.env_var_set);
895 assert_eq!(deserialized.env_var_path, original.env_var_path);
896 assert_eq!(deserialized.env_var_valid, original.env_var_valid);
897 assert_eq!(
898 deserialized.directories_checked.len(),
899 original.directories_checked.len()
900 );
901 assert_eq!(deserialized.home_directory, original.home_directory);
902 assert_eq!(
903 deserialized.home_has_intent_engine,
904 original.home_has_intent_engine
905 );
906 assert_eq!(
907 deserialized.final_database_path,
908 original.final_database_path
909 );
910 assert_eq!(deserialized.resolution_method, original.resolution_method);
911 }
912
913 #[test]
915 fn test_directory_traversal_info_all_combinations() {
916 let combinations = [(false, false), (false, true), (true, false), (true, true)];
918
919 for (has_ie, is_sel) in combinations.iter() {
920 let info = DirectoryTraversalInfo {
921 path: format!("/test/path/{}_{}", has_ie, is_sel),
922 has_intent_engine: *has_ie,
923 is_selected: *is_sel,
924 };
925
926 assert_eq!(info.has_intent_engine, *has_ie);
927 assert_eq!(info.is_selected, *is_sel);
928 }
929 }
930
931 #[test]
933 fn test_directory_traversal_info_exact_serialization() {
934 let info = DirectoryTraversalInfo {
935 path: "/exact/path/with/special-chars_123".to_string(),
936 has_intent_engine: true,
937 is_selected: false,
938 };
939
940 let json = serde_json::to_string(&info).unwrap();
941 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
942
943 assert_eq!(info.path, deserialized.path);
944 assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
945 assert_eq!(info.is_selected, deserialized.is_selected);
946 }
947
948 #[test]
950 fn test_database_path_info_all_none() {
951 let info = DatabasePathInfo {
952 current_working_directory: "/test".to_string(),
953 env_var_set: false,
954 env_var_path: None,
955 env_var_valid: None,
956 directories_checked: vec![],
957 home_directory: None,
958 home_has_intent_engine: false,
959 final_database_path: None,
960 resolution_method: None,
961 };
962
963 assert!(!info.env_var_set);
964 assert!(info.env_var_path.is_none());
965 assert!(info.env_var_valid.is_none());
966 assert!(info.directories_checked.is_empty());
967 assert!(info.home_directory.is_none());
968 assert!(info.final_database_path.is_none());
969 assert!(info.resolution_method.is_none());
970 }
971
972 #[test]
974 fn test_database_path_info_all_some() {
975 let info = DatabasePathInfo {
976 current_working_directory: "/test".to_string(),
977 env_var_set: true,
978 env_var_path: Some("/env".to_string()),
979 env_var_valid: Some(true),
980 directories_checked: vec![DirectoryTraversalInfo {
981 path: "/test".to_string(),
982 has_intent_engine: true,
983 is_selected: true,
984 }],
985 home_directory: Some("/home".to_string()),
986 home_has_intent_engine: true,
987 final_database_path: Some("/test/.intent-engine/project.db".to_string()),
988 resolution_method: Some("Test Method".to_string()),
989 };
990
991 assert!(info.env_var_set);
992 assert!(info.env_var_path.is_some());
993 assert!(info.env_var_valid.is_some());
994 assert!(!info.directories_checked.is_empty());
995 assert!(info.home_directory.is_some());
996 assert!(info.final_database_path.is_some());
997 assert!(info.resolution_method.is_some());
998 }
999
1000 #[test]
1002 fn test_get_database_path_info_home_directory() {
1003 let info = ProjectContext::get_database_path_info();
1004
1005 if std::env::var("HOME").is_ok() {
1008 assert!(
1009 info.home_directory.is_some(),
1010 "HOME env var is set, so home_directory should be Some"
1011 );
1012 }
1013 }
1014
1015 #[test]
1017 fn test_get_database_path_info_no_panic() {
1018 let info = ProjectContext::get_database_path_info();
1021
1022 assert!(!info.current_working_directory.is_empty());
1024
1025 if info.final_database_path.is_none() {
1028 let has_diagnostic_info = !info.directories_checked.is_empty()
1030 || info.env_var_set
1031 || info.home_directory.is_some();
1032
1033 assert!(
1034 has_diagnostic_info,
1035 "Even without finding a database, should provide diagnostic information"
1036 );
1037 }
1038 }
1039
1040 #[test]
1042 fn test_get_database_path_info_prefers_first_match() {
1043 let info = ProjectContext::get_database_path_info();
1044
1045 if info
1047 .resolution_method
1048 .as_ref()
1049 .is_some_and(|m| m.contains("Upward Directory"))
1050 && info.directories_checked.len() > 1
1051 {
1052 let with_ie: Vec<_> = info
1054 .directories_checked
1055 .iter()
1056 .filter(|d| d.has_intent_engine)
1057 .collect();
1058
1059 if with_ie.len() > 1 {
1060 let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1062 assert!(
1063 selected.len() <= 1,
1064 "Only the first .intent-engine found should be selected"
1065 );
1066 }
1067 }
1068 }
1069
1070 #[test]
1072 fn test_database_path_info_partial_deserialization() {
1073 let json = r#"{
1075 "current_working_directory": "/test",
1076 "env_var_set": false,
1077 "env_var_path": null,
1078 "env_var_valid": null,
1079 "directories_checked": [],
1080 "home_directory": null,
1081 "home_has_intent_engine": false,
1082 "final_database_path": null,
1083 "resolution_method": null
1084 }"#;
1085
1086 let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1087 assert_eq!(info.current_working_directory, "/test");
1088 assert!(!info.env_var_set);
1089 }
1090
1091 #[test]
1093 fn test_database_path_info_json_schema() {
1094 let info = DatabasePathInfo {
1095 current_working_directory: "/test".to_string(),
1096 env_var_set: true,
1097 env_var_path: Some("/env".to_string()),
1098 env_var_valid: Some(true),
1099 directories_checked: vec![],
1100 home_directory: Some("/home".to_string()),
1101 home_has_intent_engine: false,
1102 final_database_path: Some("/db".to_string()),
1103 resolution_method: Some("Test".to_string()),
1104 };
1105
1106 let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1107
1108 assert!(json_value.get("current_working_directory").is_some());
1110 assert!(json_value.get("env_var_set").is_some());
1111 assert!(json_value.get("env_var_path").is_some());
1112 assert!(json_value.get("env_var_valid").is_some());
1113 assert!(json_value.get("directories_checked").is_some());
1114 assert!(json_value.get("home_directory").is_some());
1115 assert!(json_value.get("home_has_intent_engine").is_some());
1116 assert!(json_value.get("final_database_path").is_some());
1117 assert!(json_value.get("resolution_method").is_some());
1118 }
1119
1120 #[test]
1122 fn test_directory_traversal_info_empty_path() {
1123 let info = DirectoryTraversalInfo {
1124 path: "".to_string(),
1125 has_intent_engine: false,
1126 is_selected: false,
1127 };
1128
1129 assert_eq!(info.path, "");
1130 let json = serde_json::to_string(&info).unwrap();
1131 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1132 assert_eq!(deserialized.path, "");
1133 }
1134
1135 #[test]
1137 fn test_directory_traversal_info_unicode_path() {
1138 let info = DirectoryTraversalInfo {
1139 path: "/test/路径/مسار/путь".to_string(),
1140 has_intent_engine: true,
1141 is_selected: false,
1142 };
1143
1144 let json = serde_json::to_string(&info).unwrap();
1145 let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1146 assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1147 }
1148
1149 #[test]
1151 fn test_database_path_info_long_paths() {
1152 let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1153 let info = DatabasePathInfo {
1154 current_working_directory: long_path.clone(),
1155 env_var_set: false,
1156 env_var_path: None,
1157 env_var_valid: None,
1158 directories_checked: vec![],
1159 home_directory: Some(long_path.clone()),
1160 home_has_intent_engine: false,
1161 final_database_path: Some(long_path.clone()),
1162 resolution_method: Some("Test".to_string()),
1163 };
1164
1165 let json = serde_json::to_string(&info).unwrap();
1166 let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1167 assert_eq!(deserialized.current_working_directory, long_path);
1168 }
1169
1170 #[test]
1172 fn test_get_database_path_info_env_var_detection() {
1173 let info = ProjectContext::get_database_path_info();
1174
1175 if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1177 assert!(
1178 info.env_var_set,
1179 "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1180 );
1181 assert!(
1182 info.env_var_path.is_some(),
1183 "env_var_path should contain the path when env var is set"
1184 );
1185 assert!(
1186 info.env_var_valid.is_some(),
1187 "env_var_valid should be set when env var is present"
1188 );
1189 } else {
1190 assert!(
1191 !info.env_var_set,
1192 "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1193 );
1194 assert!(
1195 info.env_var_path.is_none(),
1196 "env_var_path should be None when env var is not set"
1197 );
1198 assert!(
1199 info.env_var_valid.is_none(),
1200 "env_var_valid should be None when env var is not set"
1201 );
1202 }
1203 }
1204}