lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

context.rs (27803B)


      1 //! Immutable resolved bootstrap context for one service instance.
      2 
      3 use core::fmt;
      4 use std::path::{Path, PathBuf};
      5 
      6 use serde::{Serialize, Serializer, ser::SerializeStruct};
      7 use thiserror::Error;
      8 
      9 use crate::{
     10     InstanceId, RadrootsPathProfile, RadrootsPathResolver, RadrootsServiceInstancePaths,
     11     RuntimeStateDirectoryPlan, ServiceId, StateDirectoryProvisionError,
     12 };
     13 
     14 /// Closed provenance vocabulary for effective runtime configuration.
     15 ///
     16 /// Arbitrary strings, secrets, and high-cardinality labels cannot be converted
     17 /// into this vocabulary:
     18 ///
     19 /// ```compile_fail
     20 /// use radroots_runtime_paths::RuntimeContextSource;
     21 ///
     22 /// let _ = RuntimeContextSource::from("secret:caller-controlled-value");
     23 /// ```
     24 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
     25 #[serde(rename_all = "snake_case")]
     26 pub enum RuntimeContextSource {
     27     BootstrapCli,
     28     Toml,
     29     SafeDefault,
     30     DerivedPath,
     31 }
     32 
     33 /// Sealed validated bootstrap input for one runtime context.
     34 #[derive(Clone, PartialEq, Eq)]
     35 pub struct RuntimeContextBootstrap {
     36     profile: RadrootsPathProfile,
     37     repo_local_root: Option<PathBuf>,
     38     profile_source: RuntimeContextSource,
     39     instance_source: RuntimeContextSource,
     40 }
     41 
     42 impl fmt::Debug for RuntimeContextBootstrap {
     43     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     44         formatter
     45             .debug_struct("RuntimeContextBootstrap")
     46             .field("profile", &self.profile)
     47             .field(
     48                 "repo_local_root",
     49                 &self.repo_local_root.as_ref().map(|_| "[redacted]"),
     50             )
     51             .field("profile_source", &self.profile_source)
     52             .field("instance_source", &self.instance_source)
     53             .finish()
     54     }
     55 }
     56 
     57 impl RuntimeContextBootstrap {
     58     pub fn new(
     59         profile: RadrootsPathProfile,
     60         repo_local_root: Option<PathBuf>,
     61         profile_source: RuntimeContextSource,
     62         instance_source: RuntimeContextSource,
     63     ) -> Result<Self, RuntimeContextError> {
     64         if matches!(profile, RadrootsPathProfile::MobileNative)
     65             || (matches!(profile, RadrootsPathProfile::RepoLocal)
     66                 && !matches!(profile_source, RuntimeContextSource::BootstrapCli))
     67             || !matches!(
     68                 profile_source,
     69                 RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault
     70             )
     71             || !matches!(
     72                 instance_source,
     73                 RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault
     74             )
     75             || (matches!(profile, RadrootsPathProfile::RepoLocal) != repo_local_root.is_some())
     76         {
     77             return Err(RuntimeContextError::InvalidBootstrapBinding);
     78         }
     79         Ok(Self {
     80             profile,
     81             repo_local_root,
     82             profile_source,
     83             instance_source,
     84         })
     85     }
     86 
     87     #[must_use]
     88     pub fn profile(&self) -> RadrootsPathProfile {
     89         self.profile
     90     }
     91 }
     92 
     93 /// Closed provenance bound to every runtime-context field class.
     94 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
     95 pub struct RuntimeContextSources {
     96     service: RuntimeContextSource,
     97     instance: RuntimeContextSource,
     98     profile: RuntimeContextSource,
     99     repo_local_root: Option<RuntimeContextSource>,
    100     paths: RuntimeContextSource,
    101 }
    102 
    103 impl RuntimeContextSources {
    104     #[must_use]
    105     pub fn service(&self) -> RuntimeContextSource {
    106         self.service
    107     }
    108 
    109     #[must_use]
    110     pub fn instance(&self) -> RuntimeContextSource {
    111         self.instance
    112     }
    113 
    114     #[must_use]
    115     pub fn profile(&self) -> RuntimeContextSource {
    116         self.profile
    117     }
    118 
    119     #[must_use]
    120     pub fn repo_local_root(&self) -> Option<RuntimeContextSource> {
    121         self.repo_local_root
    122     }
    123 
    124     #[must_use]
    125     pub fn paths(&self) -> RuntimeContextSource {
    126         self.paths
    127     }
    128 }
    129 
    130 /// Immutable resolved bootstrap identity and canonical paths.
    131 ///
    132 /// External callers cannot forge or mutate a context:
    133 ///
    134 /// ```compile_fail
    135 /// use radroots_runtime_paths::RuntimeContext;
    136 ///
    137 /// let _ = RuntimeContext {
    138 ///     service: todo!(),
    139 ///     instance: todo!(),
    140 ///     profile: todo!(),
    141 ///     repo_local_root: todo!(),
    142 ///     paths: todo!(),
    143 ///     sources: todo!(),
    144 /// };
    145 /// ```
    146 #[derive(Clone, PartialEq, Eq)]
    147 pub struct RuntimeContext {
    148     service: ServiceId,
    149     instance: InstanceId,
    150     profile: RadrootsPathProfile,
    151     repo_local_root: Option<PathBuf>,
    152     paths: RadrootsServiceInstancePaths,
    153     sources: RuntimeContextSources,
    154 }
    155 
    156 impl RuntimeContext {
    157     pub fn resolve(
    158         resolver: &RadrootsPathResolver,
    159         bootstrap: RuntimeContextBootstrap,
    160         service: ServiceId,
    161         instance: InstanceId,
    162     ) -> Result<Self, RuntimeContextError> {
    163         let RuntimeContextBootstrap {
    164             profile,
    165             repo_local_root,
    166             profile_source,
    167             instance_source,
    168         } = bootstrap;
    169         let roots = resolver
    170             .resolve(profile, repo_local_root.as_deref())
    171             .map_err(|_| RuntimeContextError::PathSelection)?;
    172         let paths = RadrootsServiceInstancePaths::from_resolved_roots(&roots, &service, &instance);
    173         let sources = RuntimeContextSources {
    174             service: RuntimeContextSource::SafeDefault,
    175             instance: instance_source,
    176             profile: profile_source,
    177             repo_local_root: repo_local_root
    178                 .as_ref()
    179                 .map(|_| RuntimeContextSource::BootstrapCli),
    180             paths: RuntimeContextSource::DerivedPath,
    181         };
    182 
    183         Ok(Self {
    184             service,
    185             instance,
    186             profile,
    187             repo_local_root,
    188             paths,
    189             sources,
    190         })
    191     }
    192 
    193     #[must_use]
    194     pub fn service(&self) -> &ServiceId {
    195         &self.service
    196     }
    197 
    198     #[must_use]
    199     pub fn instance(&self) -> &InstanceId {
    200         &self.instance
    201     }
    202 
    203     #[must_use]
    204     pub fn profile(&self) -> RadrootsPathProfile {
    205         self.profile
    206     }
    207 
    208     /// Returns the validated explicit repo-local base when that profile is active.
    209     #[must_use]
    210     pub fn repo_local_root(&self) -> Option<&Path> {
    211         self.repo_local_root.as_deref()
    212     }
    213 
    214     #[must_use]
    215     pub fn paths(&self) -> &RadrootsServiceInstancePaths {
    216         &self.paths
    217     }
    218 
    219     #[must_use]
    220     pub fn sources(&self) -> &RuntimeContextSources {
    221         &self.sources
    222     }
    223 
    224     /// Returns a sealed, filesystem-I/O-free plan for this instance's state directory.
    225     ///
    226     /// Calling this method does not inspect or mutate the filesystem. Callers must
    227     /// explicitly invoke [`RuntimeStateDirectoryPlan::provision`] to validate or
    228     /// create the governed state-directory suffix.
    229     pub fn state_directory_plan(
    230         &self,
    231     ) -> Result<RuntimeStateDirectoryPlan, StateDirectoryProvisionError> {
    232         RuntimeStateDirectoryPlan::from_context(self)
    233     }
    234 }
    235 
    236 impl fmt::Debug for RuntimeContext {
    237     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    238         formatter
    239             .debug_struct("RuntimeContext")
    240             .field("service", &self.service)
    241             .field("instance", &self.instance)
    242             .field("profile", &self.profile)
    243             .field(
    244                 "repo_local_root",
    245                 &self.repo_local_root.as_ref().map(|_| "[redacted]"),
    246             )
    247             .field("paths", &"[redacted]")
    248             .field("sources", &self.sources)
    249             .finish()
    250     }
    251 }
    252 
    253 impl Serialize for RuntimeContext {
    254     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    255     where
    256         S: Serializer,
    257     {
    258         let mut state = serializer.serialize_struct("RuntimeContext", 5)?;
    259         state.serialize_field("service", &self.service)?;
    260         state.serialize_field("instance", &self.instance)?;
    261         state.serialize_field("profile", &self.profile.to_string())?;
    262         state.serialize_field("paths", "[redacted]")?;
    263         state.serialize_field("sources", &self.sources)?;
    264         state.end()
    265     }
    266 }
    267 
    268 /// Safe construction failures for [`RuntimeContext`].
    269 #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
    270 pub enum RuntimeContextError {
    271     #[error("runtime context bootstrap provenance does not match its selectors")]
    272     InvalidBootstrapBinding,
    273     #[error("runtime context path selection failed")]
    274     PathSelection,
    275 }
    276 
    277 #[cfg(test)]
    278 mod tests {
    279     use std::{path::PathBuf, time::SystemTime};
    280 
    281     use serde_json::json;
    282 
    283     use super::{
    284         RuntimeContext, RuntimeContextBootstrap, RuntimeContextError, RuntimeContextSource,
    285     };
    286     use crate::{
    287         InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
    288         RadrootsPlatform, ServiceId,
    289     };
    290 
    291     fn repo_local_context(base: PathBuf) -> RuntimeContext {
    292         repo_local_context_for(base, "myc", "primary")
    293     }
    294 
    295     fn repo_local_context_for(base: PathBuf, service: &str, instance: &str) -> RuntimeContext {
    296         RuntimeContext::resolve(
    297             &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
    298             RuntimeContextBootstrap::new(
    299                 RadrootsPathProfile::RepoLocal,
    300                 Some(base),
    301                 RuntimeContextSource::BootstrapCli,
    302                 RuntimeContextSource::BootstrapCli,
    303             )
    304             .expect("bootstrap"),
    305             ServiceId::new(service).expect("service"),
    306             InstanceId::new(instance).expect("instance"),
    307         )
    308         .expect("runtime context")
    309     }
    310 
    311     #[test]
    312     fn one_repo_local_base_derives_exact_noncolliding_service_instances() {
    313         let base = PathBuf::from("/repo/.local/radroots");
    314         let mut configs = Vec::new();
    315 
    316         for (service, instance) in [("myc", "primary"), ("myc", "secondary"), ("rhi", "default")] {
    317             let context = repo_local_context_for(base.clone(), service, instance);
    318             assert_eq!(context.repo_local_root(), Some(base.as_path()));
    319             let suffix = PathBuf::from("services").join(service).join(instance);
    320             assert_eq!(context.paths().config(), base.join("config").join(&suffix));
    321             assert_eq!(context.paths().state(), base.join("data").join(&suffix));
    322             assert_eq!(context.paths().cache(), base.join("cache").join(&suffix));
    323             assert_eq!(context.paths().logs(), base.join("logs").join(&suffix));
    324             assert_eq!(context.paths().run(), base.join("run").join(&suffix));
    325             assert_eq!(
    326                 context.paths().secrets(),
    327                 base.join("secrets").join(&suffix)
    328             );
    329             configs.push(context.paths().config().to_path_buf());
    330         }
    331 
    332         configs.sort();
    333         configs.dedup();
    334         assert_eq!(configs.len(), 3);
    335     }
    336 
    337     #[test]
    338     fn retained_macos_and_windows_profiles_derive_exact_context_paths() {
    339         let macos = RuntimeContext::resolve(
    340             &RadrootsPathResolver::new(
    341                 RadrootsPlatform::Macos,
    342                 RadrootsHostEnvironment {
    343                     home_dir: Some(PathBuf::from("/Users/treesap")),
    344                     ..RadrootsHostEnvironment::default()
    345                 },
    346             ),
    347             RuntimeContextBootstrap::new(
    348                 RadrootsPathProfile::InteractiveUser,
    349                 None,
    350                 RuntimeContextSource::SafeDefault,
    351                 RuntimeContextSource::BootstrapCli,
    352             )
    353             .expect("macOS bootstrap"),
    354             ServiceId::new("myc").expect("service"),
    355             InstanceId::new("primary").expect("instance"),
    356         )
    357         .expect("macOS context");
    358         assert_eq!(
    359             macos.paths().config(),
    360             PathBuf::from(
    361                 "/Users/treesap/Library/Application Support/Radroots/config/services/myc/primary"
    362             )
    363         );
    364         assert_eq!(
    365             macos.paths().state(),
    366             PathBuf::from(
    367                 "/Users/treesap/Library/Application Support/Radroots/data/services/myc/primary"
    368             )
    369         );
    370         assert_eq!(
    371             macos.paths().cache(),
    372             PathBuf::from("/Users/treesap/Library/Caches/Radroots/services/myc/primary")
    373         );
    374         assert_eq!(
    375             macos.paths().logs(),
    376             PathBuf::from("/Users/treesap/Library/Logs/Radroots/services/myc/primary")
    377         );
    378         assert_eq!(
    379             macos.paths().run(),
    380             PathBuf::from(
    381                 "/Users/treesap/Library/Application Support/Radroots/run/services/myc/primary"
    382             )
    383         );
    384         assert_eq!(
    385             macos.paths().secrets(),
    386             PathBuf::from(
    387                 "/Users/treesap/Library/Application Support/Radroots/secrets/services/myc/primary"
    388             )
    389         );
    390 
    391         let appdata = PathBuf::from(r"C:\Users\treesap\AppData\Roaming");
    392         let localappdata = PathBuf::from(r"C:\Users\treesap\AppData\Local");
    393         let windows = RuntimeContext::resolve(
    394             &RadrootsPathResolver::new(
    395                 RadrootsPlatform::Windows,
    396                 RadrootsHostEnvironment {
    397                     appdata_dir: Some(appdata.clone()),
    398                     localappdata_dir: Some(localappdata.clone()),
    399                     ..RadrootsHostEnvironment::default()
    400                 },
    401             ),
    402             RuntimeContextBootstrap::new(
    403                 RadrootsPathProfile::InteractiveUser,
    404                 None,
    405                 RuntimeContextSource::SafeDefault,
    406                 RuntimeContextSource::BootstrapCli,
    407             )
    408             .expect("Windows bootstrap"),
    409             ServiceId::new("rhi").expect("service"),
    410             InstanceId::new("default").expect("instance"),
    411         )
    412         .expect("Windows context");
    413         let suffix = PathBuf::from("services/rhi/default");
    414         assert_eq!(
    415             windows.paths().config(),
    416             appdata.join("Radroots/config").join(&suffix)
    417         );
    418         assert_eq!(
    419             windows.paths().state(),
    420             localappdata.join("Radroots/data").join(&suffix)
    421         );
    422         assert_eq!(
    423             windows.paths().cache(),
    424             localappdata.join("Radroots/cache").join(&suffix)
    425         );
    426         assert_eq!(
    427             windows.paths().logs(),
    428             localappdata.join("Radroots/logs").join(&suffix)
    429         );
    430         assert_eq!(
    431             windows.paths().run(),
    432             localappdata.join("Radroots/run").join(&suffix)
    433         );
    434         assert_eq!(
    435             windows.paths().secrets(),
    436             appdata.join("Radroots/secrets").join(&suffix)
    437         );
    438 
    439         assert_eq!(
    440             RuntimeContext::resolve(
    441                 &RadrootsPathResolver::new(
    442                     RadrootsPlatform::Windows,
    443                     RadrootsHostEnvironment::default(),
    444                 ),
    445                 RuntimeContextBootstrap::new(
    446                     RadrootsPathProfile::InteractiveUser,
    447                     None,
    448                     RuntimeContextSource::SafeDefault,
    449                     RuntimeContextSource::BootstrapCli,
    450                 )
    451                 .expect("missing-directory bootstrap"),
    452                 ServiceId::new("myc").expect("service"),
    453                 InstanceId::new("primary").expect("instance"),
    454             ),
    455             Err(RuntimeContextError::PathSelection)
    456         );
    457     }
    458 
    459     #[test]
    460     fn retained_linux_xdg_vectors_are_exact_and_ignore_invalid_optional_values() {
    461         fn resolve(
    462             environment: RadrootsHostEnvironment,
    463         ) -> Result<RuntimeContext, RuntimeContextError> {
    464             RuntimeContext::resolve(
    465                 &RadrootsPathResolver::new(RadrootsPlatform::Linux, environment),
    466                 RuntimeContextBootstrap::new(
    467                     RadrootsPathProfile::InteractiveUser,
    468                     None,
    469                     RuntimeContextSource::SafeDefault,
    470                     RuntimeContextSource::BootstrapCli,
    471                 )
    472                 .expect("Linux bootstrap"),
    473                 ServiceId::new("myc").expect("service"),
    474                 InstanceId::new("primary").expect("instance"),
    475             )
    476         }
    477 
    478         let configured = resolve(RadrootsHostEnvironment {
    479             home_dir: Some(PathBuf::from("/home/treesap")),
    480             xdg_config_home: Some(PathBuf::from("/xdg/config")),
    481             xdg_data_home: Some(PathBuf::from("/xdg/data")),
    482             xdg_state_home: Some(PathBuf::from("/xdg/state")),
    483             xdg_cache_home: Some(PathBuf::from("/xdg/cache")),
    484             xdg_runtime_dir: Some(PathBuf::from("/xdg/run")),
    485             ..RadrootsHostEnvironment::default()
    486         })
    487         .expect("configured XDG context");
    488         assert_eq!(
    489             configured.paths().config(),
    490             PathBuf::from("/xdg/config/radroots/services/myc/primary")
    491         );
    492         assert_eq!(
    493             configured.paths().state(),
    494             PathBuf::from("/xdg/data/radroots/services/myc/primary")
    495         );
    496         assert_eq!(
    497             configured.paths().cache(),
    498             PathBuf::from("/xdg/cache/radroots/services/myc/primary")
    499         );
    500         assert_eq!(
    501             configured.paths().logs(),
    502             PathBuf::from("/xdg/state/radroots/logs/services/myc/primary")
    503         );
    504         assert_eq!(
    505             configured.paths().run(),
    506             PathBuf::from("/xdg/run/radroots/services/myc/primary")
    507         );
    508         assert_eq!(
    509             configured.paths().secrets(),
    510             PathBuf::from("/xdg/config/radroots/secrets/services/myc/primary")
    511         );
    512 
    513         for invalid in ["", "relative"] {
    514             let config = resolve(RadrootsHostEnvironment {
    515                 home_dir: Some(PathBuf::from("/home/treesap")),
    516                 xdg_config_home: Some(PathBuf::from(invalid)),
    517                 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
    518                 ..RadrootsHostEnvironment::default()
    519             })
    520             .expect("invalid config override is ignored");
    521             assert_eq!(
    522                 config.paths().config(),
    523                 PathBuf::from("/home/treesap/.config/radroots/services/myc/primary")
    524             );
    525 
    526             let data = resolve(RadrootsHostEnvironment {
    527                 home_dir: Some(PathBuf::from("/home/treesap")),
    528                 xdg_data_home: Some(PathBuf::from(invalid)),
    529                 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
    530                 ..RadrootsHostEnvironment::default()
    531             })
    532             .expect("invalid data override is ignored");
    533             assert_eq!(
    534                 data.paths().state(),
    535                 PathBuf::from("/home/treesap/.local/share/radroots/services/myc/primary")
    536             );
    537 
    538             let state = resolve(RadrootsHostEnvironment {
    539                 home_dir: Some(PathBuf::from("/home/treesap")),
    540                 xdg_state_home: Some(PathBuf::from(invalid)),
    541                 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
    542                 ..RadrootsHostEnvironment::default()
    543             })
    544             .expect("invalid state override is ignored");
    545             assert_eq!(
    546                 state.paths().logs(),
    547                 PathBuf::from("/home/treesap/.local/state/radroots/logs/services/myc/primary")
    548             );
    549 
    550             let cache = resolve(RadrootsHostEnvironment {
    551                 home_dir: Some(PathBuf::from("/home/treesap")),
    552                 xdg_cache_home: Some(PathBuf::from(invalid)),
    553                 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
    554                 ..RadrootsHostEnvironment::default()
    555             })
    556             .expect("invalid cache override is ignored");
    557             assert_eq!(
    558                 cache.paths().cache(),
    559                 PathBuf::from("/home/treesap/.cache/radroots/services/myc/primary")
    560             );
    561 
    562             assert_eq!(
    563                 resolve(RadrootsHostEnvironment {
    564                     home_dir: Some(PathBuf::from("/home/treesap")),
    565                     xdg_runtime_dir: Some(PathBuf::from(invalid)),
    566                     ..RadrootsHostEnvironment::default()
    567                 }),
    568                 Err(RuntimeContextError::PathSelection)
    569             );
    570         }
    571     }
    572 
    573     #[test]
    574     fn context_is_equal_immutable_and_preserves_exact_typed_sources() {
    575         let first = repo_local_context(PathBuf::from("/repo/.local/radroots"));
    576         let second = repo_local_context(PathBuf::from("/repo/.local/radroots"));
    577         assert_eq!(first, second);
    578         assert_eq!(first.service().as_str(), "myc");
    579         assert_eq!(first.instance().as_str(), "primary");
    580         assert_eq!(first.profile(), RadrootsPathProfile::RepoLocal);
    581         assert_eq!(first.sources().service(), RuntimeContextSource::SafeDefault);
    582         assert_eq!(
    583             first.sources().instance(),
    584             RuntimeContextSource::BootstrapCli
    585         );
    586         assert_eq!(
    587             first.sources().profile(),
    588             RuntimeContextSource::BootstrapCli
    589         );
    590         assert_eq!(
    591             first.sources().repo_local_root(),
    592             Some(RuntimeContextSource::BootstrapCli)
    593         );
    594         assert_eq!(first.sources().paths(), RuntimeContextSource::DerivedPath);
    595         assert_eq!(
    596             first.paths().config(),
    597             PathBuf::from("/repo/.local/radroots/config/services/myc/primary")
    598         );
    599 
    600         let defaulted = RuntimeContext::resolve(
    601             &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
    602             RuntimeContextBootstrap::new(
    603                 RadrootsPathProfile::ServiceHost,
    604                 None,
    605                 RuntimeContextSource::SafeDefault,
    606                 RuntimeContextSource::SafeDefault,
    607             )
    608             .expect("default bootstrap"),
    609             ServiceId::new("rhi").expect("service"),
    610             InstanceId::new("default").expect("instance"),
    611         )
    612         .expect("default runtime context");
    613         assert_eq!(
    614             defaulted.sources().profile(),
    615             RuntimeContextSource::SafeDefault
    616         );
    617         assert_eq!(
    618             defaulted.sources().instance(),
    619             RuntimeContextSource::SafeDefault
    620         );
    621         assert_eq!(defaulted.sources().repo_local_root(), None);
    622         assert_eq!(defaulted.repo_local_root(), None);
    623     }
    624 
    625     #[test]
    626     fn serialization_and_debug_redact_paths_and_use_only_closed_sources() {
    627         let bootstrap = RuntimeContextBootstrap::new(
    628             RadrootsPathProfile::RepoLocal,
    629             Some(PathBuf::from("/sensitive/project-root")),
    630             RuntimeContextSource::BootstrapCli,
    631             RuntimeContextSource::BootstrapCli,
    632         )
    633         .expect("bootstrap");
    634         assert_eq!(bootstrap.profile(), RadrootsPathProfile::RepoLocal);
    635         let bootstrap_debug = format!("{bootstrap:?}");
    636         assert!(bootstrap_debug.contains("repo_local_root: Some(\"[redacted]\")"));
    637         let context = repo_local_context(PathBuf::from("/sensitive/project-root"));
    638         let serialized = serde_json::to_value(&context).expect("serialize context");
    639         assert_eq!(
    640             serialized,
    641             json!({
    642                 "service": "myc",
    643                 "instance": "primary",
    644                 "profile": "repo_local",
    645                 "paths": "[redacted]",
    646                 "sources": {
    647                     "service": "safe_default",
    648                     "instance": "bootstrap_cli",
    649                     "profile": "bootstrap_cli",
    650                     "repo_local_root": "bootstrap_cli",
    651                     "paths": "derived_path"
    652                 }
    653             })
    654         );
    655         assert_eq!(
    656             serde_json::to_value([
    657                 RuntimeContextSource::BootstrapCli,
    658                 RuntimeContextSource::Toml,
    659                 RuntimeContextSource::SafeDefault,
    660                 RuntimeContextSource::DerivedPath,
    661             ])
    662             .expect("source inventory"),
    663             json!(["bootstrap_cli", "toml", "safe_default", "derived_path"])
    664         );
    665         let debug = format!("{context:?}");
    666         assert!(debug.contains("paths: \"[redacted]\""));
    667         for forbidden in [
    668             "/sensitive",
    669             "project-root",
    670             "/config/",
    671             "/run/",
    672             "secret:caller-controlled-value",
    673             "0123456789abcdef0123456789abcdef",
    674         ] {
    675             assert!(!serialized.to_string().contains(forbidden));
    676             assert!(!debug.contains(forbidden));
    677             assert!(!bootstrap_debug.contains(forbidden));
    678         }
    679     }
    680 
    681     #[test]
    682     fn construction_performs_no_directory_file_or_ambient_bootstrap_io() {
    683         let unique = SystemTime::now()
    684             .duration_since(SystemTime::UNIX_EPOCH)
    685             .expect("clock after epoch")
    686             .as_nanos();
    687         let base = std::env::temp_dir().join(format!(
    688             "radroots-runtime-context-no-io-{}-{unique}",
    689             std::process::id()
    690         ));
    691         assert!(!base.exists(), "unique test base unexpectedly exists");
    692         let context = repo_local_context(base.clone());
    693         assert_eq!(
    694             context.paths().state(),
    695             base.join("data/services/myc/primary")
    696         );
    697         assert!(!base.exists(), "context construction created the base");
    698 
    699         let production = include_str!("context.rs")
    700             .split("#[cfg(test)]")
    701             .next()
    702             .expect("production source");
    703         for forbidden in [
    704             "std::fs",
    705             "create_dir",
    706             "create_file",
    707             "OpenOptions",
    708             "std::env",
    709         ] {
    710             assert!(
    711                 !production.contains(forbidden),
    712                 "context production source contains forbidden I/O `{forbidden}`"
    713             );
    714         }
    715     }
    716 
    717     #[test]
    718     fn typed_bootstrap_rejects_every_mismatched_provenance_combination() {
    719         for source in [
    720             RuntimeContextSource::Toml,
    721             RuntimeContextSource::DerivedPath,
    722         ] {
    723             assert_eq!(
    724                 RuntimeContextBootstrap::new(
    725                     RadrootsPathProfile::ServiceHost,
    726                     None,
    727                     source,
    728                     RuntimeContextSource::BootstrapCli,
    729                 ),
    730                 Err(RuntimeContextError::InvalidBootstrapBinding)
    731             );
    732             assert_eq!(
    733                 RuntimeContextBootstrap::new(
    734                     RadrootsPathProfile::ServiceHost,
    735                     None,
    736                     RuntimeContextSource::SafeDefault,
    737                     source,
    738                 ),
    739                 Err(RuntimeContextError::InvalidBootstrapBinding)
    740             );
    741         }
    742         assert_eq!(
    743             RuntimeContextBootstrap::new(
    744                 RadrootsPathProfile::RepoLocal,
    745                 None,
    746                 RuntimeContextSource::BootstrapCli,
    747                 RuntimeContextSource::BootstrapCli,
    748             ),
    749             Err(RuntimeContextError::InvalidBootstrapBinding)
    750         );
    751         assert_eq!(
    752             RuntimeContextBootstrap::new(
    753                 RadrootsPathProfile::RepoLocal,
    754                 Some(PathBuf::from("/repo/.local/radroots")),
    755                 RuntimeContextSource::SafeDefault,
    756                 RuntimeContextSource::SafeDefault,
    757             ),
    758             Err(RuntimeContextError::InvalidBootstrapBinding)
    759         );
    760         assert_eq!(
    761             RuntimeContextBootstrap::new(
    762                 RadrootsPathProfile::ServiceHost,
    763                 Some(PathBuf::from("/repo/.local/radroots")),
    764                 RuntimeContextSource::BootstrapCli,
    765                 RuntimeContextSource::BootstrapCli,
    766             ),
    767             Err(RuntimeContextError::InvalidBootstrapBinding)
    768         );
    769         assert_eq!(
    770             RuntimeContextBootstrap::new(
    771                 RadrootsPathProfile::MobileNative,
    772                 None,
    773                 RuntimeContextSource::SafeDefault,
    774                 RuntimeContextSource::SafeDefault,
    775             ),
    776             Err(RuntimeContextError::InvalidBootstrapBinding)
    777         );
    778     }
    779 }