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
78pub struct MemoryRuntimeApi;
83
84impl MemoryRuntimeApi {
85 pub fn bootstrap_registry() -> Result<(), Error> {
87 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
88
89 Ok(())
90 }
91}
92
93pub struct RuntimeIntrospectionApi;
98
99impl RuntimeIntrospectionApi {
100 #[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 #[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 #[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 #[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}