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