Skip to main content

relay_knowledge/paths/
mod.rs

1//! Platform path resolution for relay-knowledge runtime state.
2//!
3//! The module owns all default and override rules for config, data, state,
4//! cache, log, temp, runtime, and service directories. It never reads the
5//! process environment directly; callers pass the typed environment snapshot
6//! produced by `env`.
7
8use std::{
9    error::Error,
10    fmt,
11    path::{Component, Path, PathBuf},
12};
13
14use crate::{
15    env::{PathEnvOverrides, PlatformEnvironment, PlatformKind},
16    project::{
17        MODEL_CATALOG_CACHE_FILE_NAME, MODEL_FALLBACK_FILE_NAME, MODEL_PROFILES_FILE_NAME,
18        VERSION_CHECK_CACHE_FILE_NAME,
19    },
20};
21
22pub use crate::project::APP_DIR_NAME;
23
24/// Resolved runtime directories used by CLI, Web, services, and future workers.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct RuntimePaths {
27    pub config_dir: PathBuf,
28    pub data_dir: PathBuf,
29    pub state_dir: PathBuf,
30    pub cache_dir: PathBuf,
31    pub log_dir: PathBuf,
32    pub temp_dir: PathBuf,
33    pub runtime_dir: PathBuf,
34    pub service_dir: PathBuf,
35}
36
37impl RuntimePaths {
38    /// Resolves platform defaults and relay-specific overrides into absolute paths.
39    pub fn resolve(
40        environment: &PlatformEnvironment,
41        overrides: &PathEnvOverrides,
42    ) -> Result<Self, PathError> {
43        let defaults = if let Some(root) = overrides.home.as_deref() {
44            runtime_home_defaults(root)?
45        } else {
46            platform_defaults(environment)?
47        };
48
49        let resolved = Self {
50            config_dir: override_path(
51                PathPurpose::Config,
52                defaults.config_dir,
53                overrides.config_dir.as_deref(),
54            )?,
55            data_dir: override_path(
56                PathPurpose::Data,
57                defaults.data_dir,
58                overrides.data_dir.as_deref(),
59            )?,
60            state_dir: override_path(
61                PathPurpose::State,
62                defaults.state_dir,
63                overrides.state_dir.as_deref(),
64            )?,
65            cache_dir: override_path(
66                PathPurpose::Cache,
67                defaults.cache_dir,
68                overrides.cache_dir.as_deref(),
69            )?,
70            log_dir: override_path(
71                PathPurpose::Log,
72                defaults.log_dir,
73                overrides.log_dir.as_deref(),
74            )?,
75            temp_dir: override_path(
76                PathPurpose::Temp,
77                defaults.temp_dir,
78                overrides.temp_dir.as_deref(),
79            )?,
80            runtime_dir: override_path(
81                PathPurpose::Runtime,
82                defaults.runtime_dir,
83                overrides.runtime_dir.as_deref(),
84            )?,
85            service_dir: override_path(
86                PathPurpose::Service,
87                defaults.service_dir,
88                overrides.service_dir.as_deref(),
89            )?,
90        };
91
92        validate_all(&resolved)?;
93        Ok(resolved)
94    }
95
96    /// Returns the JSONL audit log owned by resident agent protocol adapters.
97    pub fn agent_audit_log_file(&self) -> PathBuf {
98        self.log_dir.join("agent-audit.jsonl")
99    }
100
101    /// Returns the model provider profile configuration file.
102    pub fn model_profiles_file(&self) -> PathBuf {
103        self.config_dir.join(MODEL_PROFILES_FILE_NAME)
104    }
105
106    /// Returns the model provider fallback-policy configuration file.
107    pub fn model_fallback_file(&self) -> PathBuf {
108        self.config_dir.join(MODEL_FALLBACK_FILE_NAME)
109    }
110
111    /// Returns the cached public model catalog file.
112    pub fn model_catalog_cache_file(&self) -> PathBuf {
113        self.cache_dir.join(MODEL_CATALOG_CACHE_FILE_NAME)
114    }
115
116    /// Returns the cached version-check result.
117    pub fn version_check_cache_file(&self) -> PathBuf {
118        self.cache_dir.join(VERSION_CHECK_CACHE_FILE_NAME)
119    }
120}
121
122/// Returns conservative user document roots for local file indexing.
123pub fn default_user_document_roots(
124    environment: &PlatformEnvironment,
125) -> Result<Vec<PathBuf>, PathError> {
126    let home = match environment.platform {
127        PlatformKind::Windows => environment
128            .home_dir
129            .as_deref()
130            .map(|path| validate_path(PathPurpose::Home, path).map(|_| path.to_path_buf()))
131            .transpose()?,
132        _ => validated_optional(PathPurpose::Home, environment.home_dir.as_deref())?
133            .map(Path::to_path_buf),
134    };
135    let Some(home) = home else {
136        return Ok(Vec::new());
137    };
138
139    Ok(["Documents", "Desktop", "Downloads"]
140        .into_iter()
141        .map(|child| home.join(child))
142        .collect())
143}
144
145/// Directory category attached to path validation failures.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum PathPurpose {
148    Home,
149    Config,
150    Data,
151    State,
152    Cache,
153    Log,
154    Temp,
155    Runtime,
156    Service,
157}
158
159impl fmt::Display for PathPurpose {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            Self::Home => write!(formatter, "home"),
163            Self::Config => write!(formatter, "config"),
164            Self::Data => write!(formatter, "data"),
165            Self::State => write!(formatter, "state"),
166            Self::Cache => write!(formatter, "cache"),
167            Self::Log => write!(formatter, "log"),
168            Self::Temp => write!(formatter, "temp"),
169            Self::Runtime => write!(formatter, "runtime"),
170            Self::Service => write!(formatter, "service"),
171        }
172    }
173}
174
175/// Path resolution or validation error.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct PathError {
178    pub purpose: PathPurpose,
179    pub kind: PathErrorKind,
180}
181
182impl PathError {
183    fn missing_base(purpose: PathPurpose, variable: &'static str) -> Self {
184        Self {
185            purpose,
186            kind: PathErrorKind::MissingBase { variable },
187        }
188    }
189
190    fn relative(purpose: PathPurpose, path: &Path) -> Self {
191        Self {
192            purpose,
193            kind: PathErrorKind::RelativePath {
194                path: path.to_path_buf(),
195            },
196        }
197    }
198
199    fn parent_component(purpose: PathPurpose, path: &Path) -> Self {
200        Self {
201            purpose,
202            kind: PathErrorKind::ParentComponent {
203                path: path.to_path_buf(),
204            },
205        }
206    }
207}
208
209/// Detailed path error category.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum PathErrorKind {
212    MissingBase { variable: &'static str },
213    RelativePath { path: PathBuf },
214    ParentComponent { path: PathBuf },
215}
216
217impl fmt::Display for PathError {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        match &self.kind {
220            PathErrorKind::MissingBase { variable } => write!(
221                formatter,
222                "cannot resolve {} directory because {variable} is unavailable",
223                self.purpose
224            ),
225            PathErrorKind::RelativePath { path } => write!(
226                formatter,
227                "{} directory must be absolute, got {}",
228                self.purpose,
229                path.display()
230            ),
231            PathErrorKind::ParentComponent { path } => write!(
232                formatter,
233                "{} directory must not contain '..', got {}",
234                self.purpose,
235                path.display()
236            ),
237        }
238    }
239}
240
241impl Error for PathError {}
242
243fn runtime_home_defaults(root: &Path) -> Result<RuntimePaths, PathError> {
244    validate_path(PathPurpose::Home, root)?;
245
246    Ok(RuntimePaths {
247        config_dir: root.join("config"),
248        data_dir: root.join("data"),
249        state_dir: root.join("state"),
250        cache_dir: root.join("cache"),
251        log_dir: root.join("logs"),
252        temp_dir: root.join("tmp"),
253        runtime_dir: root.join("run"),
254        service_dir: root.join("service"),
255    })
256}
257
258fn platform_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
259    match environment.platform {
260        PlatformKind::Macos => macos_defaults(environment),
261        PlatformKind::Windows => windows_defaults(environment),
262        PlatformKind::Unix | PlatformKind::Other => unix_defaults(environment),
263    }
264}
265
266fn unix_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
267    let home = validated_optional(PathPurpose::Home, environment.home_dir.as_deref())?;
268    let config_base = base_or_home_child(
269        PathPurpose::Config,
270        environment.xdg_config_home.as_deref(),
271        home,
272        ".config",
273        PathBuf::from("/etc"),
274    )?;
275    let data_base = base_or_home_child(
276        PathPurpose::Data,
277        environment.xdg_data_home.as_deref(),
278        home,
279        ".local/share",
280        PathBuf::from("/var/lib"),
281    )?;
282    let state_base = base_or_home_child(
283        PathPurpose::State,
284        environment.xdg_state_home.as_deref(),
285        home,
286        ".local/state",
287        PathBuf::from("/var/lib"),
288    )?;
289    let cache_base = base_or_home_child(
290        PathPurpose::Cache,
291        environment.xdg_cache_home.as_deref(),
292        home,
293        ".cache",
294        PathBuf::from("/var/cache"),
295    )?;
296    let temp_base = optional_or_default(
297        PathPurpose::Temp,
298        environment.temp_dir.as_deref(),
299        PathBuf::from("/tmp"),
300    )?;
301    let state_dir = state_base.join(APP_DIR_NAME);
302    let runtime_dir = if let Some(runtime_base) =
303        validated_optional(PathPurpose::Runtime, environment.xdg_runtime_dir.as_deref())?
304    {
305        runtime_base.join(APP_DIR_NAME)
306    } else {
307        state_dir.join("run")
308    };
309
310    Ok(RuntimePaths {
311        config_dir: config_base.join(APP_DIR_NAME),
312        data_dir: data_base.join(APP_DIR_NAME),
313        state_dir: state_dir.clone(),
314        cache_dir: cache_base.join(APP_DIR_NAME),
315        log_dir: state_dir.join("logs"),
316        temp_dir: temp_base.join(APP_DIR_NAME),
317        runtime_dir,
318        service_dir: config_base.join(APP_DIR_NAME).join("service"),
319    })
320}
321
322fn macos_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
323    let home = required_base(
324        PathPurpose::Home,
325        environment.home_dir.as_deref(),
326        HOME_REQUIRED,
327    )?;
328    let application_support = home.join("Library").join("Application Support");
329    let state_dir = application_support.join(APP_DIR_NAME).join("state");
330
331    Ok(RuntimePaths {
332        config_dir: application_support.join(APP_DIR_NAME).join("config"),
333        data_dir: application_support.join(APP_DIR_NAME).join("data"),
334        state_dir: state_dir.clone(),
335        cache_dir: home.join("Library").join("Caches").join(APP_DIR_NAME),
336        log_dir: home.join("Library").join("Logs").join(APP_DIR_NAME),
337        temp_dir: optional_or_default(
338            PathPurpose::Temp,
339            environment.temp_dir.as_deref(),
340            PathBuf::from("/tmp"),
341        )?
342        .join(APP_DIR_NAME),
343        runtime_dir: state_dir.join("run"),
344        service_dir: application_support.join(APP_DIR_NAME).join("service"),
345    })
346}
347
348fn windows_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
349    let config_base = environment
350        .app_data
351        .as_deref()
352        .map(|path| validate_path(PathPurpose::Config, path).map(|_| path.to_path_buf()))
353        .transpose()?
354        .or_else(|| {
355            environment
356                .home_dir
357                .as_ref()
358                .map(|home| home.join("AppData/Roaming"))
359        })
360        .ok_or_else(|| PathError::missing_base(PathPurpose::Config, "APPDATA or HOME"))?;
361    let local_base = environment
362        .local_app_data
363        .as_deref()
364        .map(|path| validate_path(PathPurpose::Data, path).map(|_| path.to_path_buf()))
365        .transpose()?
366        .or_else(|| {
367            environment
368                .home_dir
369                .as_ref()
370                .map(|home| home.join("AppData/Local"))
371        })
372        .ok_or_else(|| PathError::missing_base(PathPurpose::Data, "LOCALAPPDATA or HOME"))?;
373    let root = local_base.join(APP_DIR_NAME);
374    let temp_dir = match environment.temp_dir.as_deref() {
375        Some(path) => {
376            validate_path(PathPurpose::Temp, path)?;
377            path.join(APP_DIR_NAME)
378        }
379        None => root.join("tmp"),
380    };
381
382    Ok(RuntimePaths {
383        config_dir: config_base.join(APP_DIR_NAME),
384        data_dir: root.join("data"),
385        state_dir: root.join("state"),
386        cache_dir: root.join("cache"),
387        log_dir: root.join("logs"),
388        temp_dir,
389        runtime_dir: root.join("run"),
390        service_dir: config_base.join(APP_DIR_NAME).join("service"),
391    })
392}
393
394const HOME_REQUIRED: &str = "HOME";
395
396fn base_or_home_child(
397    purpose: PathPurpose,
398    configured: Option<&Path>,
399    home: Option<&Path>,
400    home_child: &str,
401    fallback_base: PathBuf,
402) -> Result<PathBuf, PathError> {
403    if let Some(path) = configured {
404        validate_path(purpose, path)?;
405        return Ok(path.to_path_buf());
406    }
407
408    if let Some(path) = home {
409        validate_path(purpose, path)?;
410        return Ok(path.join(home_child));
411    }
412
413    validate_path(purpose, &fallback_base)?;
414    Ok(fallback_base)
415}
416
417fn required_base(
418    purpose: PathPurpose,
419    value: Option<&Path>,
420    variable: &'static str,
421) -> Result<PathBuf, PathError> {
422    value
423        .map(|path| validate_path(purpose, path).map(|_| path.to_path_buf()))
424        .transpose()?
425        .ok_or_else(|| PathError::missing_base(purpose, variable))
426}
427
428fn validated_optional(
429    purpose: PathPurpose,
430    value: Option<&Path>,
431) -> Result<Option<&Path>, PathError> {
432    if let Some(path) = value {
433        validate_path(purpose, path)?;
434    }
435
436    Ok(value)
437}
438
439fn optional_or_default(
440    purpose: PathPurpose,
441    value: Option<&Path>,
442    default: PathBuf,
443) -> Result<PathBuf, PathError> {
444    match value {
445        Some(path) => {
446            validate_path(purpose, path)?;
447            Ok(path.to_path_buf())
448        }
449        None => {
450            validate_path(purpose, &default)?;
451            Ok(default)
452        }
453    }
454}
455
456fn override_path(
457    purpose: PathPurpose,
458    default: PathBuf,
459    override_value: Option<&Path>,
460) -> Result<PathBuf, PathError> {
461    if let Some(path) = override_value {
462        validate_path(purpose, path)?;
463        Ok(path.to_path_buf())
464    } else {
465        Ok(default)
466    }
467}
468
469fn validate_all(paths: &RuntimePaths) -> Result<(), PathError> {
470    validate_path(PathPurpose::Config, &paths.config_dir)?;
471    validate_path(PathPurpose::Data, &paths.data_dir)?;
472    validate_path(PathPurpose::State, &paths.state_dir)?;
473    validate_path(PathPurpose::Cache, &paths.cache_dir)?;
474    validate_path(PathPurpose::Log, &paths.log_dir)?;
475    validate_path(PathPurpose::Temp, &paths.temp_dir)?;
476    validate_path(PathPurpose::Runtime, &paths.runtime_dir)?;
477    validate_path(PathPurpose::Service, &paths.service_dir)
478}
479
480fn validate_path(purpose: PathPurpose, path: &Path) -> Result<(), PathError> {
481    if !path.is_absolute() {
482        return Err(PathError::relative(purpose, path));
483    }
484
485    if path
486        .components()
487        .any(|component| matches!(component, Component::ParentDir))
488    {
489        return Err(PathError::parent_component(purpose, path));
490    }
491
492    Ok(())
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use crate::env::{PathEnvOverrides, PlatformEnvironment, PlatformKind};
499
500    fn unix_environment() -> PlatformEnvironment {
501        PlatformEnvironment {
502            platform: PlatformKind::Unix,
503            home_dir: Some(PathBuf::from("/home/alice")),
504            xdg_config_home: Some(PathBuf::from("/config")),
505            xdg_data_home: Some(PathBuf::from("/data")),
506            xdg_state_home: Some(PathBuf::from("/state")),
507            xdg_cache_home: Some(PathBuf::from("/cache")),
508            xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
509            app_data: None,
510            local_app_data: None,
511            temp_dir: Some(PathBuf::from("/tmp")),
512        }
513    }
514
515    #[test]
516    fn resolves_unix_platform_paths() {
517        let paths = RuntimePaths::resolve(&unix_environment(), &PathEnvOverrides::default())
518            .expect("paths should resolve");
519
520        assert_eq!(paths.config_dir, PathBuf::from("/config/relay-knowledge"));
521        assert_eq!(paths.data_dir, PathBuf::from("/data/relay-knowledge"));
522        assert_eq!(paths.state_dir, PathBuf::from("/state/relay-knowledge"));
523        assert_eq!(paths.cache_dir, PathBuf::from("/cache/relay-knowledge"));
524        assert_eq!(paths.log_dir, PathBuf::from("/state/relay-knowledge/logs"));
525        assert_eq!(
526            paths.runtime_dir,
527            PathBuf::from("/run/user/1000/relay-knowledge")
528        );
529    }
530
531    #[test]
532    fn runtime_home_override_keeps_state_out_of_repository_paths() {
533        let overrides = PathEnvOverrides {
534            home: Some(PathBuf::from("/srv/relay")),
535            ..PathEnvOverrides::default()
536        };
537
538        let paths =
539            RuntimePaths::resolve(&unix_environment(), &overrides).expect("paths should resolve");
540
541        assert_eq!(paths.config_dir, PathBuf::from("/srv/relay/config"));
542        assert_eq!(paths.data_dir, PathBuf::from("/srv/relay/data"));
543        assert_eq!(paths.cache_dir, PathBuf::from("/srv/relay/cache"));
544        assert_eq!(paths.log_dir, PathBuf::from("/srv/relay/logs"));
545    }
546
547    #[test]
548    fn rejects_relative_overrides() {
549        let overrides = PathEnvOverrides {
550            data_dir: Some(PathBuf::from("relative-data")),
551            ..PathEnvOverrides::default()
552        };
553
554        let error = RuntimePaths::resolve(&unix_environment(), &overrides)
555            .expect_err("relative override should fail");
556
557        assert_eq!(error.purpose, PathPurpose::Data);
558        assert_eq!(
559            error.kind,
560            PathErrorKind::RelativePath {
561                path: PathBuf::from("relative-data")
562            }
563        );
564    }
565
566    #[test]
567    fn rejects_parent_components() {
568        let overrides = PathEnvOverrides {
569            cache_dir: Some(PathBuf::from("/var/cache/../relay")),
570            ..PathEnvOverrides::default()
571        };
572
573        let error = RuntimePaths::resolve(&unix_environment(), &overrides)
574            .expect_err("parent component should fail");
575
576        assert_eq!(error.purpose, PathPurpose::Cache);
577        assert!(matches!(error.kind, PathErrorKind::ParentComponent { .. }));
578    }
579
580    #[test]
581    fn resolves_unix_service_paths_without_home() {
582        let environment = PlatformEnvironment {
583            platform: PlatformKind::Unix,
584            home_dir: None,
585            xdg_config_home: None,
586            xdg_data_home: None,
587            xdg_state_home: None,
588            xdg_cache_home: None,
589            xdg_runtime_dir: None,
590            app_data: None,
591            local_app_data: None,
592            temp_dir: None,
593        };
594
595        let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
596            .expect("service defaults should resolve without HOME");
597
598        assert_eq!(paths.config_dir, PathBuf::from("/etc/relay-knowledge"));
599        assert_eq!(paths.data_dir, PathBuf::from("/var/lib/relay-knowledge"));
600        assert_eq!(paths.cache_dir, PathBuf::from("/var/cache/relay-knowledge"));
601        assert_eq!(
602            paths.runtime_dir,
603            PathBuf::from("/var/lib/relay-knowledge/run")
604        );
605    }
606
607    #[test]
608    fn windows_temp_dir_is_scoped_under_application_directory() {
609        let environment = PlatformEnvironment {
610            platform: PlatformKind::Windows,
611            home_dir: None,
612            xdg_config_home: None,
613            xdg_data_home: None,
614            xdg_state_home: None,
615            xdg_cache_home: None,
616            xdg_runtime_dir: None,
617            app_data: Some(PathBuf::from("/roaming")),
618            local_app_data: Some(PathBuf::from("/local")),
619            temp_dir: Some(PathBuf::from("/shared-temp")),
620        };
621
622        let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
623            .expect("windows paths should resolve");
624
625        assert_eq!(
626            paths.temp_dir,
627            PathBuf::from("/shared-temp/relay-knowledge")
628        );
629    }
630
631    #[test]
632    fn resolves_macos_application_support_paths() {
633        let environment = PlatformEnvironment {
634            platform: PlatformKind::Macos,
635            home_dir: Some(PathBuf::from("/Users/alice")),
636            xdg_config_home: None,
637            xdg_data_home: None,
638            xdg_state_home: None,
639            xdg_cache_home: None,
640            xdg_runtime_dir: None,
641            app_data: None,
642            local_app_data: None,
643            temp_dir: None,
644        };
645
646        let paths =
647            RuntimePaths::resolve(&environment, &PathEnvOverrides::default()).expect("mac paths");
648
649        assert_eq!(
650            paths.config_dir,
651            PathBuf::from("/Users/alice/Library/Application Support/relay-knowledge/config")
652        );
653        assert_eq!(
654            paths.cache_dir,
655            PathBuf::from("/Users/alice/Library/Caches/relay-knowledge")
656        );
657        assert_eq!(paths.temp_dir, PathBuf::from("/tmp/relay-knowledge"));
658    }
659
660    #[test]
661    fn macos_requires_home_directory() {
662        let environment = PlatformEnvironment {
663            platform: PlatformKind::Macos,
664            home_dir: None,
665            xdg_config_home: None,
666            xdg_data_home: None,
667            xdg_state_home: None,
668            xdg_cache_home: None,
669            xdg_runtime_dir: None,
670            app_data: None,
671            local_app_data: None,
672            temp_dir: None,
673        };
674
675        let error = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
676            .expect_err("missing HOME should fail");
677
678        assert_eq!(error.purpose, PathPurpose::Home);
679        assert_eq!(
680            error.to_string(),
681            "cannot resolve home directory because HOME is unavailable"
682        );
683    }
684
685    #[test]
686    fn windows_falls_back_to_home_appdata_paths() {
687        let environment = PlatformEnvironment {
688            platform: PlatformKind::Windows,
689            home_dir: Some(PathBuf::from("/Users/Alice")),
690            xdg_config_home: None,
691            xdg_data_home: None,
692            xdg_state_home: None,
693            xdg_cache_home: None,
694            xdg_runtime_dir: None,
695            app_data: None,
696            local_app_data: None,
697            temp_dir: None,
698        };
699
700        let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
701            .expect("windows fallback should resolve");
702
703        assert_eq!(
704            paths.config_dir,
705            PathBuf::from("/Users/Alice/AppData/Roaming/relay-knowledge")
706        );
707        assert_eq!(
708            paths.data_dir,
709            PathBuf::from("/Users/Alice/AppData/Local/relay-knowledge/data")
710        );
711        assert_eq!(
712            paths.temp_dir,
713            PathBuf::from("/Users/Alice/AppData/Local/relay-knowledge/tmp")
714        );
715    }
716
717    #[test]
718    fn per_directory_overrides_replace_defaults() {
719        let overrides = PathEnvOverrides {
720            config_dir: Some(PathBuf::from("/custom/config")),
721            data_dir: Some(PathBuf::from("/custom/data")),
722            state_dir: Some(PathBuf::from("/custom/state")),
723            cache_dir: Some(PathBuf::from("/custom/cache")),
724            log_dir: Some(PathBuf::from("/custom/log")),
725            temp_dir: Some(PathBuf::from("/custom/tmp")),
726            runtime_dir: Some(PathBuf::from("/custom/run")),
727            service_dir: Some(PathBuf::from("/custom/service")),
728            ..PathEnvOverrides::default()
729        };
730
731        let paths = RuntimePaths::resolve(&unix_environment(), &overrides)
732            .expect("overrides should resolve");
733
734        assert_eq!(paths.config_dir, PathBuf::from("/custom/config"));
735        assert_eq!(paths.data_dir, PathBuf::from("/custom/data"));
736        assert_eq!(paths.state_dir, PathBuf::from("/custom/state"));
737        assert_eq!(paths.cache_dir, PathBuf::from("/custom/cache"));
738        assert_eq!(paths.log_dir, PathBuf::from("/custom/log"));
739        assert_eq!(paths.temp_dir, PathBuf::from("/custom/tmp"));
740        assert_eq!(paths.runtime_dir, PathBuf::from("/custom/run"));
741        assert_eq!(paths.service_dir, PathBuf::from("/custom/service"));
742    }
743}