1pub mod install;
2
3use crate::{
4 cdk::types::Principal,
5 domain::runtime::{
6 FailureSeverity, HealthStatus, ReadinessStatus, RuntimeCheckStatus,
7 RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus, RuntimeStatus,
8 TimerStatus,
9 },
10 dto::{
11 error::Error,
12 runtime::{
13 CanicHealthStatus, CanicReadinessStatus, CanicRuntimeStatus, CanicTimerStatus,
14 RUNTIME_INTROSPECTION_SCHEMA_VERSION, RuntimeAuthStatusSummary,
15 RuntimeBlobStorageStatusSummary, RuntimeBuildInfo, RuntimeCheck, RuntimeDiagnostic,
16 RuntimeFeatureStatus, RuntimeStateDomainSummary, RuntimeStateSummary,
17 RuntimeTopologyStatus, RuntimeVisibilityEntry,
18 },
19 },
20 ops::{
21 ic::{IcOps, build_network::BuildNetworkOps},
22 runtime::{
23 env::EnvOps,
24 memory::MemoryRegistryOps,
25 metrics::timer::TimerMetrics,
26 ready::ReadyOps,
27 recent_failure::{RecentFailureInput, RecentFailureOps},
28 },
29 },
30 state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
31};
32
33const MAX_TIMER_SUBSYSTEM_BYTES: usize = 64;
34const MAX_TIMER_NAME_BYTES: usize = 96;
35const RUNTIME_FEATURE_SOURCE: &str = "compile_feature";
36const RUNTIME_FEATURE_FLAGS: [(&str, bool); 10] = [
37 (
38 "auth-chain-key-ecdsa",
39 cfg!(feature = "auth-chain-key-ecdsa"),
40 ),
41 (
42 "auth-chain-key-root-sign",
43 cfg!(feature = "auth-chain-key-root-sign"),
44 ),
45 (
46 "auth-delegated-token-verify",
47 cfg!(feature = "auth-delegated-token-verify"),
48 ),
49 (
50 "auth-issuer-canister-sig-create",
51 cfg!(feature = "auth-issuer-canister-sig-create"),
52 ),
53 (
54 "auth-issuer-canister-sig-verify",
55 cfg!(feature = "auth-issuer-canister-sig-verify"),
56 ),
57 (
58 "auth-root-canister-sig-create",
59 cfg!(feature = "auth-root-canister-sig-create"),
60 ),
61 (
62 "auth-root-canister-sig-verify",
63 cfg!(feature = "auth-root-canister-sig-verify"),
64 ),
65 ("blob-storage", cfg!(feature = "blob-storage")),
66 (
67 "blob-storage-billing",
68 cfg!(feature = "blob-storage-billing"),
69 ),
70 ("sharding", cfg!(feature = "sharding")),
71];
72
73pub struct MemoryRuntimeApi;
78
79impl MemoryRuntimeApi {
80 pub fn bootstrap_registry() -> Result<(), Error> {
82 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
83
84 Ok(())
85 }
86}
87
88pub struct RuntimeIntrospectionApi;
93
94impl RuntimeIntrospectionApi {
95 pub fn record_recent_failure(
97 occurred_at_ns: u64,
98 subsystem: impl Into<String>,
99 code: impl Into<String>,
100 severity: FailureSeverity,
101 summary: impl Into<String>,
102 correlation_id: Option<String>,
103 ) {
104 RecentFailureOps::record(RecentFailureInput {
105 occurred_at_ns,
106 subsystem: subsystem.into(),
107 code: code.into(),
108 severity,
109 summary: summary.into(),
110 correlation_id,
111 });
112 }
113
114 #[must_use]
116 pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
117 CanicHealthStatus {
118 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
119 status: HealthStatus::Healthy,
120 observed_at_ns,
121 checks: vec![RuntimeCheck {
122 category: "health".to_string(),
123 code: "canister_responsive".to_string(),
124 status: RuntimeCheckStatus::Pass,
125 subject: "canister".to_string(),
126 detail: "canister returned a health response".to_string(),
127 next: None,
128 source: "runtime_observed".to_string(),
129 }],
130 }
131 }
132
133 #[must_use]
135 pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
136 let ready = ReadyOps::is_ready();
137 let role = EnvOps::canister_role()
138 .ok()
139 .map(crate::ids::CanisterRole::into_string);
140
141 let (status, check_status, detail, next) = if ready {
142 (
143 ReadinessStatus::Ready,
144 RuntimeCheckStatus::Pass,
145 "runtime readiness barrier is marked ready",
146 None,
147 )
148 } else {
149 (
150 ReadinessStatus::NotReady,
151 RuntimeCheckStatus::Fail,
152 "runtime readiness barrier is not ready",
153 Some("wait for bootstrap to complete or inspect canic_bootstrap_status"),
154 )
155 };
156
157 let readiness_check = RuntimeCheck {
158 category: "readiness".to_string(),
159 code: "runtime_ready_barrier".to_string(),
160 status: check_status,
161 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
162 detail: detail.to_string(),
163 next: next.map(str::to_string),
164 source: "runtime_observed".to_string(),
165 };
166
167 let blockers = if ready {
168 Vec::new()
169 } else {
170 vec![RuntimeDiagnostic {
171 category: "readiness".to_string(),
172 code: "runtime_not_ready".to_string(),
173 severity: RuntimeDiagnosticSeverity::Blocked,
174 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
175 detail: "runtime readiness barrier has not completed".to_string(),
176 next: Some(
177 "inspect bootstrap status before treating the role as ready".to_string(),
178 ),
179 source: "runtime_observed".to_string(),
180 }]
181 };
182
183 CanicReadinessStatus {
184 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
185 role,
186 status,
187 observed_at_ns,
188 checks: vec![readiness_check],
189 blockers,
190 warnings: Vec::new(),
191 }
192 }
193
194 #[must_use]
196 pub fn runtime_status_for(
197 canister_id: Principal,
198 observed_at_ns: u64,
199 package_name: &str,
200 package_version: &str,
201 canic_version: &str,
202 canister_version: u64,
203 ) -> CanicRuntimeStatus {
204 let readiness = Self::readiness(observed_at_ns);
205 let role = readiness.role.clone();
206 let state = state_summary(role.as_deref());
207 let root = EnvOps::root_pid().ok();
208 let parent = EnvOps::parent_pid().ok();
209 let subnet = EnvOps::subnet_pid().ok();
210 let status = match readiness.status {
211 ReadinessStatus::Ready => RuntimeStatus::Ok,
212 ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated => RuntimeStatus::Degraded,
213 ReadinessStatus::NotReady => RuntimeStatus::Failing,
214 };
215
216 CanicRuntimeStatus {
217 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
218 observed_at_ns,
219 canister_id,
220 role,
221 root,
222 build_network: BuildNetworkOps::build_network(),
223 build: RuntimeBuildInfo {
224 package_name: package_name.to_string(),
225 package_version: package_version.to_string(),
226 canic_version: canic_version.to_string(),
227 canister_version,
228 },
229 features: runtime_features(),
230 topology: Some(RuntimeTopologyStatus {
231 root,
232 parent,
233 subnet,
234 source: "runtime_observed".to_string(),
235 }),
236 timers: timer_statuses(),
237 state,
238 auth: Some(runtime_auth_status()),
239 blob_storage: runtime_blob_storage_status(),
240 recent_failures: RecentFailureOps::snapshot(),
241 visibility: runtime_visibility(),
242 readiness,
243 status,
244 }
245 }
246
247 #[must_use]
249 pub fn runtime_status(
250 observed_at_ns: u64,
251 package_name: &str,
252 package_version: &str,
253 canic_version: &str,
254 canister_version: u64,
255 ) -> CanicRuntimeStatus {
256 Self::runtime_status_for(
257 IcOps::canister_self(),
258 observed_at_ns,
259 package_name,
260 package_version,
261 canic_version,
262 canister_version,
263 )
264 }
265}
266
267fn runtime_features() -> Vec<RuntimeFeatureStatus> {
268 RUNTIME_FEATURE_FLAGS
269 .into_iter()
270 .map(|(name, enabled)| runtime_feature_status(name, enabled))
271 .collect()
272}
273
274fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
275 RuntimeFeatureStatus {
276 name: name.to_string(),
277 enabled,
278 visibility: RuntimeFieldVisibility::OperatorOnly,
279 source: RUNTIME_FEATURE_SOURCE.to_string(),
280 }
281}
282
283fn runtime_auth_status() -> RuntimeAuthStatusSummary {
284 RuntimeAuthStatusSummary {
285 auth_features: RUNTIME_FEATURE_FLAGS
286 .into_iter()
287 .filter(|(name, _)| name.starts_with("auth-"))
288 .map(|(name, enabled)| runtime_feature_status(name, enabled))
289 .collect(),
290 }
291}
292
293fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
294 let blob_storage_enabled = cfg!(feature = "blob-storage");
295 let billing_enabled = cfg!(feature = "blob-storage-billing");
296
297 (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
298 blob_storage_features: [
299 ("blob-storage", blob_storage_enabled),
300 ("blob-storage-billing", billing_enabled),
301 ]
302 .into_iter()
303 .map(|(name, enabled)| runtime_feature_status(name, enabled))
304 .collect(),
305 })
306}
307
308fn timer_statuses() -> Vec<CanicTimerStatus> {
309 let mut timers = TimerMetrics::snapshot()
310 .entries
311 .into_iter()
312 .map(|(key, ticks)| {
313 let (subsystem, name) = split_timer_label(&key.label);
314 CanicTimerStatus {
315 name,
316 subsystem,
317 status: if ticks > 0 {
318 TimerStatus::Healthy
319 } else {
320 TimerStatus::Unknown
321 },
322 enabled: true,
323 registered: true,
324 last_success_at_ns: None,
325 last_failure_at_ns: None,
326 next_due_at_ns: None,
327 consecutive_failures: 0,
328 last_error_code: None,
329 last_error_summary: None,
330 }
331 })
332 .collect::<Vec<_>>();
333 timers.sort_by(|left, right| {
334 left.subsystem
335 .cmp(&right.subsystem)
336 .then_with(|| left.name.cmp(&right.name))
337 });
338 timers
339}
340
341fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
342 let memory_ids = MemoryRegistryOps::ledger_snapshot()
343 .ok()?
344 .memories
345 .into_iter()
346 .map(|memory| memory.memory_manager_id)
347 .collect::<std::collections::BTreeSet<_>>();
348 state_summary_for_memory_ids(role, &memory_ids)
349}
350
351fn state_summary_for_memory_ids(
352 role: Option<&str>,
353 memory_ids: &std::collections::BTreeSet<u8>,
354) -> Option<RuntimeStateSummary> {
355 role?;
356 let mut domains = canic_state_descriptors()
357 .into_iter()
358 .flat_map(|descriptor| descriptor.state)
359 .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
360 .map(|domain| RuntimeStateDomainSummary {
361 domain: domain.domain,
362 version: domain.version,
363 storage: domain.storage.as_str().to_string(),
364 memory_id: domain.memory_id,
365 status: RuntimeStateDomainStatus::Ok,
366 })
367 .collect::<Vec<_>>();
368 domains.sort_by(|left, right| left.domain.cmp(&right.domain));
369
370 if domains.is_empty() {
371 return None;
372 }
373
374 Some(RuntimeStateSummary {
375 manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
376 domains,
377 total_stable_memory_pages: None,
378 })
379}
380
381fn split_timer_label(label: &str) -> (String, String) {
382 label.split_once(':').map_or_else(
383 || {
384 (
385 "runtime".to_string(),
386 bounded_runtime_text(label, MAX_TIMER_NAME_BYTES),
387 )
388 },
389 |(subsystem, name)| {
390 (
391 bounded_runtime_text(subsystem, MAX_TIMER_SUBSYSTEM_BYTES),
392 bounded_runtime_text(name, MAX_TIMER_NAME_BYTES),
393 )
394 },
395 )
396}
397
398fn bounded_runtime_text(value: &str, max_bytes: usize) -> String {
399 let sanitized = value
400 .chars()
401 .map(|character| {
402 if character.is_control() {
403 ' '
404 } else {
405 character
406 }
407 })
408 .collect::<String>();
409
410 if sanitized.len() <= max_bytes {
411 return sanitized;
412 }
413
414 let mut end = 0;
415 for (index, character) in sanitized.char_indices() {
416 let next = index + character.len_utf8();
417 if next > max_bytes {
418 break;
419 }
420 end = next;
421 }
422
423 sanitized[..end].to_string()
424}
425
426fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
427 [
428 ("schema_version", RuntimeFieldVisibility::PublicSafe),
429 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
430 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
431 ("role", RuntimeFieldVisibility::OperatorOnly),
432 ("root", RuntimeFieldVisibility::OperatorOnly),
433 ("build_network", RuntimeFieldVisibility::OperatorOnly),
434 ("build", RuntimeFieldVisibility::OperatorOnly),
435 ("features", RuntimeFieldVisibility::OperatorOnly),
436 ("topology", RuntimeFieldVisibility::ControllerOnly),
437 ("timers", RuntimeFieldVisibility::OperatorOnly),
438 ("state", RuntimeFieldVisibility::OperatorOnly),
439 ("auth", RuntimeFieldVisibility::OperatorOnly),
440 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
441 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
442 ("readiness", RuntimeFieldVisibility::OperatorOnly),
443 ("status", RuntimeFieldVisibility::OperatorOnly),
444 ("visibility", RuntimeFieldVisibility::OperatorOnly),
445 ]
446 .into_iter()
447 .map(|(field, visibility)| RuntimeVisibilityEntry {
448 field: field.to_string(),
449 visibility,
450 })
451 .collect()
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
458 use crate::ops::runtime::recent_failure::RecentFailureOps;
459 use crate::{domain::runtime::TimerMode, ops::runtime::metrics::timer::TimerMetrics};
460 use std::time::Duration;
461
462 #[test]
463 fn health_is_minimal_and_schema_versioned() {
464 let health = RuntimeIntrospectionApi::health(Some(42));
465
466 assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
467 assert_eq!(health.status, HealthStatus::Healthy);
468 assert_eq!(health.observed_at_ns, Some(42));
469 assert_eq!(health.checks.len(), 1);
470 assert_eq!(health.checks[0].code, "canister_responsive");
471 }
472
473 #[test]
474 fn runtime_status_embeds_guarded_readiness_and_build_info() {
475 let status = RuntimeIntrospectionApi::runtime_status_for(
476 Principal::anonymous(),
477 100,
478 "test-canister",
479 "1.2.3",
480 "0.81.0",
481 7,
482 );
483
484 assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
485 assert_eq!(status.observed_at_ns, 100);
486 assert_eq!(status.canister_id, Principal::anonymous());
487 assert_eq!(status.build_network, BuildNetworkOps::build_network());
488 assert_eq!(status.build.package_name, "test-canister");
489 assert_eq!(status.build.package_version, "1.2.3");
490 assert_eq!(status.build.canic_version, "0.81.0");
491 assert_eq!(status.build.canister_version, 7);
492 assert_eq!(status.readiness.observed_at_ns, 100);
493 assert!(
494 status
495 .visibility
496 .iter()
497 .any(|entry| entry.field == "topology"
498 && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
499 );
500 }
501
502 #[test]
503 fn runtime_status_classifies_each_top_level_field_visibility() {
504 let status = RuntimeIntrospectionApi::runtime_status_for(
505 Principal::anonymous(),
506 100,
507 "test-canister",
508 "1.2.3",
509 "0.81.0",
510 7,
511 );
512 let expected = [
513 ("schema_version", RuntimeFieldVisibility::PublicSafe),
514 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
515 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
516 ("role", RuntimeFieldVisibility::OperatorOnly),
517 ("root", RuntimeFieldVisibility::OperatorOnly),
518 ("build_network", RuntimeFieldVisibility::OperatorOnly),
519 ("build", RuntimeFieldVisibility::OperatorOnly),
520 ("features", RuntimeFieldVisibility::OperatorOnly),
521 ("topology", RuntimeFieldVisibility::ControllerOnly),
522 ("timers", RuntimeFieldVisibility::OperatorOnly),
523 ("state", RuntimeFieldVisibility::OperatorOnly),
524 ("auth", RuntimeFieldVisibility::OperatorOnly),
525 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
526 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
527 ("readiness", RuntimeFieldVisibility::OperatorOnly),
528 ("status", RuntimeFieldVisibility::OperatorOnly),
529 ("visibility", RuntimeFieldVisibility::OperatorOnly),
530 ];
531
532 assert_eq!(status.visibility.len(), expected.len());
533 for (index, (field, visibility)) in expected.into_iter().enumerate() {
534 assert_eq!(status.visibility[index].field, field);
535 assert_eq!(status.visibility[index].visibility, visibility);
536 }
537 }
538
539 #[test]
540 fn runtime_status_reports_compile_features_deterministically() {
541 let status = RuntimeIntrospectionApi::runtime_status_for(
542 Principal::anonymous(),
543 100,
544 "test-canister",
545 "1.2.3",
546 "0.81.0",
547 7,
548 );
549 assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
550 for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
551 assert_eq!(status.features[index].name, name);
552 assert_eq!(status.features[index].enabled, enabled);
553 assert_eq!(
554 status.features[index].visibility,
555 RuntimeFieldVisibility::OperatorOnly
556 );
557 assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
558 }
559 }
560
561 #[test]
562 fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
563 let status = RuntimeIntrospectionApi::runtime_status_for(
564 Principal::anonymous(),
565 100,
566 "test-canister",
567 "1.2.3",
568 "0.81.0",
569 7,
570 );
571
572 let auth = status.auth.expect("auth feature summary");
573 assert!(
574 auth.auth_features
575 .windows(2)
576 .all(|features| features[0].name <= features[1].name)
577 );
578 assert_runtime_feature(
579 &auth.auth_features,
580 "auth-chain-key-ecdsa",
581 cfg!(feature = "auth-chain-key-ecdsa"),
582 );
583 assert_runtime_feature(
584 &auth.auth_features,
585 "auth-delegated-token-verify",
586 cfg!(feature = "auth-delegated-token-verify"),
587 );
588 assert_runtime_feature(
589 &auth.auth_features,
590 "auth-issuer-canister-sig-create",
591 cfg!(feature = "auth-issuer-canister-sig-create"),
592 );
593
594 if cfg!(any(
595 feature = "blob-storage",
596 feature = "blob-storage-billing"
597 )) {
598 let blob_storage = status.blob_storage.expect("blob-storage feature summary");
599 assert_runtime_feature(
600 &blob_storage.blob_storage_features,
601 "blob-storage",
602 cfg!(feature = "blob-storage"),
603 );
604 assert_runtime_feature(
605 &blob_storage.blob_storage_features,
606 "blob-storage-billing",
607 cfg!(feature = "blob-storage-billing"),
608 );
609 } else {
610 assert!(status.blob_storage.is_none());
611 }
612 }
613
614 fn assert_runtime_feature(
615 features: &[RuntimeFeatureStatus],
616 name: &str,
617 expected_enabled: bool,
618 ) {
619 let feature = features
620 .iter()
621 .find(|feature| feature.name == name)
622 .unwrap_or_else(|| panic!("expected runtime feature {name}"));
623
624 assert_eq!(feature.enabled, expected_enabled);
625 assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
626 assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
627 }
628
629 #[test]
630 fn runtime_status_projects_registered_timer_metrics() {
631 TimerMetrics::reset();
632 TimerMetrics::record_timer_scheduled(
633 TimerMode::Interval,
634 Duration::from_mins(1),
635 "cycles:interval",
636 );
637 TimerMetrics::record_timer_scheduled(
638 TimerMode::Once,
639 Duration::from_secs(1),
640 "auth_renewal:init",
641 );
642 TimerMetrics::record_timer_tick(
643 TimerMode::Once,
644 Duration::from_secs(1),
645 "auth_renewal:init",
646 );
647
648 let status = RuntimeIntrospectionApi::runtime_status_for(
649 Principal::anonymous(),
650 100,
651 "test-canister",
652 "1.2.3",
653 "0.81.0",
654 7,
655 );
656
657 assert_eq!(status.timers.len(), 2);
658 assert_eq!(status.timers[0].subsystem, "auth_renewal");
659 assert_eq!(status.timers[0].name, "init");
660 assert_eq!(status.timers[0].status, TimerStatus::Healthy);
661 assert_eq!(status.timers[1].subsystem, "cycles");
662 assert_eq!(status.timers[1].name, "interval");
663 assert_eq!(status.timers[1].status, TimerStatus::Unknown);
664
665 TimerMetrics::reset();
666 }
667
668 #[test]
669 fn runtime_status_bounds_timer_labels() {
670 TimerMetrics::reset();
671
672 let label = format!("{}\n:{}\n", "subsystem".repeat(12), "timer_name".repeat(16));
673 TimerMetrics::record_timer_scheduled(TimerMode::Once, Duration::from_secs(1), &label);
674
675 let status = RuntimeIntrospectionApi::runtime_status_for(
676 Principal::anonymous(),
677 100,
678 "test-canister",
679 "1.2.3",
680 "0.81.0",
681 7,
682 );
683
684 assert_eq!(status.timers.len(), 1);
685 assert!(status.timers[0].subsystem.len() <= MAX_TIMER_SUBSYSTEM_BYTES);
686 assert!(status.timers[0].name.len() <= MAX_TIMER_NAME_BYTES);
687 assert!(!status.timers[0].subsystem.contains('\n'));
688 assert!(!status.timers[0].name.contains('\n'));
689
690 TimerMetrics::reset();
691 }
692
693 #[test]
694 fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
695 let summary = state_summary_for_memory_ids(
696 Some("root"),
697 &std::collections::BTreeSet::from([
698 crate::role_contract::allocation::memory::env::ENV_ID,
699 ]),
700 )
701 .expect("runtime state declarations");
702
703 assert_eq!(
704 summary.manifest_schema_version,
705 u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
706 );
707 assert!(summary.total_stable_memory_pages.is_none());
708 assert!(summary.domains.iter().any(|domain| {
709 domain.domain == "env"
710 && domain.storage == "stable_memory"
711 && domain.status == RuntimeStateDomainStatus::Ok
712 }));
713 assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
714 }
715
716 #[test]
717 fn runtime_status_includes_recent_failure_snapshot() {
718 RecentFailureOps::reset();
719 RuntimeIntrospectionApi::record_recent_failure(
720 77,
721 "runtime",
722 "readiness_failed",
723 FailureSeverity::Error,
724 "bounded failure summary",
725 Some("runtime-check".to_string()),
726 );
727
728 let status = RuntimeIntrospectionApi::runtime_status_for(
729 Principal::anonymous(),
730 100,
731 "test-canister",
732 "1.2.3",
733 "0.81.0",
734 7,
735 );
736
737 assert_eq!(status.recent_failures.len(), 1);
738 assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
739 assert_eq!(status.recent_failures[0].subsystem, "runtime");
740 assert_eq!(status.recent_failures[0].code, "readiness_failed");
741
742 RecentFailureOps::reset();
743 }
744
745 #[test]
746 fn runtime_status_includes_bootstrap_failure_metadata() {
747 RecentFailureOps::reset();
748 BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
749 BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
750
751 let status = RuntimeIntrospectionApi::runtime_status_for(
752 Principal::anonymous(),
753 100,
754 "test-canister",
755 "1.2.3",
756 "0.81.0",
757 7,
758 );
759
760 let failure = status
761 .recent_failures
762 .iter()
763 .find(|failure| failure.code == "bootstrap_failed")
764 .expect("bootstrap failure metadata");
765
766 assert_eq!(failure.subsystem, "runtime_bootstrap");
767 assert_eq!(failure.severity, FailureSeverity::Error);
768 assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
769 assert!(
770 !failure.summary.contains("raw bootstrap failure detail"),
771 "runtime status recent failures should not mirror raw bootstrap errors"
772 );
773
774 RecentFailureOps::reset();
775 }
776}