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