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