Skip to main content

canic_core/api/runtime/
mod.rs

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