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