intent_engine/
project.rs

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