Skip to main content

canic_core/api/runtime/
mod.rs

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