Skip to main content

agentic_config/
paths.rs

1//! XDG path resolution for configuration files.
2//!
3//! This module provides explicit XDG Base Directory handling to ensure
4//! consistent paths across all Unix systems, including macOS (which doesn't
5//! use `~/Library/Application Support` for XDG-style applications).
6
7use anyhow::Context;
8use anyhow::Result;
9use std::path::PathBuf;
10
11/// Environment variable for test isolation of config paths.
12const TEST_CONFIG_DIR_VAR: &str = "__AGENTIC_CONFIG_DIR_FOR_TESTS";
13
14/// Read an env var, trim it, and return None if empty.
15fn env_path(var: &str) -> Option<PathBuf> {
16    std::env::var(var)
17        .ok()
18        .map(|v| v.trim().to_string())
19        .filter(|v| !v.is_empty())
20        .map(PathBuf::from)
21}
22
23/// XDG config base directory.
24///
25/// Precedence:
26/// 1. `__AGENTIC_CONFIG_DIR_FOR_TESTS` (test hook)
27/// 2. `XDG_CONFIG_HOME`
28/// 3. `$HOME/.config`
29///
30/// This function explicitly implements XDG Base Directory behavior rather than
31/// relying on `dirs::config_dir()`, which returns `~/Library/Application Support`
32/// on macOS. Our documented path is `~/.config/agentic/agentic.toml` across all
33/// Unix systems.
34pub fn xdg_config_home() -> Result<PathBuf> {
35    if let Some(p) = env_path(TEST_CONFIG_DIR_VAR) {
36        return Ok(p);
37    }
38    if let Some(p) = env_path("XDG_CONFIG_HOME") {
39        return Ok(p);
40    }
41    let home = env_path("HOME")
42        .or_else(dirs::home_dir)
43        .context("Could not determine $HOME for XDG config path")?;
44    Ok(home.join(".config"))
45}
46
47/// Get the agentic config directory (`~/.config/agentic`).
48pub fn agentic_config_dir() -> Result<PathBuf> {
49    Ok(xdg_config_home()?.join("agentic"))
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::test_support::EnvGuard;
56    use serial_test::serial;
57
58    #[test]
59    #[serial]
60    fn test_hook_overrides_xdg() {
61        let _guard = EnvGuard::set(TEST_CONFIG_DIR_VAR, "/test/override");
62        let result = xdg_config_home().unwrap();
63        assert_eq!(result, PathBuf::from("/test/override"));
64    }
65
66    #[test]
67    #[serial]
68    fn test_xdg_config_home_honored() {
69        let _g1 = EnvGuard::remove(TEST_CONFIG_DIR_VAR);
70        let _guard = EnvGuard::set("XDG_CONFIG_HOME", "/custom/config");
71        let result = xdg_config_home().unwrap();
72        assert_eq!(result, PathBuf::from("/custom/config"));
73    }
74
75    #[test]
76    #[serial]
77    fn test_empty_xdg_falls_back_to_home() {
78        let _g1 = EnvGuard::remove(TEST_CONFIG_DIR_VAR);
79        let _g2 = EnvGuard::set("XDG_CONFIG_HOME", "");
80        let _g3 = EnvGuard::set("HOME", "/home/test");
81        let result = xdg_config_home().unwrap();
82        assert_eq!(result, PathBuf::from("/home/test/.config"));
83    }
84
85    #[test]
86    #[serial]
87    fn test_agentic_config_dir() {
88        let _guard = EnvGuard::set(TEST_CONFIG_DIR_VAR, "/test/base");
89        let result = agentic_config_dir().unwrap();
90        assert_eq!(result, PathBuf::from("/test/base/agentic"));
91    }
92}