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