Skip to main content

canic_core/api/runtime/
mod.rs

1pub mod install;
2
3use crate::{
4    InternalError,
5    cdk::types::Principal,
6    domain::runtime::{
7        FailureSeverity, HealthStatus, ReadinessStatus, RuntimeCheckStatus,
8        RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus, RuntimeStatus,
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, RuntimeReceiptCapacityStatus, RuntimeStateDomainSummary,
17            RuntimeStateSummary, RuntimeTopologyStatus, RuntimeVisibilityEntry,
18        },
19    },
20    ops::{
21        ic::{IcOps, build_network::BuildNetworkOps},
22        runtime::{
23            env::EnvOps,
24            memory::MemoryRegistryOps,
25            ready::ReadyOps,
26            recent_failure::{RecentFailureInput, RecentFailureOps},
27        },
28        storage::intent::{RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, ReceiptBackedIntentOps},
29    },
30    state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
31    workflow::runtime::timer::{TimerRuntimeSnapshot, TimerWorkflow},
32};
33
34const MAX_TIMER_SUBSYSTEM_BYTES: usize = 64;
35const MAX_TIMER_NAME_BYTES: usize = 96;
36const RUNTIME_FEATURE_SOURCE: &str = "compile_feature";
37const RUNTIME_FEATURE_FLAGS: [(&str, bool); 10] = [
38    (
39        "auth-chain-key-ecdsa",
40        cfg!(feature = "auth-chain-key-ecdsa"),
41    ),
42    (
43        "auth-chain-key-root-sign",
44        cfg!(feature = "auth-chain-key-root-sign"),
45    ),
46    (
47        "auth-delegated-token-verify",
48        cfg!(feature = "auth-delegated-token-verify"),
49    ),
50    (
51        "auth-issuer-canister-sig-create",
52        cfg!(feature = "auth-issuer-canister-sig-create"),
53    ),
54    (
55        "auth-issuer-canister-sig-verify",
56        cfg!(feature = "auth-issuer-canister-sig-verify"),
57    ),
58    (
59        "auth-root-canister-sig-create",
60        cfg!(feature = "auth-root-canister-sig-create"),
61    ),
62    (
63        "auth-root-canister-sig-verify",
64        cfg!(feature = "auth-root-canister-sig-verify"),
65    ),
66    ("blob-storage", cfg!(feature = "blob-storage")),
67    (
68        "blob-storage-billing",
69        cfg!(feature = "blob-storage-billing"),
70    ),
71    ("sharding", cfg!(feature = "sharding")),
72];
73
74///
75/// MemoryRuntimeApi
76///
77
78pub struct MemoryRuntimeApi;
79
80impl MemoryRuntimeApi {
81    /// Bootstrap Canic's stable-memory declaration snapshot.
82    pub fn bootstrap_registry() -> Result<(), Error> {
83        MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
84
85        Ok(())
86    }
87}
88
89///
90/// RuntimeIntrospectionApi
91///
92
93pub struct RuntimeIntrospectionApi;
94
95impl RuntimeIntrospectionApi {
96    /// Record one heap-only recent-failure summary for guarded runtime status.
97    pub fn record_recent_failure(
98        occurred_at_ns: u64,
99        subsystem: impl Into<String>,
100        code: impl Into<String>,
101        severity: FailureSeverity,
102        summary: impl Into<String>,
103        correlation_id: Option<String>,
104    ) {
105        RecentFailureOps::record(RecentFailureInput {
106            occurred_at_ns,
107            subsystem: subsystem.into(),
108            code: code.into(),
109            severity,
110            summary: summary.into(),
111            correlation_id,
112        });
113    }
114
115    /// Return the minimal health status for a canister that answered the query.
116    #[must_use]
117    pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
118        CanicHealthStatus {
119            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
120            status: HealthStatus::Healthy,
121            observed_at_ns,
122            checks: vec![RuntimeCheck {
123                category: "health".to_string(),
124                code: "canister_responsive".to_string(),
125                status: RuntimeCheckStatus::Pass,
126                subject: "canister".to_string(),
127                detail: "canister returned a health response".to_string(),
128                next: None,
129                source: "runtime_observed".to_string(),
130            }],
131        }
132    }
133
134    /// Return guarded readiness status for the local Canic role.
135    #[must_use]
136    pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
137        let ready = ReadyOps::is_ready();
138        let role = EnvOps::canister_role()
139            .ok()
140            .map(crate::ids::CanisterRole::into_string);
141
142        let (status, check_status, detail, next) = if ready {
143            (
144                ReadinessStatus::Ready,
145                RuntimeCheckStatus::Pass,
146                "runtime readiness barrier is marked ready",
147                None,
148            )
149        } else {
150            (
151                ReadinessStatus::NotReady,
152                RuntimeCheckStatus::Fail,
153                "runtime readiness barrier is not ready",
154                Some("wait for bootstrap to complete or inspect canic_bootstrap_status"),
155            )
156        };
157
158        let readiness_check = RuntimeCheck {
159            category: "readiness".to_string(),
160            code: "runtime_ready_barrier".to_string(),
161            status: check_status,
162            subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
163            detail: detail.to_string(),
164            next: next.map(str::to_string),
165            source: "runtime_observed".to_string(),
166        };
167
168        let blockers = if ready {
169            Vec::new()
170        } else {
171            vec![RuntimeDiagnostic {
172                category: "readiness".to_string(),
173                code: "runtime_not_ready".to_string(),
174                severity: RuntimeDiagnosticSeverity::Blocked,
175                subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
176                detail: "runtime readiness barrier has not completed".to_string(),
177                next: Some(
178                    "inspect bootstrap status before treating the role as ready".to_string(),
179                ),
180                source: "runtime_observed".to_string(),
181            }]
182        };
183
184        CanicReadinessStatus {
185            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
186            role,
187            status,
188            observed_at_ns,
189            checks: vec![readiness_check],
190            blockers,
191            warnings: Vec::new(),
192        }
193    }
194
195    /// Return guarded runtime status for the local Canic role.
196    #[must_use]
197    pub fn runtime_status_for(
198        canister_id: Principal,
199        observed_at_ns: u64,
200        package_name: &str,
201        package_version: &str,
202        canic_version: &str,
203        canister_version: u64,
204    ) -> CanicRuntimeStatus {
205        let readiness = Self::readiness(observed_at_ns);
206        let role = readiness.role.clone();
207        let state = state_summary(role.as_deref());
208        let root = EnvOps::root_pid().ok();
209        let parent = EnvOps::parent_pid().ok();
210        let subnet = EnvOps::subnet_pid().ok();
211        let receipt_capacity_result = runtime_receipt_capacity();
212        let receipt_capacity_status = receipt_capacity_result
213            .as_ref()
214            .ok()
215            .map(|capacity| capacity.status);
216        let status = aggregate_runtime_status(readiness.status, receipt_capacity_status);
217        let (receipt_capacity, recent_failures) = match receipt_capacity_result {
218            Ok(capacity) => (Some(capacity), RecentFailureOps::snapshot()),
219            Err(err) => {
220                let (class, origin) = err.log_fields();
221                (
222                    None,
223                    RecentFailureOps::snapshot_with(RecentFailureInput {
224                        occurred_at_ns: observed_at_ns,
225                        subsystem: "intent_capacity".to_string(),
226                        code: "receipt_capacity_unavailable".to_string(),
227                        severity: FailureSeverity::Error,
228                        summary: format!("class={class} origin={origin}: {err}"),
229                        correlation_id: None,
230                    }),
231                )
232            }
233        };
234
235        CanicRuntimeStatus {
236            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
237            observed_at_ns,
238            canister_id,
239            role,
240            root,
241            build_network: BuildNetworkOps::build_network(),
242            build: RuntimeBuildInfo {
243                package_name: package_name.to_string(),
244                package_version: package_version.to_string(),
245                canic_version: canic_version.to_string(),
246                canister_version,
247            },
248            features: runtime_features(),
249            topology: Some(RuntimeTopologyStatus {
250                root,
251                parent,
252                subnet,
253                source: "runtime_observed".to_string(),
254            }),
255            timers: timer_statuses(),
256            state,
257            auth: Some(runtime_auth_status()),
258            blob_storage: runtime_blob_storage_status(),
259            receipt_capacity,
260            recent_failures,
261            visibility: runtime_visibility(),
262            readiness,
263            status,
264        }
265    }
266
267    /// Return guarded runtime status using ambient IC runtime values.
268    #[must_use]
269    pub fn runtime_status(
270        observed_at_ns: u64,
271        package_name: &str,
272        package_version: &str,
273        canic_version: &str,
274        canister_version: u64,
275    ) -> CanicRuntimeStatus {
276        Self::runtime_status_for(
277            IcOps::canister_self(),
278            observed_at_ns,
279            package_name,
280            package_version,
281            canic_version,
282            canister_version,
283        )
284    }
285}
286
287fn runtime_features() -> Vec<RuntimeFeatureStatus> {
288    RUNTIME_FEATURE_FLAGS
289        .into_iter()
290        .map(|(name, enabled)| runtime_feature_status(name, enabled))
291        .collect()
292}
293
294fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
295    RuntimeFeatureStatus {
296        name: name.to_string(),
297        enabled,
298        visibility: RuntimeFieldVisibility::OperatorOnly,
299        source: RUNTIME_FEATURE_SOURCE.to_string(),
300    }
301}
302
303fn runtime_auth_status() -> RuntimeAuthStatusSummary {
304    RuntimeAuthStatusSummary {
305        auth_features: RUNTIME_FEATURE_FLAGS
306            .into_iter()
307            .filter(|(name, _)| name.starts_with("auth-"))
308            .map(|(name, enabled)| runtime_feature_status(name, enabled))
309            .collect(),
310    }
311}
312
313fn runtime_receipt_capacity() -> Result<RuntimeReceiptCapacityStatus, InternalError> {
314    let capacity = ReceiptBackedIntentOps::receipt_capacity()?;
315    Ok(RuntimeReceiptCapacityStatus {
316        status: receipt_capacity_condition(
317            capacity.remaining_record_headroom,
318            capacity.remaining_resource_total_headroom,
319        ),
320        receipt_records: capacity.total_records,
321        application_receipt_records: capacity.application_records,
322        canic_owned_receipt_records: capacity.canic_owned_records,
323        pending_application_receipt_records: capacity.pending_records,
324        terminal_application_receipt_records: capacity.terminal_records,
325        receipt_record_limit: capacity.record_limit,
326        remaining_receipt_record_headroom: capacity.remaining_record_headroom,
327        resource_total_records: capacity.resource_total_records,
328        resource_total_record_limit: capacity.resource_total_record_limit,
329        remaining_resource_total_headroom: capacity.remaining_resource_total_headroom,
330        warning_headroom_threshold: RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD,
331        reserved_terminal_slots: capacity.reserved_terminal_slots,
332        reserved_terminal_pages: capacity.reserved_terminal_pages,
333        next_terminal_eligibility_at_ns: capacity.next_eligibility_at_ns,
334        source: "intent_storage".to_string(),
335    })
336}
337
338const fn receipt_capacity_condition(
339    remaining_receipt_records: u64,
340    remaining_resource_totals: u64,
341) -> RuntimeCheckStatus {
342    let minimum_headroom = if remaining_receipt_records < remaining_resource_totals {
343        remaining_receipt_records
344    } else {
345        remaining_resource_totals
346    };
347    if minimum_headroom == 0 {
348        RuntimeCheckStatus::Fail
349    } else if minimum_headroom <= RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD {
350        RuntimeCheckStatus::Warn
351    } else {
352        RuntimeCheckStatus::Pass
353    }
354}
355
356const fn aggregate_runtime_status(
357    readiness: ReadinessStatus,
358    receipt_capacity: Option<RuntimeCheckStatus>,
359) -> RuntimeStatus {
360    if matches!(readiness, ReadinessStatus::NotReady)
361        || matches!(
362            receipt_capacity,
363            None | Some(RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated)
364        )
365    {
366        RuntimeStatus::Failing
367    } else if matches!(
368        readiness,
369        ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated
370    ) || matches!(receipt_capacity, Some(RuntimeCheckStatus::Warn))
371    {
372        RuntimeStatus::Degraded
373    } else {
374        RuntimeStatus::Ok
375    }
376}
377
378fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
379    let blob_storage_enabled = cfg!(feature = "blob-storage");
380    let billing_enabled = cfg!(feature = "blob-storage-billing");
381
382    (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
383        blob_storage_features: [
384            ("blob-storage", blob_storage_enabled),
385            ("blob-storage-billing", billing_enabled),
386        ]
387        .into_iter()
388        .map(|(name, enabled)| runtime_feature_status(name, enabled))
389        .collect(),
390    })
391}
392
393fn timer_statuses() -> Vec<CanicTimerStatus> {
394    timer_statuses_from(TimerWorkflow::statuses())
395}
396
397fn timer_statuses_from(snapshots: Vec<TimerRuntimeSnapshot>) -> Vec<CanicTimerStatus> {
398    let mut timers = snapshots
399        .into_iter()
400        .map(|snapshot| {
401            let (subsystem, name) = split_timer_label(&snapshot.label);
402            CanicTimerStatus {
403                name,
404                subsystem,
405                scheduling_mode: snapshot.scheduling_mode,
406                registration: snapshot.registration,
407                condition: snapshot.condition,
408                enabled: snapshot.enabled,
409                generation: snapshot.generation,
410                next_due_at_ns: snapshot.next_due_at_ns,
411                last_outcome: snapshot.last_outcome,
412                last_work_count: snapshot.last_work_count,
413                last_success_at_ns: snapshot.last_success_at_ns,
414                last_failure_at_ns: snapshot.last_failure_at_ns,
415                consecutive_expected_failures: snapshot.consecutive_expected_failures,
416                schedules_since_runtime_start: snapshot.schedules_since_runtime_start,
417                executions_since_runtime_start: snapshot.executions_since_runtime_start,
418                successes_since_runtime_start: snapshot.successes_since_runtime_start,
419                expected_failures_since_runtime_start: snapshot
420                    .expected_failures_since_runtime_start,
421                invariant_failures_since_runtime_start: snapshot
422                    .invariant_failures_since_runtime_start,
423                stale_callbacks_since_runtime_start: snapshot.stale_callbacks_since_runtime_start,
424            }
425        })
426        .collect::<Vec<_>>();
427    timers.sort_by(|left, right| {
428        left.subsystem
429            .cmp(&right.subsystem)
430            .then_with(|| left.name.cmp(&right.name))
431    });
432    timers
433}
434
435fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
436    let memory_ids = MemoryRegistryOps::ledger_snapshot()
437        .ok()?
438        .memories
439        .into_iter()
440        .map(|memory| memory.memory_manager_id)
441        .collect::<std::collections::BTreeSet<_>>();
442    state_summary_for_memory_ids(role, &memory_ids)
443}
444
445fn state_summary_for_memory_ids(
446    role: Option<&str>,
447    memory_ids: &std::collections::BTreeSet<u8>,
448) -> Option<RuntimeStateSummary> {
449    role?;
450    let mut domains = canic_state_descriptors()
451        .into_iter()
452        .flat_map(|descriptor| descriptor.state)
453        .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
454        .map(|domain| RuntimeStateDomainSummary {
455            domain: domain.domain,
456            version: domain.version,
457            storage: domain.storage.as_str().to_string(),
458            memory_id: domain.memory_id,
459            status: RuntimeStateDomainStatus::Ok,
460        })
461        .collect::<Vec<_>>();
462    domains.sort_by(|left, right| left.domain.cmp(&right.domain));
463
464    if domains.is_empty() {
465        return None;
466    }
467
468    Some(RuntimeStateSummary {
469        manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
470        domains,
471        total_stable_memory_pages: None,
472    })
473}
474
475fn split_timer_label(label: &str) -> (String, String) {
476    label
477        .split_once("::")
478        .or_else(|| label.split_once(':'))
479        .map_or_else(
480            || {
481                (
482                    "runtime".to_string(),
483                    bounded_runtime_text(label, MAX_TIMER_NAME_BYTES),
484                )
485            },
486            |(subsystem, name)| {
487                (
488                    bounded_runtime_text(subsystem, MAX_TIMER_SUBSYSTEM_BYTES),
489                    bounded_runtime_text(name, MAX_TIMER_NAME_BYTES),
490                )
491            },
492        )
493}
494
495fn bounded_runtime_text(value: &str, max_bytes: usize) -> String {
496    let sanitized = value
497        .chars()
498        .map(|character| {
499            if character.is_control() {
500                ' '
501            } else {
502                character
503            }
504        })
505        .collect::<String>();
506
507    if sanitized.len() <= max_bytes {
508        return sanitized;
509    }
510
511    let mut end = 0;
512    for (index, character) in sanitized.char_indices() {
513        let next = index + character.len_utf8();
514        if next > max_bytes {
515            break;
516        }
517        end = next;
518    }
519
520    sanitized[..end].to_string()
521}
522
523fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
524    [
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        ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
539        ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
540        ("readiness", RuntimeFieldVisibility::OperatorOnly),
541        ("status", RuntimeFieldVisibility::OperatorOnly),
542        ("visibility", RuntimeFieldVisibility::OperatorOnly),
543    ]
544    .into_iter()
545    .map(|(field, visibility)| RuntimeVisibilityEntry {
546        field: field.to_string(),
547        visibility,
548    })
549    .collect()
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::domain::runtime::{
556        TimerExecutionOutcome, TimerProcessCondition, TimerRegistrationStatus, TimerSchedulingMode,
557    };
558    use crate::ids::IntentResourceKey;
559    use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
560    use crate::ops::runtime::recent_failure::RecentFailureOps;
561    use crate::ops::storage::intent::{
562        INTENT_RESOURCE_TOTAL_RECORD_LIMIT, IntentStoreOps, RECEIPT_BACKED_INTENT_RECORD_LIMIT,
563    };
564    use crate::storage::stable::intent::{IntentResourceTotalsRecord, IntentStore};
565
566    #[test]
567    fn health_is_minimal_and_schema_versioned() {
568        let health = RuntimeIntrospectionApi::health(Some(42));
569
570        assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
571        assert_eq!(health.status, HealthStatus::Healthy);
572        assert_eq!(health.observed_at_ns, Some(42));
573        assert_eq!(health.checks.len(), 1);
574        assert_eq!(health.checks[0].code, "canister_responsive");
575    }
576
577    #[test]
578    fn runtime_status_embeds_guarded_readiness_and_build_info() {
579        let status = RuntimeIntrospectionApi::runtime_status_for(
580            Principal::anonymous(),
581            100,
582            "test-canister",
583            "1.2.3",
584            "0.81.0",
585            7,
586        );
587
588        assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
589        assert_eq!(status.observed_at_ns, 100);
590        assert_eq!(status.canister_id, Principal::anonymous());
591        assert_eq!(status.build_network, BuildNetworkOps::build_network());
592        assert_eq!(status.build.package_name, "test-canister");
593        assert_eq!(status.build.package_version, "1.2.3");
594        assert_eq!(status.build.canic_version, "0.81.0");
595        assert_eq!(status.build.canister_version, 7);
596        assert_eq!(status.readiness.observed_at_ns, 100);
597        assert!(
598            status
599                .visibility
600                .iter()
601                .any(|entry| entry.field == "topology"
602                    && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
603        );
604    }
605
606    #[test]
607    fn runtime_status_classifies_each_top_level_field_visibility() {
608        let status = RuntimeIntrospectionApi::runtime_status_for(
609            Principal::anonymous(),
610            100,
611            "test-canister",
612            "1.2.3",
613            "0.81.0",
614            7,
615        );
616        let expected = [
617            ("schema_version", RuntimeFieldVisibility::PublicSafe),
618            ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
619            ("canister_id", RuntimeFieldVisibility::OperatorOnly),
620            ("role", RuntimeFieldVisibility::OperatorOnly),
621            ("root", RuntimeFieldVisibility::OperatorOnly),
622            ("build_network", RuntimeFieldVisibility::OperatorOnly),
623            ("build", RuntimeFieldVisibility::OperatorOnly),
624            ("features", RuntimeFieldVisibility::OperatorOnly),
625            ("topology", RuntimeFieldVisibility::ControllerOnly),
626            ("timers", RuntimeFieldVisibility::OperatorOnly),
627            ("state", RuntimeFieldVisibility::OperatorOnly),
628            ("auth", RuntimeFieldVisibility::OperatorOnly),
629            ("blob_storage", RuntimeFieldVisibility::FeatureGated),
630            ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
631            ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
632            ("readiness", RuntimeFieldVisibility::OperatorOnly),
633            ("status", RuntimeFieldVisibility::OperatorOnly),
634            ("visibility", RuntimeFieldVisibility::OperatorOnly),
635        ];
636
637        assert_eq!(status.visibility.len(), expected.len());
638        for (index, (field, visibility)) in expected.into_iter().enumerate() {
639            assert_eq!(status.visibility[index].field, field);
640            assert_eq!(status.visibility[index].visibility, visibility);
641        }
642    }
643
644    #[test]
645    fn runtime_status_projects_empty_receipt_capacity() {
646        IntentStoreOps::reset_for_tests();
647
648        let status = RuntimeIntrospectionApi::runtime_status_for(
649            Principal::anonymous(),
650            100,
651            "test-canister",
652            "1.2.3",
653            "0.96.6",
654            7,
655        );
656        let capacity = status.receipt_capacity.expect("receipt capacity");
657
658        assert_eq!(capacity.status, RuntimeCheckStatus::Pass);
659        assert_eq!(capacity.receipt_records, 0);
660        assert_eq!(
661            capacity.receipt_record_limit,
662            RECEIPT_BACKED_INTENT_RECORD_LIMIT
663        );
664        assert_eq!(capacity.resource_total_records, 0);
665        assert_eq!(
666            capacity.resource_total_record_limit,
667            INTENT_RESOURCE_TOTAL_RECORD_LIMIT
668        );
669        assert_eq!(
670            capacity.warning_headroom_threshold,
671            RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD
672        );
673        assert_eq!(capacity.source, "intent_storage");
674    }
675
676    #[test]
677    fn receipt_capacity_condition_has_exact_warning_and_failure_boundaries() {
678        assert_eq!(
679            receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD + 1, u64::MAX,),
680            RuntimeCheckStatus::Pass
681        );
682        assert_eq!(
683            receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, u64::MAX),
684            RuntimeCheckStatus::Warn
685        );
686        assert_eq!(
687            receipt_capacity_condition(u64::MAX, 1),
688            RuntimeCheckStatus::Warn
689        );
690        assert_eq!(
691            receipt_capacity_condition(u64::MAX, 0),
692            RuntimeCheckStatus::Fail
693        );
694        assert_eq!(
695            aggregate_runtime_status(ReadinessStatus::Ready, Some(RuntimeCheckStatus::Warn)),
696            RuntimeStatus::Degraded
697        );
698        assert_eq!(
699            aggregate_runtime_status(ReadinessStatus::Ready, None),
700            RuntimeStatus::Failing
701        );
702    }
703
704    #[test]
705    fn runtime_status_fails_closed_with_typed_capacity_diagnostic() {
706        IntentStoreOps::reset_for_tests();
707        RecentFailureOps::reset();
708        for value in 0..=INTENT_RESOURCE_TOTAL_RECORD_LIMIT {
709            IntentStore::set_totals(
710                IntentResourceKey::new(format!("runtime-capacity:{value}")),
711                IntentResourceTotalsRecord {
712                    reserved_qty: 0,
713                    committed_qty: 1,
714                    pending_count: 0,
715                },
716            );
717        }
718
719        let status = RuntimeIntrospectionApi::runtime_status_for(
720            Principal::anonymous(),
721            100,
722            "test-canister",
723            "1.2.3",
724            "0.96.6",
725            7,
726        );
727
728        assert_eq!(status.status, RuntimeStatus::Failing);
729        assert!(status.receipt_capacity.is_none());
730        let failure = status
731            .recent_failures
732            .first()
733            .expect("current capacity failure diagnostic");
734        assert_eq!(failure.subsystem, "intent_capacity");
735        assert_eq!(failure.code, "receipt_capacity_unavailable");
736        assert_eq!(failure.severity, FailureSeverity::Error);
737        assert!(
738            failure
739                .summary
740                .contains("resource-total record limit exceeded")
741        );
742        assert!(RecentFailureOps::snapshot().is_empty());
743
744        IntentStoreOps::reset_for_tests();
745        RecentFailureOps::reset();
746    }
747
748    #[test]
749    fn runtime_status_reports_compile_features_deterministically() {
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        assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
759        for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
760            assert_eq!(status.features[index].name, name);
761            assert_eq!(status.features[index].enabled, enabled);
762            assert_eq!(
763                status.features[index].visibility,
764                RuntimeFieldVisibility::OperatorOnly
765            );
766            assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
767        }
768    }
769
770    #[test]
771    fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
772        let status = RuntimeIntrospectionApi::runtime_status_for(
773            Principal::anonymous(),
774            100,
775            "test-canister",
776            "1.2.3",
777            "0.81.0",
778            7,
779        );
780
781        let auth = status.auth.expect("auth feature summary");
782        assert!(
783            auth.auth_features
784                .windows(2)
785                .all(|features| features[0].name <= features[1].name)
786        );
787        assert_runtime_feature(
788            &auth.auth_features,
789            "auth-chain-key-ecdsa",
790            cfg!(feature = "auth-chain-key-ecdsa"),
791        );
792        assert_runtime_feature(
793            &auth.auth_features,
794            "auth-delegated-token-verify",
795            cfg!(feature = "auth-delegated-token-verify"),
796        );
797        assert_runtime_feature(
798            &auth.auth_features,
799            "auth-issuer-canister-sig-create",
800            cfg!(feature = "auth-issuer-canister-sig-create"),
801        );
802
803        if cfg!(any(
804            feature = "blob-storage",
805            feature = "blob-storage-billing"
806        )) {
807            let blob_storage = status.blob_storage.expect("blob-storage feature summary");
808            assert_runtime_feature(
809                &blob_storage.blob_storage_features,
810                "blob-storage",
811                cfg!(feature = "blob-storage"),
812            );
813            assert_runtime_feature(
814                &blob_storage.blob_storage_features,
815                "blob-storage-billing",
816                cfg!(feature = "blob-storage-billing"),
817            );
818        } else {
819            assert!(status.blob_storage.is_none());
820        }
821    }
822
823    fn assert_runtime_feature(
824        features: &[RuntimeFeatureStatus],
825        name: &str,
826        expected_enabled: bool,
827    ) {
828        let feature = features
829            .iter()
830            .find(|feature| feature.name == name)
831            .unwrap_or_else(|| panic!("expected runtime feature {name}"));
832
833        assert_eq!(feature.enabled, expected_enabled);
834        assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
835        assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
836    }
837
838    #[test]
839    fn runtime_status_projects_live_registration_and_process_condition() {
840        let statuses = timer_statuses_from(vec![timer_snapshot("cycles:topup")]);
841
842        assert_eq!(statuses.len(), 1);
843        assert_eq!(statuses[0].subsystem, "cycles");
844        assert_eq!(statuses[0].name, "topup");
845        assert_eq!(statuses[0].registration, TimerRegistrationStatus::Scheduled);
846        assert_eq!(statuses[0].condition, TimerProcessCondition::Active);
847        assert_eq!(
848            statuses[0].last_outcome,
849            Some(TimerExecutionOutcome::Success)
850        );
851        assert_eq!(statuses[0].schedules_since_runtime_start, 3);
852    }
853
854    #[test]
855    fn runtime_status_bounds_timer_labels() {
856        let label = format!("{}\n:{}\n", "subsystem".repeat(12), "timer_name".repeat(16));
857        let status = timer_statuses_from(vec![timer_snapshot(&label)]);
858
859        assert_eq!(status.len(), 1);
860        assert!(status[0].subsystem.len() <= MAX_TIMER_SUBSYSTEM_BYTES);
861        assert!(status[0].name.len() <= MAX_TIMER_NAME_BYTES);
862        assert!(!status[0].subsystem.contains('\n'));
863        assert!(!status[0].name.contains('\n'));
864    }
865
866    fn timer_snapshot(label: &str) -> TimerRuntimeSnapshot {
867        TimerRuntimeSnapshot {
868            label: label.to_string(),
869            scheduling_mode: TimerSchedulingMode::AfterCompletion,
870            registration: TimerRegistrationStatus::Scheduled,
871            condition: TimerProcessCondition::Active,
872            enabled: true,
873            generation: 4,
874            next_due_at_ns: Some(500),
875            last_outcome: Some(TimerExecutionOutcome::Success),
876            last_work_count: 2,
877            last_success_at_ns: Some(400),
878            last_failure_at_ns: None,
879            consecutive_expected_failures: 0,
880            schedules_since_runtime_start: 3,
881            executions_since_runtime_start: 2,
882            successes_since_runtime_start: 2,
883            expected_failures_since_runtime_start: 0,
884            invariant_failures_since_runtime_start: 0,
885            stale_callbacks_since_runtime_start: 0,
886        }
887    }
888
889    #[test]
890    fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
891        let summary = state_summary_for_memory_ids(
892            Some("root"),
893            &std::collections::BTreeSet::from([
894                crate::role_contract::allocation::memory::env::ENV_ID,
895            ]),
896        )
897        .expect("runtime state declarations");
898
899        assert_eq!(
900            summary.manifest_schema_version,
901            u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
902        );
903        assert!(summary.total_stable_memory_pages.is_none());
904        assert!(summary.domains.iter().any(|domain| {
905            domain.domain == "env"
906                && domain.storage == "stable_memory"
907                && domain.status == RuntimeStateDomainStatus::Ok
908        }));
909        assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
910    }
911
912    #[test]
913    fn runtime_status_includes_recent_failure_snapshot() {
914        RecentFailureOps::reset();
915        RuntimeIntrospectionApi::record_recent_failure(
916            77,
917            "runtime",
918            "readiness_failed",
919            FailureSeverity::Error,
920            "bounded failure summary",
921            Some("runtime-check".to_string()),
922        );
923
924        let status = RuntimeIntrospectionApi::runtime_status_for(
925            Principal::anonymous(),
926            100,
927            "test-canister",
928            "1.2.3",
929            "0.81.0",
930            7,
931        );
932
933        assert_eq!(status.recent_failures.len(), 1);
934        assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
935        assert_eq!(status.recent_failures[0].subsystem, "runtime");
936        assert_eq!(status.recent_failures[0].code, "readiness_failed");
937
938        RecentFailureOps::reset();
939    }
940
941    #[test]
942    fn runtime_status_includes_bootstrap_failure_metadata() {
943        RecentFailureOps::reset();
944        BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
945        BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
946
947        let status = RuntimeIntrospectionApi::runtime_status_for(
948            Principal::anonymous(),
949            100,
950            "test-canister",
951            "1.2.3",
952            "0.81.0",
953            7,
954        );
955
956        let failure = status
957            .recent_failures
958            .iter()
959            .find(|failure| failure.code == "bootstrap_failed")
960            .expect("bootstrap failure metadata");
961
962        assert_eq!(failure.subsystem, "runtime_bootstrap");
963        assert_eq!(failure.severity, FailureSeverity::Error);
964        assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
965        assert!(
966            !failure.summary.contains("raw bootstrap failure detail"),
967            "runtime status recent failures should not mirror raw bootstrap errors"
968        );
969
970        RecentFailureOps::reset();
971    }
972}