Skip to main content

canic_core/api/runtime/
mod.rs

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
73///
74/// MemoryRuntimeApi
75///
76
77pub struct MemoryRuntimeApi;
78
79impl MemoryRuntimeApi {
80    /// Bootstrap Canic's stable-memory declaration snapshot.
81    pub fn bootstrap_registry() -> Result<(), Error> {
82        MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
83
84        Ok(())
85    }
86}
87
88///
89/// RuntimeIntrospectionApi
90///
91
92pub struct RuntimeIntrospectionApi;
93
94impl RuntimeIntrospectionApi {
95    /// Record one heap-only recent-failure summary for guarded runtime status.
96    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    /// Return the minimal health status for a canister that answered the query.
115    #[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    /// Return guarded readiness status for the local Canic role.
134    #[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    /// Return guarded runtime status for the local Canic role.
195    #[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    /// Return guarded runtime status using ambient IC runtime values.
248    #[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}