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