Skip to main content

gglib_core/paths/
resolver.rs

1//! Pure path resolver for testing and CLI introspection.
2//!
3//! This module provides a single struct that captures all resolved paths
4//! in one call, making it easy to compare path resolution across adapters
5//! and expose via `gglib config paths` CLI command.
6
7use std::path::PathBuf;
8
9use super::{
10    ModelsDirSource, PathError, data_root, database_path, llama_server_path, resolve_models_dir,
11    resource_root,
12};
13
14/// All resolved paths captured in a single struct.
15///
16/// This is the "golden truth" for path resolution - use it for:
17/// - Integration tests comparing adapter parity
18/// - CLI `gglib config paths` command output
19/// - Debugging path resolution issues
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ResolvedPaths {
22    /// Root directory for application data (database, logs, etc.)
23    pub data_root: PathBuf,
24    /// Root directory for application resources (binaries, assets)
25    pub resource_root: PathBuf,
26    /// Path to the `SQLite` database file
27    pub database_path: PathBuf,
28    /// Path to the llama-server binary
29    pub llama_server_path: PathBuf,
30    /// Path to the models directory
31    pub models_dir: PathBuf,
32    /// How the models directory was resolved
33    pub models_source: ModelsDirSource,
34}
35
36impl ResolvedPaths {
37    /// Resolve all paths using the current environment.
38    ///
39    /// This calls each path resolver once and captures the results.
40    /// Use this instead of calling individual resolvers when you need
41    /// multiple paths - it's more efficient and guarantees consistency.
42    pub fn resolve() -> Result<Self, PathError> {
43        let data_root = data_root()?;
44        let resource_root = resource_root()?;
45        let database_path = database_path()?;
46        let llama_server_path = llama_server_path()?;
47        let models_resolution = resolve_models_dir(None)?;
48
49        Ok(Self {
50            data_root,
51            resource_root,
52            database_path,
53            llama_server_path,
54            models_dir: models_resolution.path,
55            models_source: models_resolution.source,
56        })
57    }
58
59    /// Resolve with an explicit models directory override.
60    ///
61    /// Use this to test behavior when `--models-dir` is passed.
62    pub fn resolve_with_models_dir(models_dir: Option<&str>) -> Result<Self, PathError> {
63        let data_root = data_root()?;
64        let resource_root = resource_root()?;
65        let database_path = database_path()?;
66        let llama_server_path = llama_server_path()?;
67        let models_resolution = resolve_models_dir(models_dir)?;
68
69        Ok(Self {
70            data_root,
71            resource_root,
72            database_path,
73            llama_server_path,
74            models_dir: models_resolution.path,
75            models_source: models_resolution.source,
76        })
77    }
78}
79
80impl std::fmt::Display for ResolvedPaths {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        writeln!(f, "data_root = {}", self.data_root.display())?;
83        writeln!(f, "resource_root = {}", self.resource_root.display())?;
84        writeln!(f, "database_path = {}", self.database_path.display())?;
85        writeln!(
86            f,
87            "llama_server_path = {}",
88            self.llama_server_path.display()
89        )?;
90        writeln!(f, "models_dir = {}", self.models_dir.display())?;
91        write!(f, "models_source = {:?}", self.models_source)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::paths::test_utils::{ENV_LOCK, EnvVarGuard};
99    use tempfile::tempdir;
100
101    #[test]
102    fn resolve_returns_consistent_paths() {
103        // Lock ensures this test doesn't run concurrently with config.rs tests
104        // that modify GGLIB_DATA_DIR, preventing non-deterministic results
105        let _guard = ENV_LOCK.lock().unwrap();
106
107        let first = ResolvedPaths::resolve().expect("first resolve");
108        let second = ResolvedPaths::resolve().expect("second resolve");
109
110        assert_eq!(first, second, "path resolution should be deterministic");
111    }
112
113    #[test]
114    fn display_format_is_parseable() {
115        // Under the same lock as every test that sets GGLIB_DATA_DIR: without
116        // it this test resolved whatever root a neighbour had pointed the
117        // variable at, and once found that root being removed under it
118        // (#1082). Its own temporary data root keeps it off the real data
119        // directory as well, since resolve() creates what it names. (The
120        // resource root is still the checkout's in a debug build.)
121        let _guard = ENV_LOCK.lock().unwrap();
122        let temp = tempdir().unwrap();
123        let _env_guard = EnvVarGuard::set("GGLIB_DATA_DIR", temp.path().to_string_lossy().as_ref());
124
125        let paths = ResolvedPaths::resolve().expect("resolve");
126        let output = paths.to_string();
127
128        // The data root printed is this test's own, not whoever runs the tests'.
129        assert!(
130            output.contains(&format!("data_root = {}", temp.path().display())),
131            "resolved outside the test's own root:\n{output}"
132        );
133        // Should contain key = value pairs
134        assert!(output.contains("resource_root = "));
135        assert!(output.contains("database_path = "));
136        assert!(output.contains("llama_server_path = "));
137        assert!(output.contains("models_dir = "));
138        assert!(output.contains("models_source = "));
139    }
140}