intent_engine/
project.rs

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
10/// Project root markers in priority order (highest priority first)
11/// These are used to identify the root directory of a project
12const PROJECT_ROOT_MARKERS: &[&str] = &[
13    ".git",           // Git (highest priority)
14    ".hg",            // Mercurial
15    "package.json",   // Node.js
16    "Cargo.toml",     // Rust
17    "pyproject.toml", // Python (PEP 518)
18    "go.mod",         // Go Modules
19    "pom.xml",        // Maven (Java)
20    "build.gradle",   // Gradle (Java/Kotlin)
21];
22
23#[derive(Debug)]
24pub struct ProjectContext {
25    pub root: PathBuf,
26    pub db_path: PathBuf,
27    pub pool: SqlitePool,
28}
29
30/// Information about directory traversal for database location
31#[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/// Detailed information about database path resolution
39#[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    /// Collect detailed information about database path resolution for diagnostics
54    ///
55    /// This function traces through all the steps of finding the database location,
56    /// showing which directories were checked and why a particular location was chosen.
57    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        // Check strategy 1: Environment variable
76        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        // Check strategy 2: Upward directory traversal
95        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                    // Continue traversal to show all directories checked
113                }
114
115                if !current.pop() {
116                    break;
117                }
118            }
119        }
120
121        // Check strategy 3: Home directory
122        #[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    /// Find the project root by searching upwards for .intent-engine directory
147    ///
148    /// Search strategy (in priority order):
149    /// 1. Check INTENT_ENGINE_PROJECT_DIR environment variable
150    /// 2. Search upwards from current directory for .intent-engine/, but:
151    ///    - Stop at project boundary (defined by PROJECT_ROOT_MARKERS)
152    ///    - Do NOT cross into parent projects to prevent database mixing
153    /// 3. Check user's home directory for .intent-engine/
154    ///
155    /// **Important**: This function now respects project boundaries to prevent
156    /// nested projects from accidentally using parent project databases.
157    pub fn find_project_root() -> Option<PathBuf> {
158        // Strategy 1: Check environment variable (highest priority)
159        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        // Strategy 2: Search upwards from current directory
177        // BUT respect project boundaries (don't cross into parent projects)
178        // UNLESS we're not inside any project (to support MCP server startup)
179        if let Ok(current_dir) = std::env::current_dir() {
180            let start_dir = current_dir.clone();
181
182            // First, find the boundary of the current project (if any)
183            // This is the directory that contains a project marker
184            let project_boundary = Self::infer_project_root();
185
186            let mut current = start_dir.clone();
187            loop {
188                let intent_dir = current.join(INTENT_DIR);
189                if intent_dir.exists() && intent_dir.is_dir() {
190                    // Found .intent-engine directory
191
192                    // Check if we're within or at the project boundary
193                    // If there's a project boundary and we've crossed it, don't use this .intent-engine
194                    // BUT: if project_boundary is None (not in any project), allow searching anywhere
195                    if let Some(ref boundary) = project_boundary {
196                        // Check if the found .intent-engine is within our project boundary
197                        // (current path should be equal to or a child of boundary)
198                        if !current.starts_with(boundary) && current != *boundary {
199                            // We've crossed the project boundary into a parent project
200                            // Do NOT use this .intent-engine
201                            break;
202                        }
203                    }
204
205                    if current != start_dir {
206                        eprintln!("✓ Found project: {}", current.display());
207                    }
208                    return Some(current);
209                }
210
211                // Check if we've reached the project boundary
212                // If so, return the project root (will trigger lazy initialization)
213                if let Some(ref boundary) = project_boundary {
214                    if current == *boundary {
215                        // We've reached the boundary without finding .intent-engine
216                        // Return the project root to enable lazy initialization
217                        eprintln!("✓ Detected project root: {}", boundary.display());
218                        return Some(boundary.clone());
219                    }
220                }
221
222                if !current.pop() {
223                    break;
224                }
225            }
226        }
227
228        // Strategy 3: Check user's home directory (fallback)
229        // ONLY use if no project boundary was detected
230        if let Ok(home) = std::env::var("HOME") {
231            let home_path = PathBuf::from(home);
232            let intent_dir = home_path.join(INTENT_DIR);
233            if intent_dir.exists() && intent_dir.is_dir() {
234                eprintln!("✓ Using home project: {}", home_path.display());
235                return Some(home_path);
236            }
237        }
238
239        // Windows: also check USERPROFILE
240        #[cfg(target_os = "windows")]
241        if let Ok(userprofile) = std::env::var("USERPROFILE") {
242            let home_path = PathBuf::from(userprofile);
243            let intent_dir = home_path.join(INTENT_DIR);
244            if intent_dir.exists() && intent_dir.is_dir() {
245                eprintln!("✓ Using home project: {}", home_path.display());
246                return Some(home_path);
247            }
248        }
249
250        None
251    }
252
253    /// Infer the project root directory starting from a given path
254    ///
255    /// This is a helper function that implements the core root-finding logic
256    /// without relying on the global current directory.
257    ///
258    /// # Arguments
259    /// * `start_path` - The directory path to start searching from
260    ///
261    /// # Returns
262    /// * `Some(PathBuf)` - The project root if a marker is found
263    /// * `None` - If no project marker is found up to the filesystem root
264    fn infer_project_root_from(start_path: &std::path::Path) -> Option<PathBuf> {
265        let mut current = start_path.to_path_buf();
266
267        loop {
268            // Check if any marker exists in current directory
269            for marker in PROJECT_ROOT_MARKERS {
270                let marker_path = current.join(marker);
271                if marker_path.exists() {
272                    return Some(current);
273                }
274            }
275
276            // Try to move up to parent directory
277            if !current.pop() {
278                // Reached filesystem root without finding any marker
279                break;
280            }
281        }
282
283        None
284    }
285
286    /// Infer the project root directory based on common project markers
287    ///
288    /// This function implements a smart algorithm to find the project root:
289    /// 1. Start from current directory and traverse upwards
290    /// 2. Check each directory for project markers (in priority order)
291    /// 3. Return the first directory that contains any marker
292    /// 4. If no marker found, return None (fallback to CWD handled by caller)
293    fn infer_project_root() -> Option<PathBuf> {
294        let cwd = std::env::current_dir().ok()?;
295        Self::infer_project_root_from(&cwd)
296    }
297
298    /// Initialize a new Intent-Engine project using smart root inference
299    ///
300    /// This function implements the smart lazy initialization algorithm:
301    /// 1. Try to infer project root based on common markers
302    /// 2. If inference succeeds, initialize in the inferred root
303    /// 3. If inference fails, fallback to CWD and print warning to stderr
304    pub async fn initialize_project() -> Result<Self> {
305        let cwd = std::env::current_dir()?;
306
307        // Try to infer the project root
308        let root = match Self::infer_project_root() {
309            Some(inferred_root) => {
310                // Successfully inferred project root
311                inferred_root
312            },
313            None => {
314                // Fallback: use current working directory
315                // Print warning to stderr
316                eprintln!(
317                    "Warning: Could not determine a project root based on common markers (e.g., .git, package.json).\n\
318                     Initialized Intent-Engine in the current directory '{}'.\n\
319                     For predictable behavior, it's recommended to initialize from a directory containing a root marker.",
320                    cwd.display()
321                );
322                cwd
323            },
324        };
325
326        let intent_dir = root.join(INTENT_DIR);
327        let db_path = intent_dir.join(DB_FILE);
328
329        // Create .intent-engine directory if it doesn't exist
330        if !intent_dir.exists() {
331            std::fs::create_dir_all(&intent_dir)?;
332        }
333
334        // Create database connection
335        let pool = create_pool(&db_path).await?;
336
337        // Run migrations
338        run_migrations(&pool).await?;
339
340        Ok(ProjectContext {
341            root,
342            db_path,
343            pool,
344        })
345    }
346
347    /// Initialize a new Intent-Engine project at a specific directory
348    ///
349    /// This is a thread-safe alternative to `initialize_project()` that doesn't
350    /// rely on the global current directory. It's particularly useful for:
351    /// - Concurrent tests that need isolated project initialization
352    /// - Tools that need to initialize projects in specific directories
353    /// - Any scenario where changing the global current directory is undesirable
354    ///
355    /// # Arguments
356    /// * `project_dir` - The directory where the project should be initialized
357    ///
358    /// # Algorithm
359    /// 1. Try to infer project root starting from `project_dir`
360    /// 2. If inference succeeds, initialize in the inferred root
361    /// 3. If inference fails, use `project_dir` as the root directly
362    ///
363    /// # Examples
364    /// ```no_run
365    /// use intent_engine::project::ProjectContext;
366    /// use std::path::PathBuf;
367    ///
368    /// #[tokio::main]
369    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
370    ///     let project_dir = PathBuf::from("/tmp/my-project");
371    ///     let ctx = ProjectContext::initialize_project_at(project_dir).await?;
372    ///     Ok(())
373    /// }
374    /// ```
375    pub async fn initialize_project_at(project_dir: PathBuf) -> Result<Self> {
376        // Try to infer the project root starting from the provided directory
377        let root = match Self::infer_project_root_from(&project_dir) {
378            Some(inferred_root) => {
379                // Successfully inferred project root
380                inferred_root
381            },
382            None => {
383                // No marker found, use provided directory as root
384                // This is expected for test environments where we explicitly
385                // create a .git marker in a temp directory
386                project_dir
387            },
388        };
389
390        let intent_dir = root.join(INTENT_DIR);
391        let db_path = intent_dir.join(DB_FILE);
392
393        // Create .intent-engine directory if it doesn't exist
394        if !intent_dir.exists() {
395            std::fs::create_dir_all(&intent_dir)?;
396        }
397
398        // Create database connection
399        let pool = create_pool(&db_path).await?;
400
401        // Run migrations
402        run_migrations(&pool).await?;
403
404        Ok(ProjectContext {
405            root,
406            db_path,
407            pool,
408        })
409    }
410
411    /// Load an existing project context
412    pub async fn load() -> Result<Self> {
413        let root = Self::find_project_root().ok_or(IntentError::NotAProject)?;
414        let intent_dir = root.join(INTENT_DIR);
415
416        // Check if .intent-engine directory exists
417        // If not, this is not an initialized project yet
418        if !intent_dir.exists() || !intent_dir.is_dir() {
419            return Err(IntentError::NotAProject);
420        }
421
422        let db_path = intent_dir.join(DB_FILE);
423
424        let pool = create_pool(&db_path).await?;
425
426        // Always ensure migrations are run (idempotent operation)
427        // This handles cases where the database file exists but hasn't been migrated yet
428        run_migrations(&pool).await?;
429
430        Ok(ProjectContext {
431            root,
432            db_path,
433            pool,
434        })
435    }
436
437    /// Load project context, initializing if necessary (for write commands)
438    pub async fn load_or_init() -> Result<Self> {
439        match Self::load().await {
440            Ok(ctx) => Ok(ctx),
441            Err(IntentError::NotAProject) => Self::initialize_project().await,
442            Err(e) => Err(e),
443        }
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    // Note: Tests that modify the current directory are intentionally limited
452    // because they can interfere with other tests running in parallel.
453    // These functionalities are thoroughly tested by integration tests.
454
455    #[test]
456    fn test_constants() {
457        assert_eq!(INTENT_DIR, ".intent-engine");
458        assert_eq!(DB_FILE, "project.db");
459    }
460
461    #[test]
462    fn test_project_context_debug() {
463        // Just verify that ProjectContext implements Debug
464        // We can't easily create one without side effects in a unit test
465        let _type_check = |ctx: ProjectContext| {
466            let _ = format!("{:?}", ctx);
467        };
468    }
469
470    #[test]
471    fn test_project_root_markers_list() {
472        // Verify that the markers list contains expected markers
473        assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
474        assert!(PROJECT_ROOT_MARKERS.contains(&"Cargo.toml"));
475        assert!(PROJECT_ROOT_MARKERS.contains(&"package.json"));
476    }
477
478    #[test]
479    fn test_project_root_markers_priority() {
480        // Verify that .git has highest priority (comes first)
481        assert_eq!(PROJECT_ROOT_MARKERS[0], ".git");
482    }
483
484    /// Test infer_project_root in an isolated environment
485    /// Note: This test creates a temporary directory structure but doesn't change CWD
486    #[test]
487    fn test_infer_project_root_with_git() {
488        // This test is limited because we can't easily change CWD in unit tests
489        // The actual behavior is tested in integration tests
490        // Here we just verify the marker list is correct
491        assert!(PROJECT_ROOT_MARKERS.contains(&".git"));
492    }
493
494    /// Test that markers list includes all major project types
495    #[test]
496    fn test_all_major_project_types_covered() {
497        let markers = PROJECT_ROOT_MARKERS;
498
499        // Git version control
500        assert!(markers.contains(&".git"));
501        assert!(markers.contains(&".hg"));
502
503        // Programming languages
504        assert!(markers.contains(&"Cargo.toml")); // Rust
505        assert!(markers.contains(&"package.json")); // Node.js
506        assert!(markers.contains(&"pyproject.toml")); // Python
507        assert!(markers.contains(&"go.mod")); // Go
508        assert!(markers.contains(&"pom.xml")); // Java (Maven)
509        assert!(markers.contains(&"build.gradle")); // Java/Kotlin (Gradle)
510    }
511
512    /// Test DirectoryTraversalInfo structure
513    #[test]
514    fn test_directory_traversal_info_creation() {
515        let info = DirectoryTraversalInfo {
516            path: "/test/path".to_string(),
517            has_intent_engine: true,
518            is_selected: false,
519        };
520
521        assert_eq!(info.path, "/test/path");
522        assert!(info.has_intent_engine);
523        assert!(!info.is_selected);
524    }
525
526    /// Test DirectoryTraversalInfo Clone trait
527    #[test]
528    fn test_directory_traversal_info_clone() {
529        let info = DirectoryTraversalInfo {
530            path: "/test/path".to_string(),
531            has_intent_engine: true,
532            is_selected: true,
533        };
534
535        let cloned = info.clone();
536        assert_eq!(cloned.path, info.path);
537        assert_eq!(cloned.has_intent_engine, info.has_intent_engine);
538        assert_eq!(cloned.is_selected, info.is_selected);
539    }
540
541    /// Test DirectoryTraversalInfo Debug trait
542    #[test]
543    fn test_directory_traversal_info_debug() {
544        let info = DirectoryTraversalInfo {
545            path: "/test/path".to_string(),
546            has_intent_engine: false,
547            is_selected: true,
548        };
549
550        let debug_str = format!("{:?}", info);
551        assert!(debug_str.contains("DirectoryTraversalInfo"));
552        assert!(debug_str.contains("/test/path"));
553    }
554
555    /// Test DirectoryTraversalInfo serialization
556    #[test]
557    fn test_directory_traversal_info_serialization() {
558        let info = DirectoryTraversalInfo {
559            path: "/test/path".to_string(),
560            has_intent_engine: true,
561            is_selected: false,
562        };
563
564        let json = serde_json::to_string(&info).unwrap();
565        assert!(json.contains("path"));
566        assert!(json.contains("has_intent_engine"));
567        assert!(json.contains("is_selected"));
568        assert!(json.contains("/test/path"));
569    }
570
571    /// Test DirectoryTraversalInfo deserialization
572    #[test]
573    fn test_directory_traversal_info_deserialization() {
574        let json = r#"{"path":"/test/path","has_intent_engine":true,"is_selected":false}"#;
575        let info: DirectoryTraversalInfo = serde_json::from_str(json).unwrap();
576
577        assert_eq!(info.path, "/test/path");
578        assert!(info.has_intent_engine);
579        assert!(!info.is_selected);
580    }
581
582    /// Test DatabasePathInfo structure creation
583    #[test]
584    fn test_database_path_info_creation() {
585        let info = DatabasePathInfo {
586            current_working_directory: "/test/cwd".to_string(),
587            env_var_set: false,
588            env_var_path: None,
589            env_var_valid: None,
590            directories_checked: vec![],
591            home_directory: Some("/home/user".to_string()),
592            home_has_intent_engine: false,
593            final_database_path: Some("/test/db.db".to_string()),
594            resolution_method: Some("Test Method".to_string()),
595        };
596
597        assert_eq!(info.current_working_directory, "/test/cwd");
598        assert!(!info.env_var_set);
599        assert_eq!(info.env_var_path, None);
600        assert_eq!(info.home_directory, Some("/home/user".to_string()));
601        assert!(!info.home_has_intent_engine);
602        assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
603        assert_eq!(info.resolution_method, Some("Test Method".to_string()));
604    }
605
606    /// Test DatabasePathInfo with environment variable set
607    #[test]
608    fn test_database_path_info_with_env_var() {
609        let info = DatabasePathInfo {
610            current_working_directory: "/test/cwd".to_string(),
611            env_var_set: true,
612            env_var_path: Some("/env/path".to_string()),
613            env_var_valid: Some(true),
614            directories_checked: vec![],
615            home_directory: Some("/home/user".to_string()),
616            home_has_intent_engine: false,
617            final_database_path: Some("/env/path/.intent-engine/project.db".to_string()),
618            resolution_method: Some("Environment Variable".to_string()),
619        };
620
621        assert!(info.env_var_set);
622        assert_eq!(info.env_var_path, Some("/env/path".to_string()));
623        assert_eq!(info.env_var_valid, Some(true));
624        assert_eq!(
625            info.resolution_method,
626            Some("Environment Variable".to_string())
627        );
628    }
629
630    /// Test DatabasePathInfo with directories checked
631    #[test]
632    fn test_database_path_info_with_directories() {
633        let dirs = vec![
634            DirectoryTraversalInfo {
635                path: "/test/path1".to_string(),
636                has_intent_engine: false,
637                is_selected: false,
638            },
639            DirectoryTraversalInfo {
640                path: "/test/path2".to_string(),
641                has_intent_engine: true,
642                is_selected: true,
643            },
644        ];
645
646        let info = DatabasePathInfo {
647            current_working_directory: "/test/path1".to_string(),
648            env_var_set: false,
649            env_var_path: None,
650            env_var_valid: None,
651            directories_checked: dirs.clone(),
652            home_directory: Some("/home/user".to_string()),
653            home_has_intent_engine: false,
654            final_database_path: Some("/test/path2/.intent-engine/project.db".to_string()),
655            resolution_method: Some("Upward Directory Traversal".to_string()),
656        };
657
658        assert_eq!(info.directories_checked.len(), 2);
659        assert!(!info.directories_checked[0].has_intent_engine);
660        assert!(info.directories_checked[1].has_intent_engine);
661        assert!(info.directories_checked[1].is_selected);
662    }
663
664    /// Test DatabasePathInfo Debug trait
665    #[test]
666    fn test_database_path_info_debug() {
667        let info = DatabasePathInfo {
668            current_working_directory: "/test/cwd".to_string(),
669            env_var_set: false,
670            env_var_path: None,
671            env_var_valid: None,
672            directories_checked: vec![],
673            home_directory: Some("/home/user".to_string()),
674            home_has_intent_engine: false,
675            final_database_path: Some("/test/db.db".to_string()),
676            resolution_method: Some("Test".to_string()),
677        };
678
679        let debug_str = format!("{:?}", info);
680        assert!(debug_str.contains("DatabasePathInfo"));
681        assert!(debug_str.contains("/test/cwd"));
682    }
683
684    /// Test DatabasePathInfo serialization
685    #[test]
686    fn test_database_path_info_serialization() {
687        let info = DatabasePathInfo {
688            current_working_directory: "/test/cwd".to_string(),
689            env_var_set: true,
690            env_var_path: Some("/env/path".to_string()),
691            env_var_valid: Some(true),
692            directories_checked: vec![],
693            home_directory: Some("/home/user".to_string()),
694            home_has_intent_engine: false,
695            final_database_path: Some("/test/db.db".to_string()),
696            resolution_method: Some("Test Method".to_string()),
697        };
698
699        let json = serde_json::to_string(&info).unwrap();
700        assert!(json.contains("current_working_directory"));
701        assert!(json.contains("env_var_set"));
702        assert!(json.contains("env_var_path"));
703        assert!(json.contains("final_database_path"));
704        assert!(json.contains("/test/cwd"));
705        assert!(json.contains("/env/path"));
706    }
707
708    /// Test DatabasePathInfo deserialization
709    #[test]
710    fn test_database_path_info_deserialization() {
711        let json = r#"{
712            "current_working_directory": "/test/cwd",
713            "env_var_set": true,
714            "env_var_path": "/env/path",
715            "env_var_valid": true,
716            "directories_checked": [],
717            "home_directory": "/home/user",
718            "home_has_intent_engine": false,
719            "final_database_path": "/test/db.db",
720            "resolution_method": "Test Method"
721        }"#;
722
723        let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
724        assert_eq!(info.current_working_directory, "/test/cwd");
725        assert!(info.env_var_set);
726        assert_eq!(info.env_var_path, Some("/env/path".to_string()));
727        assert_eq!(info.env_var_valid, Some(true));
728        assert_eq!(info.home_directory, Some("/home/user".to_string()));
729        assert_eq!(info.final_database_path, Some("/test/db.db".to_string()));
730        assert_eq!(info.resolution_method, Some("Test Method".to_string()));
731    }
732
733    /// Test DatabasePathInfo with complete directory traversal data
734    #[test]
735    fn test_database_path_info_complete_structure() {
736        let dirs = vec![
737            DirectoryTraversalInfo {
738                path: "/home/user/project/src".to_string(),
739                has_intent_engine: false,
740                is_selected: false,
741            },
742            DirectoryTraversalInfo {
743                path: "/home/user/project".to_string(),
744                has_intent_engine: true,
745                is_selected: true,
746            },
747            DirectoryTraversalInfo {
748                path: "/home/user".to_string(),
749                has_intent_engine: false,
750                is_selected: false,
751            },
752        ];
753
754        let info = DatabasePathInfo {
755            current_working_directory: "/home/user/project/src".to_string(),
756            env_var_set: false,
757            env_var_path: None,
758            env_var_valid: None,
759            directories_checked: dirs,
760            home_directory: Some("/home/user".to_string()),
761            home_has_intent_engine: false,
762            final_database_path: Some("/home/user/project/.intent-engine/project.db".to_string()),
763            resolution_method: Some("Upward Directory Traversal".to_string()),
764        };
765
766        // Verify the complete structure
767        assert_eq!(info.directories_checked.len(), 3);
768        assert_eq!(info.directories_checked[0].path, "/home/user/project/src");
769        assert_eq!(info.directories_checked[1].path, "/home/user/project");
770        assert_eq!(info.directories_checked[2].path, "/home/user");
771
772        // Only the second directory should be selected
773        assert!(!info.directories_checked[0].is_selected);
774        assert!(info.directories_checked[1].is_selected);
775        assert!(!info.directories_checked[2].is_selected);
776
777        // Only the second directory has intent-engine
778        assert!(!info.directories_checked[0].has_intent_engine);
779        assert!(info.directories_checked[1].has_intent_engine);
780        assert!(!info.directories_checked[2].has_intent_engine);
781    }
782
783    /// Test get_database_path_info returns valid structure
784    #[test]
785    fn test_get_database_path_info_structure() {
786        let info = ProjectContext::get_database_path_info();
787
788        // Verify basic structure is populated
789        assert!(!info.current_working_directory.is_empty());
790
791        // Verify that we got some result for directories checked or home directory
792        let has_data = !info.directories_checked.is_empty()
793            || info.home_directory.is_some()
794            || info.env_var_set;
795
796        assert!(
797            has_data,
798            "get_database_path_info should return some directory information"
799        );
800    }
801
802    /// Test get_database_path_info with actual filesystem
803    #[test]
804    fn test_get_database_path_info_checks_current_dir() {
805        let info = ProjectContext::get_database_path_info();
806
807        // The current working directory should be set
808        assert!(!info.current_working_directory.is_empty());
809
810        // At minimum, it should check the current directory (unless env var is set and valid)
811        if !info.env_var_set || info.env_var_valid != Some(true) {
812            assert!(
813                !info.directories_checked.is_empty(),
814                "Should check at least the current directory"
815            );
816        }
817    }
818
819    /// Test get_database_path_info includes current directory in checked list
820    #[test]
821    fn test_get_database_path_info_includes_cwd() {
822        let info = ProjectContext::get_database_path_info();
823
824        // If env var is not set or invalid, should check directories
825        if !info.env_var_set || info.env_var_valid != Some(true) {
826            assert!(!info.directories_checked.is_empty());
827
828            // First checked directory should start with the CWD or be a parent
829            let cwd = &info.current_working_directory;
830            let first_checked = &info.directories_checked[0].path;
831
832            assert!(
833                cwd.starts_with(first_checked) || first_checked.starts_with(cwd),
834                "First checked directory should be related to CWD"
835            );
836        }
837    }
838
839    /// Test get_database_path_info resolution method is set when database found
840    #[test]
841    fn test_get_database_path_info_resolution_method() {
842        let info = ProjectContext::get_database_path_info();
843
844        // If a database path was found, resolution method should be set
845        if info.final_database_path.is_some() {
846            assert!(
847                info.resolution_method.is_some(),
848                "Resolution method should be set when database path is found"
849            );
850
851            let method = info.resolution_method.unwrap();
852            assert!(
853                method.contains("Environment Variable")
854                    || method.contains("Upward Directory Traversal")
855                    || method.contains("Home Directory"),
856                "Resolution method should be one of the known strategies"
857            );
858        }
859    }
860
861    /// Test get_database_path_info marks selected directory correctly
862    #[test]
863    fn test_get_database_path_info_selected_directory() {
864        let info = ProjectContext::get_database_path_info();
865
866        // If env var not used and directories were checked
867        if (!info.env_var_set || info.env_var_valid != Some(true))
868            && !info.directories_checked.is_empty()
869            && info.final_database_path.is_some()
870        {
871            // Exactly one directory should be marked as selected
872            let selected_count = info
873                .directories_checked
874                .iter()
875                .filter(|d| d.is_selected)
876                .count();
877
878            assert!(
879                selected_count <= 1,
880                "At most one directory should be marked as selected"
881            );
882
883            // If one is selected, it should have intent_engine
884            if let Some(selected) = info.directories_checked.iter().find(|d| d.is_selected) {
885                assert!(
886                    selected.has_intent_engine,
887                    "Selected directory should have .intent-engine"
888                );
889            }
890        }
891    }
892
893    /// Test DatabasePathInfo with no database found scenario
894    #[test]
895    fn test_database_path_info_no_database_found() {
896        let info = DatabasePathInfo {
897            current_working_directory: "/test/path".to_string(),
898            env_var_set: false,
899            env_var_path: None,
900            env_var_valid: None,
901            directories_checked: vec![
902                DirectoryTraversalInfo {
903                    path: "/test/path".to_string(),
904                    has_intent_engine: false,
905                    is_selected: false,
906                },
907                DirectoryTraversalInfo {
908                    path: "/test".to_string(),
909                    has_intent_engine: false,
910                    is_selected: false,
911                },
912            ],
913            home_directory: Some("/home/user".to_string()),
914            home_has_intent_engine: false,
915            final_database_path: None,
916            resolution_method: None,
917        };
918
919        assert!(info.final_database_path.is_none());
920        assert!(info.resolution_method.is_none());
921        assert_eq!(info.directories_checked.len(), 2);
922        assert!(!info.home_has_intent_engine);
923    }
924
925    /// Test DatabasePathInfo with env var set but invalid
926    #[test]
927    fn test_database_path_info_env_var_invalid() {
928        let info = DatabasePathInfo {
929            current_working_directory: "/test/cwd".to_string(),
930            env_var_set: true,
931            env_var_path: Some("/invalid/path".to_string()),
932            env_var_valid: Some(false),
933            directories_checked: vec![DirectoryTraversalInfo {
934                path: "/test/cwd".to_string(),
935                has_intent_engine: true,
936                is_selected: true,
937            }],
938            home_directory: Some("/home/user".to_string()),
939            home_has_intent_engine: false,
940            final_database_path: Some("/test/cwd/.intent-engine/project.db".to_string()),
941            resolution_method: Some("Upward Directory Traversal".to_string()),
942        };
943
944        assert!(info.env_var_set);
945        assert_eq!(info.env_var_valid, Some(false));
946        assert!(info.final_database_path.is_some());
947        // Should fallback to directory traversal when env var is invalid
948        assert!(info.resolution_method.unwrap().contains("Upward Directory"));
949    }
950
951    /// Test DatabasePathInfo with home directory fallback
952    #[test]
953    fn test_database_path_info_home_directory_used() {
954        let info = DatabasePathInfo {
955            current_working_directory: "/tmp/work".to_string(),
956            env_var_set: false,
957            env_var_path: None,
958            env_var_valid: None,
959            directories_checked: vec![
960                DirectoryTraversalInfo {
961                    path: "/tmp/work".to_string(),
962                    has_intent_engine: false,
963                    is_selected: false,
964                },
965                DirectoryTraversalInfo {
966                    path: "/tmp".to_string(),
967                    has_intent_engine: false,
968                    is_selected: false,
969                },
970            ],
971            home_directory: Some("/home/user".to_string()),
972            home_has_intent_engine: true,
973            final_database_path: Some("/home/user/.intent-engine/project.db".to_string()),
974            resolution_method: Some("Home Directory Fallback".to_string()),
975        };
976
977        assert!(info.home_has_intent_engine);
978        assert_eq!(
979            info.final_database_path,
980            Some("/home/user/.intent-engine/project.db".to_string())
981        );
982        assert_eq!(
983            info.resolution_method,
984            Some("Home Directory Fallback".to_string())
985        );
986    }
987
988    /// Test DatabasePathInfo serialization round-trip with all fields
989    #[test]
990    fn test_database_path_info_full_roundtrip() {
991        let original = DatabasePathInfo {
992            current_working_directory: "/test/cwd".to_string(),
993            env_var_set: true,
994            env_var_path: Some("/env/path".to_string()),
995            env_var_valid: Some(false),
996            directories_checked: vec![
997                DirectoryTraversalInfo {
998                    path: "/test/cwd".to_string(),
999                    has_intent_engine: false,
1000                    is_selected: false,
1001                },
1002                DirectoryTraversalInfo {
1003                    path: "/test".to_string(),
1004                    has_intent_engine: true,
1005                    is_selected: true,
1006                },
1007            ],
1008            home_directory: Some("/home/user".to_string()),
1009            home_has_intent_engine: false,
1010            final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1011            resolution_method: Some("Upward Directory Traversal".to_string()),
1012        };
1013
1014        // Serialize
1015        let json = serde_json::to_string(&original).unwrap();
1016
1017        // Deserialize
1018        let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1019
1020        // Verify all fields match
1021        assert_eq!(
1022            deserialized.current_working_directory,
1023            original.current_working_directory
1024        );
1025        assert_eq!(deserialized.env_var_set, original.env_var_set);
1026        assert_eq!(deserialized.env_var_path, original.env_var_path);
1027        assert_eq!(deserialized.env_var_valid, original.env_var_valid);
1028        assert_eq!(
1029            deserialized.directories_checked.len(),
1030            original.directories_checked.len()
1031        );
1032        assert_eq!(deserialized.home_directory, original.home_directory);
1033        assert_eq!(
1034            deserialized.home_has_intent_engine,
1035            original.home_has_intent_engine
1036        );
1037        assert_eq!(
1038            deserialized.final_database_path,
1039            original.final_database_path
1040        );
1041        assert_eq!(deserialized.resolution_method, original.resolution_method);
1042    }
1043
1044    /// Test DirectoryTraversalInfo with all boolean combinations
1045    #[test]
1046    fn test_directory_traversal_info_all_combinations() {
1047        // Test all 4 combinations of boolean flags
1048        let combinations = [(false, false), (false, true), (true, false), (true, true)];
1049
1050        for (has_ie, is_sel) in combinations.iter() {
1051            let info = DirectoryTraversalInfo {
1052                path: format!("/test/path/{}_{}", has_ie, is_sel),
1053                has_intent_engine: *has_ie,
1054                is_selected: *is_sel,
1055            };
1056
1057            assert_eq!(info.has_intent_engine, *has_ie);
1058            assert_eq!(info.is_selected, *is_sel);
1059        }
1060    }
1061
1062    /// Test DirectoryTraversalInfo serialization preserves exact values
1063    #[test]
1064    fn test_directory_traversal_info_exact_serialization() {
1065        let info = DirectoryTraversalInfo {
1066            path: "/exact/path/with/special-chars_123".to_string(),
1067            has_intent_engine: true,
1068            is_selected: false,
1069        };
1070
1071        let json = serde_json::to_string(&info).unwrap();
1072        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1073
1074        assert_eq!(info.path, deserialized.path);
1075        assert_eq!(info.has_intent_engine, deserialized.has_intent_engine);
1076        assert_eq!(info.is_selected, deserialized.is_selected);
1077    }
1078
1079    /// Test DatabasePathInfo with None values for optional fields
1080    #[test]
1081    fn test_database_path_info_all_none() {
1082        let info = DatabasePathInfo {
1083            current_working_directory: "/test".to_string(),
1084            env_var_set: false,
1085            env_var_path: None,
1086            env_var_valid: None,
1087            directories_checked: vec![],
1088            home_directory: None,
1089            home_has_intent_engine: false,
1090            final_database_path: None,
1091            resolution_method: None,
1092        };
1093
1094        assert!(!info.env_var_set);
1095        assert!(info.env_var_path.is_none());
1096        assert!(info.env_var_valid.is_none());
1097        assert!(info.directories_checked.is_empty());
1098        assert!(info.home_directory.is_none());
1099        assert!(info.final_database_path.is_none());
1100        assert!(info.resolution_method.is_none());
1101    }
1102
1103    /// Test DatabasePathInfo with Some values for all optional fields
1104    #[test]
1105    fn test_database_path_info_all_some() {
1106        let info = DatabasePathInfo {
1107            current_working_directory: "/test".to_string(),
1108            env_var_set: true,
1109            env_var_path: Some("/env".to_string()),
1110            env_var_valid: Some(true),
1111            directories_checked: vec![DirectoryTraversalInfo {
1112                path: "/test".to_string(),
1113                has_intent_engine: true,
1114                is_selected: true,
1115            }],
1116            home_directory: Some("/home".to_string()),
1117            home_has_intent_engine: true,
1118            final_database_path: Some("/test/.intent-engine/project.db".to_string()),
1119            resolution_method: Some("Test Method".to_string()),
1120        };
1121
1122        assert!(info.env_var_set);
1123        assert!(info.env_var_path.is_some());
1124        assert!(info.env_var_valid.is_some());
1125        assert!(!info.directories_checked.is_empty());
1126        assert!(info.home_directory.is_some());
1127        assert!(info.final_database_path.is_some());
1128        assert!(info.resolution_method.is_some());
1129    }
1130
1131    /// Test get_database_path_info home directory field is set
1132    #[test]
1133    fn test_get_database_path_info_home_directory() {
1134        let info = ProjectContext::get_database_path_info();
1135
1136        // Home directory should typically be set (unless in very restricted environment)
1137        // This tests that the home directory detection logic runs
1138        if std::env::var("HOME").is_ok() {
1139            assert!(
1140                info.home_directory.is_some(),
1141                "HOME env var is set, so home_directory should be Some"
1142            );
1143        }
1144    }
1145
1146    /// Test get_database_path_info doesn't panic with edge cases
1147    #[test]
1148    fn test_get_database_path_info_no_panic() {
1149        // This test ensures the function handles edge cases gracefully
1150        // Even in unusual environments, it should return valid data
1151        let info = ProjectContext::get_database_path_info();
1152
1153        // Basic sanity checks - should always have these
1154        assert!(!info.current_working_directory.is_empty());
1155
1156        // If final_database_path is None, that's okay - it means no database was found
1157        // The function should still provide diagnostic information
1158        if info.final_database_path.is_none() {
1159            // Should still have checked some directories or reported env var status
1160            let has_diagnostic_info = !info.directories_checked.is_empty()
1161                || info.env_var_set
1162                || info.home_directory.is_some();
1163
1164            assert!(
1165                has_diagnostic_info,
1166                "Even without finding a database, should provide diagnostic information"
1167            );
1168        }
1169    }
1170
1171    /// Test get_database_path_info with multiple .intent-engine directories
1172    #[test]
1173    fn test_get_database_path_info_prefers_first_match() {
1174        let info = ProjectContext::get_database_path_info();
1175
1176        // If database was found via directory traversal and multiple directories were checked
1177        if info
1178            .resolution_method
1179            .as_ref()
1180            .is_some_and(|m| m.contains("Upward Directory"))
1181            && info.directories_checked.len() > 1
1182        {
1183            // Find all directories with .intent-engine
1184            let with_ie: Vec<_> = info
1185                .directories_checked
1186                .iter()
1187                .filter(|d| d.has_intent_engine)
1188                .collect();
1189
1190            if with_ie.len() > 1 {
1191                // Only the first one found (closest to CWD) should be selected
1192                let selected: Vec<_> = with_ie.iter().filter(|d| d.is_selected).collect();
1193                assert!(
1194                    selected.len() <= 1,
1195                    "Only the first .intent-engine found should be selected"
1196                );
1197            }
1198        }
1199    }
1200
1201    /// Test DatabasePathInfo deserialization with missing optional fields
1202    #[test]
1203    fn test_database_path_info_partial_deserialization() {
1204        // Test with minimal required fields
1205        let json = r#"{
1206            "current_working_directory": "/test",
1207            "env_var_set": false,
1208            "env_var_path": null,
1209            "env_var_valid": null,
1210            "directories_checked": [],
1211            "home_directory": null,
1212            "home_has_intent_engine": false,
1213            "final_database_path": null,
1214            "resolution_method": null
1215        }"#;
1216
1217        let info: DatabasePathInfo = serde_json::from_str(json).unwrap();
1218        assert_eq!(info.current_working_directory, "/test");
1219        assert!(!info.env_var_set);
1220    }
1221
1222    /// Test DatabasePathInfo JSON format matches expected schema
1223    #[test]
1224    fn test_database_path_info_json_schema() {
1225        let info = DatabasePathInfo {
1226            current_working_directory: "/test".to_string(),
1227            env_var_set: true,
1228            env_var_path: Some("/env".to_string()),
1229            env_var_valid: Some(true),
1230            directories_checked: vec![],
1231            home_directory: Some("/home".to_string()),
1232            home_has_intent_engine: false,
1233            final_database_path: Some("/db".to_string()),
1234            resolution_method: Some("Test".to_string()),
1235        };
1236
1237        let json_value: serde_json::Value = serde_json::to_value(&info).unwrap();
1238
1239        // Verify all expected fields are present
1240        assert!(json_value.get("current_working_directory").is_some());
1241        assert!(json_value.get("env_var_set").is_some());
1242        assert!(json_value.get("env_var_path").is_some());
1243        assert!(json_value.get("env_var_valid").is_some());
1244        assert!(json_value.get("directories_checked").is_some());
1245        assert!(json_value.get("home_directory").is_some());
1246        assert!(json_value.get("home_has_intent_engine").is_some());
1247        assert!(json_value.get("final_database_path").is_some());
1248        assert!(json_value.get("resolution_method").is_some());
1249    }
1250
1251    /// Test DirectoryTraversalInfo with empty path
1252    #[test]
1253    fn test_directory_traversal_info_empty_path() {
1254        let info = DirectoryTraversalInfo {
1255            path: "".to_string(),
1256            has_intent_engine: false,
1257            is_selected: false,
1258        };
1259
1260        assert_eq!(info.path, "");
1261        let json = serde_json::to_string(&info).unwrap();
1262        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1263        assert_eq!(deserialized.path, "");
1264    }
1265
1266    /// Test DirectoryTraversalInfo with unicode path
1267    #[test]
1268    fn test_directory_traversal_info_unicode_path() {
1269        let info = DirectoryTraversalInfo {
1270            path: "/test/路径/مسار/путь".to_string(),
1271            has_intent_engine: true,
1272            is_selected: false,
1273        };
1274
1275        let json = serde_json::to_string(&info).unwrap();
1276        let deserialized: DirectoryTraversalInfo = serde_json::from_str(&json).unwrap();
1277        assert_eq!(deserialized.path, "/test/路径/مسار/путь");
1278    }
1279
1280    /// Test DatabasePathInfo with very long paths
1281    #[test]
1282    fn test_database_path_info_long_paths() {
1283        let long_path = "/".to_owned() + &"very_long_directory_name/".repeat(50);
1284        let info = DatabasePathInfo {
1285            current_working_directory: long_path.clone(),
1286            env_var_set: false,
1287            env_var_path: None,
1288            env_var_valid: None,
1289            directories_checked: vec![],
1290            home_directory: Some(long_path.clone()),
1291            home_has_intent_engine: false,
1292            final_database_path: Some(long_path.clone()),
1293            resolution_method: Some("Test".to_string()),
1294        };
1295
1296        let json = serde_json::to_string(&info).unwrap();
1297        let deserialized: DatabasePathInfo = serde_json::from_str(&json).unwrap();
1298        assert_eq!(deserialized.current_working_directory, long_path);
1299    }
1300
1301    /// Test get_database_path_info env var handling
1302    #[test]
1303    fn test_get_database_path_info_env_var_detection() {
1304        let info = ProjectContext::get_database_path_info();
1305
1306        // Check if INTENT_ENGINE_PROJECT_DIR is set
1307        if std::env::var("INTENT_ENGINE_PROJECT_DIR").is_ok() {
1308            assert!(
1309                info.env_var_set,
1310                "env_var_set should be true when INTENT_ENGINE_PROJECT_DIR is set"
1311            );
1312            assert!(
1313                info.env_var_path.is_some(),
1314                "env_var_path should contain the path when env var is set"
1315            );
1316            assert!(
1317                info.env_var_valid.is_some(),
1318                "env_var_valid should be set when env var is present"
1319            );
1320        } else {
1321            assert!(
1322                !info.env_var_set,
1323                "env_var_set should be false when INTENT_ENGINE_PROJECT_DIR is not set"
1324            );
1325            assert!(
1326                info.env_var_path.is_none(),
1327                "env_var_path should be None when env var is not set"
1328            );
1329            assert!(
1330                info.env_var_valid.is_none(),
1331                "env_var_valid should be None when env var is not set"
1332            );
1333        }
1334    }
1335}