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
74pub struct MemoryRuntimeApi;
79
80impl MemoryRuntimeApi {
81 pub fn bootstrap_registry() -> Result<(), Error> {
83 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
84
85 Ok(())
86 }
87}
88
89pub struct RuntimeIntrospectionApi;
94
95impl RuntimeIntrospectionApi {
96 #[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 #[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 #[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 #[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}