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