rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

config.rs (21744B)


      1 use anyhow::{Context, Result, bail};
      2 use radroots_nostr::prelude::RadrootsNostrMetadata;
      3 use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig};
      4 use serde::{Deserialize, Serialize};
      5 use std::path::{Path, PathBuf};
      6 
      7 use crate::features::trade_validation_receipt::TradeValidationReceiptProverPolicy;
      8 use crate::paths::{
      9     RhiRuntimePaths, default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
     10 };
     11 
     12 fn default_replay_window_secs() -> u64 {
     13     24 * 60 * 60
     14 }
     15 
     16 fn default_replay_overlap_secs() -> u64 {
     17     5 * 60
     18 }
     19 
     20 fn default_logging_filter() -> String {
     21     "info".to_owned()
     22 }
     23 
     24 fn default_logging_stdout() -> bool {
     25     true
     26 }
     27 
     28 #[derive(Debug, Clone, Serialize, Deserialize)]
     29 pub struct LoggingConfig {
     30     pub output_dir: PathBuf,
     31     pub filter: String,
     32     pub stdout: bool,
     33 }
     34 
     35 #[derive(Debug, Deserialize, Clone, Default)]
     36 #[serde(default, deny_unknown_fields)]
     37 struct RawLoggingConfig {
     38     pub output_dir: Option<PathBuf>,
     39     pub filter: Option<String>,
     40     pub stdout: Option<bool>,
     41 }
     42 
     43 impl RawLoggingConfig {
     44     fn into_logging_config(self, paths: &RhiRuntimePaths) -> Result<LoggingConfig> {
     45         let filter = self.filter.unwrap_or_else(default_logging_filter);
     46         let filter = filter.trim();
     47         if filter.is_empty() {
     48             bail!("logging.filter must not be empty");
     49         }
     50 
     51         Ok(LoggingConfig {
     52             output_dir: self.output_dir.unwrap_or_else(|| paths.logs_dir.clone()),
     53             filter: filter.to_owned(),
     54             stdout: self.stdout.unwrap_or_else(default_logging_stdout),
     55         })
     56     }
     57 }
     58 
     59 #[derive(Debug, Deserialize, Clone, Default)]
     60 #[serde(default, deny_unknown_fields)]
     61 struct RawRelaysConfig {
     62     pub urls: Vec<String>,
     63 }
     64 
     65 #[derive(Debug, Deserialize, Clone, Default)]
     66 #[serde(default, deny_unknown_fields)]
     67 struct RawNostrConfig {
     68     pub nip89: RawNip89Config,
     69 }
     70 
     71 #[derive(Debug, Deserialize, Clone, Default)]
     72 #[serde(default, deny_unknown_fields)]
     73 struct RawNip89Config {
     74     pub identifier: Option<String>,
     75     pub extra_tags: Vec<Vec<String>>,
     76 }
     77 
     78 #[derive(Debug, Clone)]
     79 struct RawServiceConfig {
     80     pub logging: LoggingConfig,
     81     pub relays: RawRelaysConfig,
     82     pub nostr: RawNostrConfig,
     83 }
     84 
     85 impl RawServiceConfig {
     86     fn into_service_config(self) -> RadrootsNostrServiceConfig {
     87         RadrootsNostrServiceConfig {
     88             logs_dir: self.logging.output_dir.display().to_string(),
     89             relays: self.relays.urls,
     90             nip89_identifier: self.nostr.nip89.identifier,
     91             nip89_extra_tags: self.nostr.nip89.extra_tags,
     92         }
     93     }
     94 }
     95 
     96 #[derive(Debug, Clone, Serialize, Deserialize)]
     97 pub struct Configuration {
     98     #[serde(flatten)]
     99     pub service: RadrootsNostrServiceConfig,
    100     pub logging: LoggingConfig,
    101     #[serde(default)]
    102     pub subscriber: SubscriberConfig,
    103     #[serde(default)]
    104     pub trade_validation_receipt: TradeValidationReceiptProverPolicy,
    105 }
    106 
    107 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
    108 pub struct SubscriberConfig {
    109     #[serde(default)]
    110     pub backoff: BackoffConfig,
    111     #[serde(default)]
    112     pub state: SubscriberStateConfig,
    113 }
    114 
    115 #[derive(Debug, Deserialize, Clone, Default)]
    116 #[serde(default, deny_unknown_fields)]
    117 struct RawSubscriberConfig {
    118     #[serde(default)]
    119     pub backoff: BackoffConfig,
    120     #[serde(default)]
    121     pub state: RawSubscriberStateConfig,
    122 }
    123 
    124 impl RawSubscriberConfig {
    125     fn into_subscriber_config(self, paths: &RhiRuntimePaths) -> SubscriberConfig {
    126         SubscriberConfig {
    127             backoff: self.backoff,
    128             state: self.state.into_subscriber_state_config(paths),
    129         }
    130     }
    131 }
    132 
    133 #[derive(Debug, Clone, Serialize, Deserialize)]
    134 pub struct SubscriberStateConfig {
    135     pub path: PathBuf,
    136     pub replay_window_secs: u64,
    137     pub replay_overlap_secs: u64,
    138 }
    139 
    140 #[derive(Debug, Deserialize, Clone)]
    141 #[serde(deny_unknown_fields)]
    142 struct RawSubscriberStateConfig {
    143     #[serde(default)]
    144     pub path: Option<PathBuf>,
    145     #[serde(default = "default_replay_window_secs")]
    146     pub replay_window_secs: u64,
    147     #[serde(default = "default_replay_overlap_secs")]
    148     pub replay_overlap_secs: u64,
    149 }
    150 
    151 impl Default for RawSubscriberStateConfig {
    152     fn default() -> Self {
    153         Self {
    154             path: None,
    155             replay_window_secs: default_replay_window_secs(),
    156             replay_overlap_secs: default_replay_overlap_secs(),
    157         }
    158     }
    159 }
    160 
    161 impl RawSubscriberStateConfig {
    162     fn into_subscriber_state_config(self, paths: &RhiRuntimePaths) -> SubscriberStateConfig {
    163         SubscriberStateConfig {
    164             path: self
    165                 .path
    166                 .unwrap_or_else(|| paths.subscriber_state_path.clone()),
    167             replay_window_secs: self.replay_window_secs,
    168             replay_overlap_secs: self.replay_overlap_secs,
    169         }
    170     }
    171 }
    172 
    173 impl Default for SubscriberStateConfig {
    174     fn default() -> Self {
    175         Self {
    176             path: default_subscriber_state_path_for_process()
    177                 .expect("resolve canonical rhi subscriber state path"),
    178             replay_window_secs: default_replay_window_secs(),
    179             replay_overlap_secs: default_replay_overlap_secs(),
    180         }
    181     }
    182 }
    183 
    184 #[derive(Debug, Deserialize, Clone)]
    185 #[serde(deny_unknown_fields)]
    186 struct RawSettings {
    187     pub metadata: RadrootsNostrMetadata,
    188     #[serde(default)]
    189     pub logging: RawLoggingConfig,
    190     #[serde(default)]
    191     pub relays: RawRelaysConfig,
    192     #[serde(default)]
    193     pub nostr: RawNostrConfig,
    194     #[serde(default)]
    195     pub subscriber: RawSubscriberConfig,
    196     #[serde(default)]
    197     pub trade_validation_receipt: TradeValidationReceiptProverPolicy,
    198 }
    199 
    200 impl RawSettings {
    201     fn into_settings(self, paths: &RhiRuntimePaths) -> Result<Settings> {
    202         let logging = self.logging.into_logging_config(paths)?;
    203         let service = RawServiceConfig {
    204             logging: logging.clone(),
    205             relays: self.relays,
    206             nostr: self.nostr,
    207         }
    208         .into_service_config();
    209 
    210         Ok(Settings {
    211             metadata: self.metadata,
    212             config: Configuration {
    213                 service,
    214                 logging,
    215                 subscriber: self.subscriber.into_subscriber_config(paths),
    216                 trade_validation_receipt: self.trade_validation_receipt,
    217             },
    218         })
    219     }
    220 }
    221 
    222 #[derive(Debug, Clone, Serialize, Deserialize)]
    223 pub struct Settings {
    224     pub metadata: RadrootsNostrMetadata,
    225     pub config: Configuration,
    226 }
    227 
    228 fn load_settings_from_path_with_resolver(
    229     path: &Path,
    230     resolver: &radroots_runtime_paths::RadrootsPathResolver,
    231     profile: radroots_runtime_paths::RadrootsPathProfile,
    232     repo_local_root: Option<&Path>,
    233 ) -> Result<Settings> {
    234     let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?;
    235     let raw = std::fs::read_to_string(path)
    236         .with_context(|| format!("read configuration from {}", path.display()))?;
    237     let settings: RawSettings =
    238         toml::from_str(&raw).with_context(|| format!("parse configuration {}", path.display()))?;
    239     settings.into_settings(&paths)
    240 }
    241 
    242 pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
    243     let (profile, repo_local_root) = crate::paths::process_path_selection()?;
    244     load_settings_from_path_with_resolver(
    245         path,
    246         &radroots_runtime_paths::RadrootsPathResolver::current(),
    247         profile,
    248         repo_local_root.as_deref(),
    249     )
    250 }
    251 
    252 #[cfg(test)]
    253 mod tests {
    254     use super::load_settings_from_path_with_resolver;
    255     use crate::features::trade_validation_receipt::TradeValidationReceiptProverBackend;
    256     use crate::paths::{
    257         default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
    258         runtime_contract_with_resolver,
    259     };
    260     use radroots_runtime_paths::{
    261         RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
    262         RadrootsPlatform, RadrootsRuntimeNamespace,
    263     };
    264     use radroots_sp1_host_trade::RadrootsSp1TradeProofMode;
    265     use std::path::PathBuf;
    266 
    267     fn linux_resolver() -> RadrootsPathResolver {
    268         RadrootsPathResolver::new(
    269             RadrootsPlatform::Linux,
    270             RadrootsHostEnvironment {
    271                 home_dir: Some(PathBuf::from("/home/treesap")),
    272                 ..RadrootsHostEnvironment::default()
    273             },
    274         )
    275     }
    276 
    277     #[test]
    278     fn worker_namespace_uses_canonical_interactive_roots() {
    279         let namespace = RadrootsRuntimeNamespace::worker("rhi").expect("worker namespace");
    280         let namespaced = linux_resolver()
    281             .resolve(
    282                 RadrootsPathProfile::InteractiveUser,
    283                 &RadrootsPathOverrides::default(),
    284             )
    285             .expect("interactive_user roots")
    286             .namespaced(&namespace);
    287 
    288         assert_eq!(
    289             namespaced.config,
    290             PathBuf::from("/home/treesap/.radroots/config/workers/rhi")
    291         );
    292         assert_eq!(
    293             namespaced.data,
    294             PathBuf::from("/home/treesap/.radroots/data/workers/rhi")
    295         );
    296         assert_eq!(
    297             namespaced.logs,
    298             PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
    299         );
    300         assert_eq!(
    301             namespaced.secrets,
    302             PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi")
    303         );
    304     }
    305 
    306     #[test]
    307     fn runtime_paths_follow_interactive_user_contract() {
    308         let paths = resolve_runtime_paths_with_resolver(
    309             &linux_resolver(),
    310             RadrootsPathProfile::InteractiveUser,
    311             None,
    312         )
    313         .expect("interactive_user paths should resolve");
    314 
    315         assert_eq!(
    316             paths.config_path,
    317             PathBuf::from("/home/treesap/.radroots/config/workers/rhi/config.toml")
    318         );
    319         assert_eq!(
    320             paths.logs_dir,
    321             PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
    322         );
    323         assert_eq!(
    324             paths.identity_path,
    325             PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json")
    326         );
    327         assert_eq!(
    328             paths.subscriber_state_path,
    329             PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json")
    330         );
    331     }
    332 
    333     #[test]
    334     fn runtime_paths_follow_service_host_contract() {
    335         let resolver =
    336             RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
    337         let paths =
    338             resolve_runtime_paths_with_resolver(&resolver, RadrootsPathProfile::ServiceHost, None)
    339                 .expect("service_host paths should resolve");
    340 
    341         assert_eq!(
    342             paths.config_path,
    343             PathBuf::from("/etc/radroots/workers/rhi/config.toml")
    344         );
    345         assert_eq!(
    346             paths.logs_dir,
    347             PathBuf::from("/var/log/radroots/workers/rhi")
    348         );
    349         assert_eq!(
    350             paths.identity_path,
    351             PathBuf::from("/etc/radroots/secrets/workers/rhi/identity.secret.json")
    352         );
    353         assert_eq!(
    354             paths.subscriber_state_path,
    355             PathBuf::from("/var/lib/radroots/workers/rhi/trade-listing/state.json")
    356         );
    357     }
    358 
    359     #[test]
    360     fn runtime_paths_follow_repo_local_contract() {
    361         let repo_local_root = PathBuf::from("/repo/.local/radroots/dev/rhi");
    362         let paths = resolve_runtime_paths_with_resolver(
    363             &linux_resolver(),
    364             RadrootsPathProfile::RepoLocal,
    365             Some(repo_local_root.as_path()),
    366         )
    367         .expect("repo_local paths should resolve");
    368 
    369         assert_eq!(
    370             paths.config_path,
    371             repo_local_root.join("config/workers/rhi/config.toml")
    372         );
    373         assert_eq!(paths.logs_dir, repo_local_root.join("logs/workers/rhi"));
    374         assert_eq!(
    375             paths.identity_path,
    376             repo_local_root.join("secrets/workers/rhi/identity.secret.json")
    377         );
    378         assert_eq!(
    379             paths.subscriber_state_path,
    380             repo_local_root.join("data/workers/rhi/trade-listing/state.json")
    381         );
    382     }
    383 
    384     #[test]
    385     fn load_settings_materializes_profile_defaults_when_paths_are_omitted() {
    386         let temp = tempfile::tempdir().expect("tempdir");
    387         let config_path = temp.path().join("config.toml");
    388         std::fs::write(
    389             &config_path,
    390             r#"
    391 [metadata]
    392 name = "rhi-test"
    393 
    394 [relays]
    395 urls = ["wss://relay.example.com"]
    396 
    397 [nostr.nip89]
    398 identifier = "rhi"
    399 
    400 [subscriber.state]
    401 replay_window_secs = 123
    402 replay_overlap_secs = 45
    403 "#,
    404         )
    405         .expect("write config");
    406 
    407         let settings = load_settings_from_path_with_resolver(
    408             &config_path,
    409             &linux_resolver(),
    410             RadrootsPathProfile::InteractiveUser,
    411             None,
    412         )
    413         .expect("load settings");
    414 
    415         assert_eq!(
    416             settings.config.service.logs_dir,
    417             "/home/treesap/.radroots/logs/workers/rhi"
    418         );
    419         assert_eq!(
    420             settings.config.logging.output_dir,
    421             PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
    422         );
    423         assert_eq!(settings.config.logging.filter, "info");
    424         assert!(settings.config.logging.stdout);
    425         assert_eq!(
    426             settings.config.service.relays,
    427             vec!["wss://relay.example.com"]
    428         );
    429         assert_eq!(
    430             settings.config.service.nip89_identifier.as_deref(),
    431             Some("rhi")
    432         );
    433         assert_eq!(
    434             settings.config.subscriber.state.path,
    435             PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json")
    436         );
    437         assert_eq!(settings.config.subscriber.state.replay_window_secs, 123);
    438         assert_eq!(settings.config.subscriber.state.replay_overlap_secs, 45);
    439         assert_eq!(
    440             settings.config.trade_validation_receipt.backend,
    441             TradeValidationReceiptProverBackend::Disabled
    442         );
    443         assert_eq!(
    444             settings.config.trade_validation_receipt.proof_mode,
    445             RadrootsSp1TradeProofMode::None
    446         );
    447     }
    448 
    449     #[test]
    450     fn load_settings_parses_trade_validation_receipt_policy() {
    451         let temp = tempfile::tempdir().expect("tempdir");
    452         let config_path = temp.path().join("config.toml");
    453         std::fs::write(
    454             &config_path,
    455             r#"
    456 [metadata]
    457 name = "rhi-test"
    458 
    459 [logging]
    460 output_dir = "logs/rhi"
    461 filter = "warn"
    462 stdout = false
    463 
    464 [relays]
    465 urls = ["wss://relay.example.com"]
    466 
    467 [nostr.nip89]
    468 identifier = "rhi"
    469 extra_tags = [["t", "radroots"]]
    470 
    471 [subscriber.backoff]
    472 base_ms = 10
    473 max_ms = 100
    474 factor = 3
    475 jitter_ms = 5
    476 
    477 [subscriber.state]
    478 path = "state/trade-listing.json"
    479 
    480 [trade_validation_receipt]
    481 backend = "deterministic_none"
    482 proof_mode = "none"
    483 "#,
    484         )
    485         .expect("write config");
    486 
    487         let settings = load_settings_from_path_with_resolver(
    488             &config_path,
    489             &linux_resolver(),
    490             RadrootsPathProfile::InteractiveUser,
    491             None,
    492         )
    493         .expect("load settings");
    494 
    495         assert_eq!(settings.config.service.logs_dir, "logs/rhi");
    496         assert_eq!(
    497             settings.config.logging.output_dir,
    498             PathBuf::from("logs/rhi")
    499         );
    500         assert_eq!(settings.config.logging.filter, "warn");
    501         assert!(!settings.config.logging.stdout);
    502         assert_eq!(
    503             settings.config.service.relays,
    504             vec!["wss://relay.example.com"]
    505         );
    506         assert_eq!(
    507             settings.config.service.nip89_identifier.as_deref(),
    508             Some("rhi")
    509         );
    510         assert_eq!(
    511             settings.config.service.nip89_extra_tags,
    512             vec![vec!["t".to_owned(), "radroots".to_owned()]]
    513         );
    514         assert_eq!(settings.config.subscriber.backoff.base_ms, 10);
    515         assert_eq!(settings.config.subscriber.backoff.max_ms, 100);
    516         assert_eq!(settings.config.subscriber.backoff.factor, 3);
    517         assert_eq!(settings.config.subscriber.backoff.jitter_ms, 5);
    518         assert_eq!(
    519             settings.config.subscriber.state.path,
    520             PathBuf::from("state/trade-listing.json")
    521         );
    522         assert_eq!(
    523             settings.config.trade_validation_receipt.backend,
    524             TradeValidationReceiptProverBackend::DeterministicNone
    525         );
    526         assert_eq!(
    527             settings.config.trade_validation_receipt.proof_mode,
    528             RadrootsSp1TradeProofMode::None
    529         );
    530     }
    531 
    532     #[test]
    533     fn old_config_roots_are_rejected() {
    534         let temp = tempfile::tempdir().expect("tempdir");
    535         for (name, body, needle) in [
    536             (
    537                 "config-root",
    538                 r#"
    539 [metadata]
    540 name = "rhi-test"
    541 
    542 [config]
    543 relays = ["wss://relay.example.com"]
    544 "#,
    545                 "unknown field `config`",
    546             ),
    547             (
    548                 "config-subscriber-backoff",
    549                 r#"
    550 [metadata]
    551 name = "rhi-test"
    552 
    553 [config.subscriber.backoff]
    554 base_ms = 10
    555 "#,
    556                 "unknown field `config`",
    557             ),
    558             (
    559                 "config-subscriber-state",
    560                 r#"
    561 [metadata]
    562 name = "rhi-test"
    563 
    564 [config.subscriber.state]
    565 replay_window_secs = 10
    566 "#,
    567                 "unknown field `config`",
    568             ),
    569             (
    570                 "config-trade-validation-receipt",
    571                 r#"
    572 [metadata]
    573 name = "rhi-test"
    574 
    575 [config.trade_validation_receipt]
    576 backend = "deterministic_none"
    577 proof_mode = "none"
    578 "#,
    579                 "unknown field `config`",
    580             ),
    581         ] {
    582             let config_path = temp.path().join(format!("{name}.toml"));
    583             std::fs::write(&config_path, body).expect("write config");
    584 
    585             let error = load_settings_from_path_with_resolver(
    586                 &config_path,
    587                 &linux_resolver(),
    588                 RadrootsPathProfile::InteractiveUser,
    589                 None,
    590             )
    591             .expect_err("old config root must fail");
    592             let message = format!("{error:#}");
    593             assert!(message.contains(needle), "{message}");
    594         }
    595     }
    596 
    597     #[test]
    598     fn default_subscriber_state_path_is_canonical_for_current_process() {
    599         let path =
    600             default_subscriber_state_path_for_process().expect("resolve current process defaults");
    601         assert!(path.ends_with("trade-listing/state.json"));
    602     }
    603 
    604     #[test]
    605     fn runtime_contract_output_matches_interactive_user_contract() {
    606         let contract = runtime_contract_with_resolver(
    607             &linux_resolver(),
    608             RadrootsPathProfile::InteractiveUser,
    609             None,
    610         )
    611         .expect("interactive-user contract");
    612 
    613         assert_eq!(contract.active_profile, "interactive_user");
    614         assert_eq!(contract.path_overrides.profile_source, "caller");
    615         assert_eq!(contract.path_overrides.root_source, "host_defaults");
    616         assert_eq!(contract.path_overrides.repo_local_root, None);
    617         assert_eq!(contract.path_overrides.repo_local_root_source, None);
    618         assert_eq!(
    619             contract.path_overrides.subordinate_path_override_source,
    620             "config_artifact"
    621         );
    622         assert_eq!(
    623             contract.path_overrides.subordinate_path_override_keys,
    624             vec![
    625                 "logging.output_dir".to_owned(),
    626                 "subscriber.state.path".to_owned(),
    627             ]
    628         );
    629         assert_eq!(
    630             contract.allowed_profiles,
    631             vec![
    632                 "interactive_user".to_owned(),
    633                 "service_host".to_owned(),
    634                 "repo_local".to_owned(),
    635             ]
    636         );
    637         assert_eq!(contract.default_shared_secret_backend, "encrypted_file");
    638         assert_eq!(
    639             contract.allowed_shared_secret_backends,
    640             vec!["encrypted_file".to_owned()]
    641         );
    642         assert_eq!(
    643             contract.migration.posture,
    644             "explicit_operator_import_required"
    645         );
    646         assert_eq!(contract.migration.state, "ready");
    647         assert_eq!(contract.migration.silent_startup_relocation, false);
    648         assert_eq!(
    649             contract.migration.compatibility_window,
    650             "detect_and_report_only"
    651         );
    652         assert!(contract.migration.detected_legacy_paths.is_empty());
    653         assert_eq!(
    654             contract.canonical_config_path,
    655             PathBuf::from("/home/treesap/.radroots/config/workers/rhi/config.toml")
    656         );
    657         assert_eq!(
    658             contract.canonical_logs_dir,
    659             PathBuf::from("/home/treesap/.radroots/logs/workers/rhi")
    660         );
    661         assert_eq!(
    662             contract.canonical_identity_path,
    663             PathBuf::from("/home/treesap/.radroots/secrets/workers/rhi/identity.secret.json")
    664         );
    665         assert_eq!(
    666             contract.canonical_subscriber_state_path,
    667             PathBuf::from("/home/treesap/.radroots/data/workers/rhi/trade-listing/state.json")
    668         );
    669     }
    670 
    671     #[test]
    672     fn runtime_contract_output_matches_service_host_contract() {
    673         let resolver =
    674             RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
    675         let contract =
    676             runtime_contract_with_resolver(&resolver, RadrootsPathProfile::ServiceHost, None)
    677                 .expect("service-host contract");
    678 
    679         assert_eq!(contract.active_profile, "service_host");
    680         assert_eq!(
    681             contract.canonical_config_path,
    682             PathBuf::from("/etc/radroots/workers/rhi/config.toml")
    683         );
    684         assert_eq!(
    685             contract.canonical_logs_dir,
    686             PathBuf::from("/var/log/radroots/workers/rhi")
    687         );
    688         assert_eq!(
    689             contract.canonical_identity_path,
    690             PathBuf::from("/etc/radroots/secrets/workers/rhi/identity.secret.json")
    691         );
    692         assert_eq!(
    693             contract.canonical_subscriber_state_path,
    694             PathBuf::from("/var/lib/radroots/workers/rhi/trade-listing/state.json")
    695         );
    696     }
    697 }