runtime_foundation.rs (25860B)
1 //! Existing-only, join-owned Myc runtime foundation. 2 3 use core::fmt; 4 use std::{error::Error, sync::mpsc}; 5 6 use radroots_service_host::{ 7 HostError, HostErrorKind, ShutdownPhase, TaskClassification, TaskMetadata, TaskName, 8 TaskSupervisor, 9 }; 10 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 11 12 use crate::{ 13 MycConfigDocumentV1, MycDecryptedIdentity, MycLocalSignerClient, MycProviderBinding, 14 MycProviderKind, MycProviderRole, MycRuntimeContext, MycStateHost, MycStateMetadata, 15 open_myc_encrypted_identity, open_myc_state_read_write, resolve_myc_wrapping_credential, 16 }; 17 18 #[cfg(test)] 19 const RUNTIME_FOUNDATION_CONTRACT: &str = 20 include_str!("../contracts/services_hardening/runtime_foundation.v1.json"); 21 22 /// Exact version of the Myc runtime-foundation contract. 23 pub const MYC_RUNTIME_FOUNDATION_CONTRACT_VERSION: u32 = 1; 24 25 /// Closed startup conditions that must all be satisfied before Myc is ready. 26 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 27 pub enum MycRuntimePrerequisite { 28 ExistingState, 29 TransportProvider, 30 UserProvider, 31 DiscoveryProvider, 32 OutboxRecovery, 33 RequiredRelayConnectivity, 34 RequiredRelaySubscription, 35 AdminListener, 36 OperationsListener, 37 } 38 39 impl MycRuntimePrerequisite { 40 /// Returns the exact machine-contract spelling. 41 #[must_use] 42 pub const fn as_str(self) -> &'static str { 43 match self { 44 Self::ExistingState => "existing_state", 45 Self::TransportProvider => "transport_provider", 46 Self::UserProvider => "user_provider", 47 Self::DiscoveryProvider => "discovery_provider", 48 Self::OutboxRecovery => "outbox_recovery", 49 Self::RequiredRelayConnectivity => "required_relay_connectivity", 50 Self::RequiredRelaySubscription => "required_relay_subscription", 51 Self::AdminListener => "admin_listener", 52 Self::OperationsListener => "operations_listener", 53 } 54 } 55 56 const fn reason(self) -> MycRuntimeReadinessReason { 57 match self { 58 Self::ExistingState => MycRuntimeReadinessReason::DatabaseSchemaMismatch, 59 Self::TransportProvider | Self::UserProvider | Self::DiscoveryProvider => { 60 MycRuntimeReadinessReason::SignerProviderUnavailable 61 } 62 Self::OutboxRecovery => MycRuntimeReadinessReason::OutboxInvariantFailed, 63 Self::RequiredRelayConnectivity => MycRuntimeReadinessReason::RequiredRelayUnavailable, 64 Self::RequiredRelaySubscription => MycRuntimeReadinessReason::SubscriberNotActive, 65 Self::AdminListener => MycRuntimeReadinessReason::AdminListenerFailed, 66 Self::OperationsListener => MycRuntimeReadinessReason::OperationsListenerFailed, 67 } 68 } 69 } 70 71 /// Closed stable reason vocabulary for an unsatisfied runtime prerequisite. 72 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 73 pub enum MycRuntimeReadinessReason { 74 AdminListenerFailed, 75 DatabaseSchemaMismatch, 76 OperationsListenerFailed, 77 OutboxInvariantFailed, 78 RequiredRelayUnavailable, 79 SignerProviderUnavailable, 80 SubscriberNotActive, 81 } 82 83 impl MycRuntimeReadinessReason { 84 /// Returns the exact machine-contract spelling. 85 #[must_use] 86 pub const fn as_str(self) -> &'static str { 87 match self { 88 Self::AdminListenerFailed => "admin_listener_failed", 89 Self::DatabaseSchemaMismatch => "database_schema_mismatch", 90 Self::OperationsListenerFailed => "operations_listener_failed", 91 Self::OutboxInvariantFailed => "outbox_invariant_failed", 92 Self::RequiredRelayUnavailable => "required_relay_unavailable", 93 Self::SignerProviderUnavailable => "signer_provider_unavailable", 94 Self::SubscriberNotActive => "subscriber_not_active", 95 } 96 } 97 } 98 99 /// Immutable startup-readiness projection derived from admitted configuration. 100 /// 101 /// This snapshot is passive evidence. It performs no provider, relay, SQLite, 102 /// DNS, listener, or filesystem probe when read. 103 #[derive(Clone, PartialEq, Eq)] 104 pub struct MycRuntimeReadiness { 105 required: Box<[MycRuntimePrerequisite]>, 106 satisfied: Box<[MycRuntimePrerequisite]>, 107 reasons: Box<[MycRuntimeReadinessReason]>, 108 } 109 110 impl MycRuntimeReadiness { 111 /// Returns readiness only when every exact prerequisite is satisfied. 112 #[must_use] 113 pub fn is_ready(&self) -> bool { 114 self.required.len() == self.satisfied.len() 115 && self 116 .required 117 .iter() 118 .all(|required| self.satisfied.contains(required)) 119 } 120 121 /// Returns the exact ordered prerequisite inventory for this configuration. 122 #[must_use] 123 pub fn required(&self) -> &[MycRuntimePrerequisite] { 124 &self.required 125 } 126 127 /// Returns the exact ordered prerequisites already proven at construction. 128 #[must_use] 129 pub fn satisfied(&self) -> &[MycRuntimePrerequisite] { 130 &self.satisfied 131 } 132 133 /// Returns bounded stable reasons for every class of missing prerequisite. 134 #[must_use] 135 pub const fn reasons(&self) -> &[MycRuntimeReadinessReason] { 136 &self.reasons 137 } 138 } 139 140 impl fmt::Debug for MycRuntimeReadiness { 141 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 142 formatter 143 .debug_struct("MycRuntimeReadiness") 144 .field("ready", &self.is_ready()) 145 .field("required", &self.required) 146 .field("satisfied", &self.satisfied) 147 .field("reasons", &self.reasons) 148 .finish() 149 } 150 } 151 152 /// Stable source-free runtime-foundation failure class. 153 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 154 pub enum MycRuntimeFoundationErrorKind { 155 InvalidBinding, 156 StateOpen, 157 Provider, 158 TaskRegistration, 159 TaskFailure, 160 Readiness, 161 Close, 162 } 163 164 impl MycRuntimeFoundationErrorKind { 165 /// Returns the stable machine-facing safe code. 166 #[must_use] 167 pub const fn code(self) -> &'static str { 168 match self { 169 Self::InvalidBinding => "runtime_binding_invalid", 170 Self::StateOpen => "runtime_state_open_failed", 171 Self::Provider => "runtime_provider_unavailable", 172 Self::TaskRegistration => "runtime_task_registration_failed", 173 Self::TaskFailure => "runtime_task_failed", 174 Self::Readiness => "runtime_readiness_invalid", 175 Self::Close => "runtime_close_failed", 176 } 177 } 178 179 const fn message(self) -> &'static str { 180 match self { 181 Self::InvalidBinding => "Myc runtime binding is invalid", 182 Self::StateOpen => "Myc existing state could not be opened", 183 Self::Provider => "Myc provider startup failed", 184 Self::TaskRegistration => "Myc runtime task registration failed", 185 Self::TaskFailure => "Myc supervised startup task failed", 186 Self::Readiness => "Myc readiness prerequisites are invalid", 187 Self::Close => "Myc runtime foundation could not close", 188 } 189 } 190 } 191 192 /// One redacted source-free runtime-foundation failure. 193 #[derive(Clone, Copy, PartialEq, Eq)] 194 pub struct MycRuntimeFoundationError { 195 kind: MycRuntimeFoundationErrorKind, 196 } 197 198 impl MycRuntimeFoundationError { 199 const fn new(kind: MycRuntimeFoundationErrorKind) -> Self { 200 Self { kind } 201 } 202 203 /// Returns the stable failure kind. 204 #[must_use] 205 pub const fn kind(self) -> MycRuntimeFoundationErrorKind { 206 self.kind 207 } 208 209 /// Returns the stable machine-facing safe code. 210 #[must_use] 211 pub const fn code(self) -> &'static str { 212 self.kind.code() 213 } 214 } 215 216 impl fmt::Debug for MycRuntimeFoundationError { 217 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 218 formatter 219 .debug_struct("MycRuntimeFoundationError") 220 .field("kind", &self.kind) 221 .finish() 222 } 223 } 224 225 impl fmt::Display for MycRuntimeFoundationError { 226 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 227 formatter.write_str(self.kind.message()) 228 } 229 } 230 231 impl Error for MycRuntimeFoundationError {} 232 233 enum MycRuntimeProvider { 234 EncryptedFile { 235 role: MycProviderRole, 236 _identity: MycDecryptedIdentity, 237 }, 238 LocalSigner { 239 role: MycProviderRole, 240 _client: Box<MycLocalSignerClient>, 241 }, 242 } 243 244 enum ProviderStartupOutcome { 245 Ready(MycDecryptedIdentity), 246 ProviderFailure, 247 TaskFailure, 248 } 249 250 impl MycRuntimeProvider { 251 const fn role(&self) -> MycProviderRole { 252 match self { 253 Self::EncryptedFile { role, .. } | Self::LocalSigner { role, .. } => *role, 254 } 255 } 256 257 const fn kind(&self) -> MycProviderKind { 258 match self { 259 Self::EncryptedFile { .. } => MycProviderKind::EncryptedFile, 260 Self::LocalSigner { .. } => MycProviderKind::LocalSigner, 261 } 262 } 263 } 264 265 /// Existing-only Myc foundation with sealed state, provider, and task ownership. 266 /// 267 /// The final relay/admin/process task graph remains owned by later RCLDs. This 268 /// value cannot be constructed directly or used to extract raw SQLite, 269 /// provider-secret, task-handle, or cancellation authority. 270 #[must_use = "the runtime foundation must be shut down so owned tasks and state are joined"] 271 pub struct MycRuntimeFoundation { 272 runtime: MycRuntimeContext, 273 configuration: MycConfigDocumentV1, 274 metadata: MycStateMetadata, 275 state: MycStateHost, 276 providers: Box<[MycRuntimeProvider]>, 277 readiness: MycRuntimeReadiness, 278 supervisor: TaskSupervisor, 279 } 280 281 impl MycRuntimeFoundation { 282 /// Returns the immutable canonical instance context. 283 #[must_use] 284 pub const fn runtime_context(&self) -> &MycRuntimeContext { 285 &self.runtime 286 } 287 288 /// Returns the admitted immutable configuration. 289 #[must_use] 290 pub const fn configuration(&self) -> &MycConfigDocumentV1 { 291 &self.configuration 292 } 293 294 /// Returns metadata proven against the existing state host. 295 #[must_use] 296 pub const fn metadata(&self) -> &MycStateMetadata { 297 &self.metadata 298 } 299 300 /// Returns the passive startup-readiness snapshot. 301 #[must_use] 302 pub const fn readiness(&self) -> &MycRuntimeReadiness { 303 &self.readiness 304 } 305 306 /// Returns the configured retained provider kind for one enabled role. 307 #[must_use] 308 pub fn provider_kind(&self, role: MycProviderRole) -> Option<MycProviderKind> { 309 self.providers 310 .iter() 311 .find(|provider| provider.role() == role) 312 .map(MycRuntimeProvider::kind) 313 } 314 315 /// Requests cancellation, joins every owned task, and explicitly closes state. 316 pub async fn shutdown(mut self) -> Result<(), MycRuntimeFoundationError> { 317 self.supervisor.request_cancellation(); 318 let supervised = self.supervisor.supervise().await; 319 let closed = self.state.close().await; 320 if supervised.is_err() { 321 Err(MycRuntimeFoundationError::new( 322 MycRuntimeFoundationErrorKind::TaskFailure, 323 )) 324 } else if closed.is_err() { 325 Err(MycRuntimeFoundationError::new( 326 MycRuntimeFoundationErrorKind::Close, 327 )) 328 } else { 329 Ok(()) 330 } 331 } 332 } 333 334 impl fmt::Debug for MycRuntimeFoundation { 335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 336 formatter 337 .debug_struct("MycRuntimeFoundation") 338 .field("runtime", &"[redacted]") 339 .field("configuration", &"[redacted]") 340 .field("metadata", &"[redacted]") 341 .field("state", &"[sealed]") 342 .field("provider_count", &self.providers.len()) 343 .field("readiness", &self.readiness) 344 .field("task_count", &self.supervisor.task_count()) 345 .finish() 346 } 347 } 348 349 /// Opens an existing Myc state host and composes its sealed startup foundation. 350 /// 351 /// Missing state is never initialized. Encrypted-file providers are opened on 352 /// bounded one-shot worker threads that are synchronously joined by 353 /// `TaskSupervisor`; local-signer clients are constructed without I/O and 354 /// remain unready until a later governed handshake succeeds. No task handle is 355 /// detached or returned. 356 pub async fn open_myc_runtime_foundation( 357 runtime: MycRuntimeContext, 358 configuration: MycConfigDocumentV1, 359 metadata: MycStateMetadata, 360 applied_at: MigrationAppliedAtUnixSeconds, 361 build: &MigrationBuildIdentity, 362 ) -> Result<MycRuntimeFoundation, MycRuntimeFoundationError> { 363 if !metadata.matches_configuration(&runtime, &configuration) { 364 return Err(MycRuntimeFoundationError::new( 365 MycRuntimeFoundationErrorKind::InvalidBinding, 366 )); 367 } 368 let state = open_myc_state_read_write(&runtime, &metadata, applied_at, build) 369 .await 370 .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::StateOpen))?; 371 372 match compose_after_state_open(runtime, configuration, metadata, state).await { 373 Ok(foundation) => Ok(foundation), 374 Err((error, state)) => { 375 if state.close().await.is_err() { 376 Err(MycRuntimeFoundationError::new( 377 MycRuntimeFoundationErrorKind::Close, 378 )) 379 } else { 380 Err(error) 381 } 382 } 383 } 384 } 385 386 async fn compose_after_state_open( 387 runtime: MycRuntimeContext, 388 configuration: MycConfigDocumentV1, 389 metadata: MycStateMetadata, 390 state: MycStateHost, 391 ) -> Result<MycRuntimeFoundation, (MycRuntimeFoundationError, MycStateHost)> { 392 let (providers, readiness, supervisor) = 393 match compose_runtime_components(&runtime, &configuration).await { 394 Ok(components) => components, 395 Err(error) => return Err((error, state)), 396 }; 397 Ok(MycRuntimeFoundation { 398 runtime, 399 configuration, 400 metadata, 401 state, 402 providers, 403 readiness, 404 supervisor, 405 }) 406 } 407 408 async fn compose_runtime_components( 409 runtime: &MycRuntimeContext, 410 configuration: &MycConfigDocumentV1, 411 ) -> Result< 412 ( 413 Box<[MycRuntimeProvider]>, 414 MycRuntimeReadiness, 415 TaskSupervisor, 416 ), 417 MycRuntimeFoundationError, 418 > { 419 let mut supervisor = TaskSupervisor::new(); 420 let mut providers: Vec<Option<MycRuntimeProvider>> = configuration 421 .provider_contract() 422 .bindings() 423 .iter() 424 .map(|_| None) 425 .collect(); 426 let mut encrypted = Vec::new(); 427 let mut pending = Vec::new(); 428 429 for (index, binding) in configuration 430 .provider_contract() 431 .bindings() 432 .iter() 433 .cloned() 434 .enumerate() 435 { 436 match binding.kind() { 437 MycProviderKind::EncryptedFile => { 438 let role = binding.role(); 439 let task = provider_task_metadata(role)?; 440 encrypted.push((index, role, binding, task)); 441 } 442 MycProviderKind::LocalSigner => { 443 let role = binding.role(); 444 let client = MycLocalSignerClient::new(&binding).map_err(|_| { 445 MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Provider) 446 })?; 447 providers[index] = Some(MycRuntimeProvider::LocalSigner { 448 role, 449 _client: Box::new(client), 450 }); 451 } 452 } 453 } 454 455 for (index, role, binding, task) in encrypted { 456 let (sender, receiver) = mpsc::sync_channel(1); 457 let task_runtime = runtime.clone(); 458 let registered = supervisor.spawn(task, move |_cancellation| async move { 459 let outcome = match std::thread::Builder::new() 460 .name(provider_thread_name(role).to_owned()) 461 .spawn(move || open_encrypted_provider(&task_runtime, &binding)) 462 { 463 Ok(thread) => match thread.join() { 464 Ok(Ok(identity)) => ProviderStartupOutcome::Ready(identity), 465 Ok(Err(())) => ProviderStartupOutcome::ProviderFailure, 466 Err(_) => ProviderStartupOutcome::TaskFailure, 467 }, 468 Err(_) => ProviderStartupOutcome::TaskFailure, 469 }; 470 let succeeded = matches!(outcome, ProviderStartupOutcome::Ready(_)); 471 let _ = sender.send(outcome); 472 if succeeded { 473 Ok(()) 474 } else { 475 Err(HostError::new(HostErrorKind::TaskFailure)) 476 } 477 }); 478 if registered.is_err() { 479 supervisor.request_cancellation(); 480 let _ = supervisor.supervise().await; 481 return Err(MycRuntimeFoundationError::new( 482 MycRuntimeFoundationErrorKind::TaskRegistration, 483 )); 484 } 485 pending.push((index, role, receiver)); 486 } 487 488 let supervised = supervisor.supervise().await; 489 let mut provider_failure = false; 490 let mut task_failure = false; 491 for (index, role, receiver) in pending { 492 match receiver.recv() { 493 Ok(ProviderStartupOutcome::Ready(identity)) => { 494 providers[index] = Some(MycRuntimeProvider::EncryptedFile { 495 role, 496 _identity: identity, 497 }); 498 } 499 Ok(ProviderStartupOutcome::ProviderFailure) => provider_failure = true, 500 Ok(ProviderStartupOutcome::TaskFailure) | Err(_) => task_failure = true, 501 } 502 } 503 if task_failure { 504 return Err(MycRuntimeFoundationError::new( 505 MycRuntimeFoundationErrorKind::TaskFailure, 506 )); 507 } 508 if provider_failure { 509 return Err(MycRuntimeFoundationError::new( 510 MycRuntimeFoundationErrorKind::Provider, 511 )); 512 } 513 if supervised.is_err() { 514 return Err(MycRuntimeFoundationError::new( 515 MycRuntimeFoundationErrorKind::TaskFailure, 516 )); 517 } 518 let providers = providers 519 .into_iter() 520 .collect::<Option<Vec<_>>>() 521 .ok_or_else(|| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Provider))? 522 .into_boxed_slice(); 523 524 let readiness = startup_readiness(configuration, &providers)?; 525 let lifetime = TaskMetadata::new( 526 TaskName::new("runtime_lifetime").map_err(|_| { 527 MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration) 528 })?, 529 TaskClassification::Critical, 530 Some(ShutdownPhase::RejectNewMutations), 531 ) 532 .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration))?; 533 supervisor 534 .spawn(lifetime, |cancellation| async move { 535 cancellation.cancelled().await; 536 Ok(()) 537 }) 538 .map_err(|_| { 539 MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration) 540 })?; 541 542 Ok((providers, readiness, supervisor)) 543 } 544 545 fn open_encrypted_provider( 546 runtime: &MycRuntimeContext, 547 binding: &MycProviderBinding, 548 ) -> Result<MycDecryptedIdentity, ()> { 549 let credential = resolve_myc_wrapping_credential(runtime, binding).map_err(|_| ())?; 550 open_myc_encrypted_identity(binding, &credential).map_err(|_| ()) 551 } 552 553 fn provider_task_metadata( 554 role: MycProviderRole, 555 ) -> Result<TaskMetadata, MycRuntimeFoundationError> { 556 TaskMetadata::new( 557 TaskName::new(provider_task_name(role)).map_err(|_| { 558 MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration) 559 })?, 560 TaskClassification::OneShot, 561 None, 562 ) 563 .map_err(|_| MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::TaskRegistration)) 564 } 565 566 const fn provider_task_name(role: MycProviderRole) -> &'static str { 567 match role { 568 MycProviderRole::Transport => "startup_transport_provider", 569 MycProviderRole::User => "startup_user_provider", 570 MycProviderRole::Discovery => "startup_discovery_provider", 571 } 572 } 573 574 const fn provider_thread_name(role: MycProviderRole) -> &'static str { 575 match role { 576 MycProviderRole::Transport => "myc-provider-transport", 577 MycProviderRole::User => "myc-provider-user", 578 MycProviderRole::Discovery => "myc-provider-discovery", 579 } 580 } 581 582 fn startup_readiness( 583 configuration: &MycConfigDocumentV1, 584 providers: &[MycRuntimeProvider], 585 ) -> Result<MycRuntimeReadiness, MycRuntimeFoundationError> { 586 let document = configuration.normalized(); 587 let mut required = vec![ 588 MycRuntimePrerequisite::ExistingState, 589 MycRuntimePrerequisite::TransportProvider, 590 MycRuntimePrerequisite::UserProvider, 591 ]; 592 if configuration 593 .provider_contract() 594 .binding(MycProviderRole::Discovery) 595 .is_some() 596 { 597 required.push(MycRuntimePrerequisite::DiscoveryProvider); 598 } 599 required.push(MycRuntimePrerequisite::OutboxRecovery); 600 601 let relays = document 602 .pointer("/relays") 603 .and_then(serde_json::Value::as_array) 604 .ok_or_else(readiness_error)?; 605 if relays.iter().any(|relay| { 606 relay 607 .pointer("/required") 608 .and_then(serde_json::Value::as_bool) 609 == Some(true) 610 }) { 611 required.push(MycRuntimePrerequisite::RequiredRelayConnectivity); 612 } 613 if relays.iter().any(|relay| { 614 relay 615 .pointer("/required") 616 .and_then(serde_json::Value::as_bool) 617 == Some(true) 618 && relay.pointer("/read").and_then(serde_json::Value::as_bool) == Some(true) 619 }) { 620 required.push(MycRuntimePrerequisite::RequiredRelaySubscription); 621 } 622 required.push(MycRuntimePrerequisite::AdminListener); 623 if document 624 .pointer("/operations/enabled") 625 .and_then(serde_json::Value::as_bool) 626 .ok_or_else(readiness_error)? 627 { 628 required.push(MycRuntimePrerequisite::OperationsListener); 629 } 630 631 let mut satisfied = vec![MycRuntimePrerequisite::ExistingState]; 632 for provider in providers { 633 if provider.kind() == MycProviderKind::EncryptedFile { 634 satisfied.push(provider_prerequisite(provider.role())); 635 } 636 } 637 satisfied.sort_by_key(|prerequisite| { 638 required 639 .iter() 640 .position(|candidate| candidate == prerequisite) 641 .unwrap_or(usize::MAX) 642 }); 643 let mut reasons = required 644 .iter() 645 .filter(|prerequisite| !satisfied.contains(prerequisite)) 646 .map(|prerequisite| prerequisite.reason()) 647 .collect::<Vec<_>>(); 648 reasons.sort_unstable(); 649 reasons.dedup(); 650 Ok(MycRuntimeReadiness { 651 required: required.into_boxed_slice(), 652 satisfied: satisfied.into_boxed_slice(), 653 reasons: reasons.into_boxed_slice(), 654 }) 655 } 656 657 const fn provider_prerequisite(role: MycProviderRole) -> MycRuntimePrerequisite { 658 match role { 659 MycProviderRole::Transport => MycRuntimePrerequisite::TransportProvider, 660 MycProviderRole::User => MycRuntimePrerequisite::UserProvider, 661 MycProviderRole::Discovery => MycRuntimePrerequisite::DiscoveryProvider, 662 } 663 } 664 665 const fn readiness_error() -> MycRuntimeFoundationError { 666 MycRuntimeFoundationError::new(MycRuntimeFoundationErrorKind::Readiness) 667 } 668 669 #[cfg(test)] 670 mod tests { 671 use super::*; 672 673 #[test] 674 fn machine_contract_and_closed_names_are_exact() { 675 let contract: serde_json::Value = 676 serde_json::from_str(RUNTIME_FOUNDATION_CONTRACT).expect("runtime contract"); 677 assert_eq!(contract["schema"], "radroots.myc.runtime-foundation"); 678 assert_eq!(contract["contract_version"], 1); 679 let names = contract["readiness_prerequisites"] 680 .as_array() 681 .expect("prerequisite inventory") 682 .iter() 683 .map(|entry| entry["id"].as_str().expect("prerequisite id")) 684 .collect::<Vec<_>>(); 685 let expected = [ 686 MycRuntimePrerequisite::ExistingState, 687 MycRuntimePrerequisite::TransportProvider, 688 MycRuntimePrerequisite::UserProvider, 689 MycRuntimePrerequisite::DiscoveryProvider, 690 MycRuntimePrerequisite::OutboxRecovery, 691 MycRuntimePrerequisite::RequiredRelayConnectivity, 692 MycRuntimePrerequisite::RequiredRelaySubscription, 693 MycRuntimePrerequisite::AdminListener, 694 MycRuntimePrerequisite::OperationsListener, 695 ]; 696 assert_eq!( 697 names, 698 expected 699 .into_iter() 700 .map(MycRuntimePrerequisite::as_str) 701 .collect::<Vec<_>>() 702 ); 703 let reasons = contract["readiness_prerequisites"] 704 .as_array() 705 .expect("prerequisite inventory") 706 .iter() 707 .map(|entry| entry["reason"].as_str().expect("prerequisite reason")) 708 .collect::<Vec<_>>(); 709 assert_eq!( 710 reasons, 711 expected 712 .into_iter() 713 .map(MycRuntimePrerequisite::reason) 714 .map(MycRuntimeReadinessReason::as_str) 715 .collect::<Vec<_>>() 716 ); 717 } 718 719 #[test] 720 fn safe_errors_cover_the_closed_inventory_without_sources() { 721 for kind in [ 722 MycRuntimeFoundationErrorKind::InvalidBinding, 723 MycRuntimeFoundationErrorKind::StateOpen, 724 MycRuntimeFoundationErrorKind::Provider, 725 MycRuntimeFoundationErrorKind::TaskRegistration, 726 MycRuntimeFoundationErrorKind::TaskFailure, 727 MycRuntimeFoundationErrorKind::Readiness, 728 MycRuntimeFoundationErrorKind::Close, 729 ] { 730 let error = MycRuntimeFoundationError::new(kind); 731 assert!(!error.code().is_empty()); 732 assert!(Error::source(&error).is_none()); 733 assert!(!format!("{error} {error:?}").contains("source")); 734 } 735 } 736 }