1use 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#[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 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 pub fn agent_audit_log_file(&self) -> PathBuf {
95 self.log_dir.join("agent-audit.jsonl")
96 }
97
98 pub fn model_profiles_file(&self) -> PathBuf {
100 self.config_dir.join(MODEL_PROFILES_FILE_NAME)
101 }
102
103 pub fn model_fallback_file(&self) -> PathBuf {
105 self.config_dir.join(MODEL_FALLBACK_FILE_NAME)
106 }
107
108 pub fn model_catalog_cache_file(&self) -> PathBuf {
110 self.cache_dir.join(MODEL_CATALOG_CACHE_FILE_NAME)
111 }
112}
113
114pub 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#[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#[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#[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}