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