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