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