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, CanicTimerStatus,
14 RUNTIME_INTROSPECTION_SCHEMA_VERSION, RuntimeAuthStatusSummary,
15 RuntimeBlobStorageStatusSummary, RuntimeBuildInfo, RuntimeCheck, RuntimeDiagnostic,
16 RuntimeFeatureStatus, RuntimeReceiptCapacityStatus, RuntimeStateDomainSummary,
17 RuntimeStateSummary, RuntimeTopologyStatus, RuntimeVisibilityEntry,
18 },
19 },
20 ops::{
21 ic::{IcOps, build_network::BuildNetworkOps},
22 runtime::{
23 env::EnvOps,
24 memory::MemoryRegistryOps,
25 ready::ReadyOps,
26 recent_failure::{RecentFailureInput, RecentFailureOps},
27 },
28 storage::intent::{RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, ReceiptBackedIntentOps},
29 },
30 state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
31 workflow::runtime::timer::{TimerRuntimeSnapshot, TimerWorkflow},
32};
33
34const MAX_TIMER_SUBSYSTEM_BYTES: usize = 64;
35const MAX_TIMER_NAME_BYTES: usize = 96;
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_bootstrap_status"),
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 status = aggregate_runtime_status(readiness.status, receipt_capacity_status);
198 let (receipt_capacity, recent_failures) = match receipt_capacity_result {
199 Ok(capacity) => (Some(capacity), RecentFailureOps::snapshot()),
200 Err(err) => {
201 let (class, origin) = err.log_fields();
202 (
203 None,
204 RecentFailureOps::snapshot_with(RecentFailureInput {
205 occurred_at_ns: observed_at_ns,
206 subsystem: "intent_capacity".to_string(),
207 code: "receipt_capacity_unavailable".to_string(),
208 severity: FailureSeverity::Error,
209 summary: format!("class={class} origin={origin}: {err}"),
210 correlation_id: None,
211 }),
212 )
213 }
214 };
215
216 CanicRuntimeStatus {
217 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
218 observed_at_ns,
219 canister_id,
220 role,
221 root,
222 build_network: BuildNetworkOps::build_network(),
223 build: RuntimeBuildInfo {
224 package_name: package_name.to_string(),
225 package_version: package_version.to_string(),
226 canic_version: canic_version.to_string(),
227 canister_version,
228 },
229 features: runtime_features(),
230 topology: Some(RuntimeTopologyStatus {
231 root,
232 parent,
233 subnet,
234 source: "runtime_observed".to_string(),
235 }),
236 timers: timer_statuses(),
237 state,
238 auth: Some(runtime_auth_status()),
239 blob_storage: runtime_blob_storage_status(),
240 receipt_capacity,
241 recent_failures,
242 visibility: runtime_visibility(),
243 readiness,
244 status,
245 }
246 }
247
248 #[must_use]
250 pub fn runtime_status(
251 observed_at_ns: u64,
252 package_name: &str,
253 package_version: &str,
254 canic_version: &str,
255 canister_version: u64,
256 ) -> CanicRuntimeStatus {
257 Self::runtime_status_for(
258 IcOps::canister_self(),
259 observed_at_ns,
260 package_name,
261 package_version,
262 canic_version,
263 canister_version,
264 )
265 }
266}
267
268fn runtime_features() -> Vec<RuntimeFeatureStatus> {
269 RUNTIME_FEATURE_FLAGS
270 .into_iter()
271 .map(|(name, enabled)| runtime_feature_status(name, enabled))
272 .collect()
273}
274
275fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
276 RuntimeFeatureStatus {
277 name: name.to_string(),
278 enabled,
279 visibility: RuntimeFieldVisibility::OperatorOnly,
280 source: RUNTIME_FEATURE_SOURCE.to_string(),
281 }
282}
283
284fn runtime_auth_status() -> RuntimeAuthStatusSummary {
285 RuntimeAuthStatusSummary {
286 auth_features: RUNTIME_FEATURE_FLAGS
287 .into_iter()
288 .filter(|(name, _)| name.starts_with("auth-"))
289 .map(|(name, enabled)| runtime_feature_status(name, enabled))
290 .collect(),
291 }
292}
293
294fn runtime_receipt_capacity() -> Result<RuntimeReceiptCapacityStatus, InternalError> {
295 let capacity = ReceiptBackedIntentOps::receipt_capacity()?;
296 Ok(RuntimeReceiptCapacityStatus {
297 status: receipt_capacity_condition(
298 capacity.remaining_record_headroom,
299 capacity.remaining_resource_total_headroom,
300 ),
301 receipt_records: capacity.total_records,
302 application_receipt_records: capacity.application_records,
303 canic_owned_receipt_records: capacity.canic_owned_records,
304 pending_application_receipt_records: capacity.pending_records,
305 terminal_application_receipt_records: capacity.terminal_records,
306 receipt_record_limit: capacity.record_limit,
307 remaining_receipt_record_headroom: capacity.remaining_record_headroom,
308 resource_total_records: capacity.resource_total_records,
309 resource_total_record_limit: capacity.resource_total_record_limit,
310 remaining_resource_total_headroom: capacity.remaining_resource_total_headroom,
311 warning_headroom_threshold: RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD,
312 reserved_terminal_slots: capacity.reserved_terminal_slots,
313 reserved_terminal_pages: capacity.reserved_terminal_pages,
314 next_terminal_eligibility_at_ns: capacity.next_eligibility_at_ns,
315 source: "intent_storage".to_string(),
316 })
317}
318
319const fn receipt_capacity_condition(
320 remaining_receipt_records: u64,
321 remaining_resource_totals: u64,
322) -> RuntimeCheckStatus {
323 let minimum_headroom = if remaining_receipt_records < remaining_resource_totals {
324 remaining_receipt_records
325 } else {
326 remaining_resource_totals
327 };
328 if minimum_headroom == 0 {
329 RuntimeCheckStatus::Fail
330 } else if minimum_headroom <= RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD {
331 RuntimeCheckStatus::Warn
332 } else {
333 RuntimeCheckStatus::Pass
334 }
335}
336
337const fn aggregate_runtime_status(
338 readiness: ReadinessStatus,
339 receipt_capacity: Option<RuntimeCheckStatus>,
340) -> RuntimeStatus {
341 if matches!(readiness, ReadinessStatus::NotReady)
342 || matches!(
343 receipt_capacity,
344 None | Some(RuntimeCheckStatus::Fail | RuntimeCheckStatus::NotEvaluated)
345 )
346 {
347 RuntimeStatus::Failing
348 } else if matches!(
349 readiness,
350 ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated
351 ) || matches!(receipt_capacity, Some(RuntimeCheckStatus::Warn))
352 {
353 RuntimeStatus::Degraded
354 } else {
355 RuntimeStatus::Ok
356 }
357}
358
359fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
360 let blob_storage_enabled = cfg!(feature = "blob-storage");
361 let billing_enabled = cfg!(feature = "blob-storage-billing");
362
363 (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
364 blob_storage_features: [
365 ("blob-storage", blob_storage_enabled),
366 ("blob-storage-billing", billing_enabled),
367 ]
368 .into_iter()
369 .map(|(name, enabled)| runtime_feature_status(name, enabled))
370 .collect(),
371 })
372}
373
374fn timer_statuses() -> Vec<CanicTimerStatus> {
375 timer_statuses_from(TimerWorkflow::statuses())
376}
377
378fn timer_statuses_from(snapshots: Vec<TimerRuntimeSnapshot>) -> Vec<CanicTimerStatus> {
379 let mut timers = snapshots
380 .into_iter()
381 .map(|snapshot| {
382 let (subsystem, name) = split_timer_label(&snapshot.label);
383 CanicTimerStatus {
384 name,
385 subsystem,
386 scheduling_mode: snapshot.scheduling_mode,
387 registration: snapshot.registration,
388 condition: snapshot.condition,
389 enabled: snapshot.enabled,
390 generation: snapshot.generation,
391 next_due_at_ns: snapshot.next_due_at_ns,
392 last_outcome: snapshot.last_outcome,
393 last_work_count: snapshot.last_work_count,
394 last_success_at_ns: snapshot.last_success_at_ns,
395 last_failure_at_ns: snapshot.last_failure_at_ns,
396 consecutive_expected_failures: snapshot.consecutive_expected_failures,
397 schedules_since_runtime_start: snapshot.schedules_since_runtime_start,
398 executions_since_runtime_start: snapshot.executions_since_runtime_start,
399 successes_since_runtime_start: snapshot.successes_since_runtime_start,
400 expected_failures_since_runtime_start: snapshot
401 .expected_failures_since_runtime_start,
402 invariant_failures_since_runtime_start: snapshot
403 .invariant_failures_since_runtime_start,
404 stale_callbacks_since_runtime_start: snapshot.stale_callbacks_since_runtime_start,
405 }
406 })
407 .collect::<Vec<_>>();
408 timers.sort_by(|left, right| {
409 left.subsystem
410 .cmp(&right.subsystem)
411 .then_with(|| left.name.cmp(&right.name))
412 });
413 timers
414}
415
416fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
417 let memory_ids = MemoryRegistryOps::ledger_snapshot()
418 .ok()?
419 .memories
420 .into_iter()
421 .map(|memory| memory.memory_manager_id)
422 .collect::<std::collections::BTreeSet<_>>();
423 state_summary_for_memory_ids(role, &memory_ids)
424}
425
426fn state_summary_for_memory_ids(
427 role: Option<&str>,
428 memory_ids: &std::collections::BTreeSet<u8>,
429) -> Option<RuntimeStateSummary> {
430 role?;
431 let mut domains = canic_state_descriptors()
432 .into_iter()
433 .flat_map(|descriptor| descriptor.state)
434 .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
435 .map(|domain| RuntimeStateDomainSummary {
436 domain: domain.domain,
437 version: domain.version,
438 storage: domain.storage.as_str().to_string(),
439 memory_id: domain.memory_id,
440 status: RuntimeStateDomainStatus::Ok,
441 })
442 .collect::<Vec<_>>();
443 domains.sort_by(|left, right| left.domain.cmp(&right.domain));
444
445 if domains.is_empty() {
446 return None;
447 }
448
449 Some(RuntimeStateSummary {
450 manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
451 domains,
452 total_stable_memory_pages: None,
453 })
454}
455
456fn split_timer_label(label: &str) -> (String, String) {
457 label
458 .split_once("::")
459 .or_else(|| label.split_once(':'))
460 .map_or_else(
461 || {
462 (
463 "runtime".to_string(),
464 bounded_runtime_text(label, MAX_TIMER_NAME_BYTES),
465 )
466 },
467 |(subsystem, name)| {
468 (
469 bounded_runtime_text(subsystem, MAX_TIMER_SUBSYSTEM_BYTES),
470 bounded_runtime_text(name, MAX_TIMER_NAME_BYTES),
471 )
472 },
473 )
474}
475
476fn bounded_runtime_text(value: &str, max_bytes: usize) -> String {
477 let sanitized = value
478 .chars()
479 .map(|character| {
480 if character.is_control() {
481 ' '
482 } else {
483 character
484 }
485 })
486 .collect::<String>();
487
488 if sanitized.len() <= max_bytes {
489 return sanitized;
490 }
491
492 let mut end = 0;
493 for (index, character) in sanitized.char_indices() {
494 let next = index + character.len_utf8();
495 if next > max_bytes {
496 break;
497 }
498 end = next;
499 }
500
501 sanitized[..end].to_string()
502}
503
504fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
505 [
506 ("schema_version", RuntimeFieldVisibility::PublicSafe),
507 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
508 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
509 ("role", RuntimeFieldVisibility::OperatorOnly),
510 ("root", RuntimeFieldVisibility::OperatorOnly),
511 ("build_network", RuntimeFieldVisibility::OperatorOnly),
512 ("build", RuntimeFieldVisibility::OperatorOnly),
513 ("features", RuntimeFieldVisibility::OperatorOnly),
514 ("topology", RuntimeFieldVisibility::ControllerOnly),
515 ("timers", RuntimeFieldVisibility::OperatorOnly),
516 ("state", RuntimeFieldVisibility::OperatorOnly),
517 ("auth", RuntimeFieldVisibility::OperatorOnly),
518 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
519 ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
520 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
521 ("readiness", RuntimeFieldVisibility::OperatorOnly),
522 ("status", RuntimeFieldVisibility::OperatorOnly),
523 ("visibility", RuntimeFieldVisibility::OperatorOnly),
524 ]
525 .into_iter()
526 .map(|(field, visibility)| RuntimeVisibilityEntry {
527 field: field.to_string(),
528 visibility,
529 })
530 .collect()
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use crate::domain::runtime::{
537 TimerExecutionOutcome, TimerProcessCondition, TimerRegistrationStatus, TimerSchedulingMode,
538 };
539 use crate::ids::IntentResourceKey;
540 use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
541 use crate::ops::runtime::recent_failure::RecentFailureOps;
542 use crate::ops::storage::intent::{
543 INTENT_RESOURCE_TOTAL_RECORD_LIMIT, IntentStoreOps, RECEIPT_BACKED_INTENT_RECORD_LIMIT,
544 };
545 use crate::storage::stable::intent::{IntentResourceTotalsRecord, IntentStore};
546
547 #[test]
548 fn health_is_minimal_and_schema_versioned() {
549 let health = RuntimeIntrospectionApi::health(Some(42));
550
551 assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
552 assert_eq!(health.status, HealthStatus::Healthy);
553 assert_eq!(health.observed_at_ns, Some(42));
554 assert_eq!(health.checks.len(), 1);
555 assert_eq!(health.checks[0].code, "canister_responsive");
556 }
557
558 #[test]
559 fn runtime_status_embeds_guarded_readiness_and_build_info() {
560 let status = RuntimeIntrospectionApi::runtime_status_for(
561 Principal::anonymous(),
562 100,
563 "test-canister",
564 "1.2.3",
565 "0.81.0",
566 7,
567 );
568
569 assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
570 assert_eq!(status.observed_at_ns, 100);
571 assert_eq!(status.canister_id, Principal::anonymous());
572 assert_eq!(status.build_network, BuildNetworkOps::build_network());
573 assert_eq!(status.build.package_name, "test-canister");
574 assert_eq!(status.build.package_version, "1.2.3");
575 assert_eq!(status.build.canic_version, "0.81.0");
576 assert_eq!(status.build.canister_version, 7);
577 assert_eq!(status.readiness.observed_at_ns, 100);
578 assert!(
579 status
580 .visibility
581 .iter()
582 .any(|entry| entry.field == "topology"
583 && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
584 );
585 }
586
587 #[test]
588 fn runtime_status_classifies_each_top_level_field_visibility() {
589 let status = RuntimeIntrospectionApi::runtime_status_for(
590 Principal::anonymous(),
591 100,
592 "test-canister",
593 "1.2.3",
594 "0.81.0",
595 7,
596 );
597 let expected = [
598 ("schema_version", RuntimeFieldVisibility::PublicSafe),
599 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
600 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
601 ("role", RuntimeFieldVisibility::OperatorOnly),
602 ("root", RuntimeFieldVisibility::OperatorOnly),
603 ("build_network", RuntimeFieldVisibility::OperatorOnly),
604 ("build", RuntimeFieldVisibility::OperatorOnly),
605 ("features", RuntimeFieldVisibility::OperatorOnly),
606 ("topology", RuntimeFieldVisibility::ControllerOnly),
607 ("timers", RuntimeFieldVisibility::OperatorOnly),
608 ("state", RuntimeFieldVisibility::OperatorOnly),
609 ("auth", RuntimeFieldVisibility::OperatorOnly),
610 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
611 ("receipt_capacity", RuntimeFieldVisibility::OperatorOnly),
612 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
613 ("readiness", RuntimeFieldVisibility::OperatorOnly),
614 ("status", RuntimeFieldVisibility::OperatorOnly),
615 ("visibility", RuntimeFieldVisibility::OperatorOnly),
616 ];
617
618 assert_eq!(status.visibility.len(), expected.len());
619 for (index, (field, visibility)) in expected.into_iter().enumerate() {
620 assert_eq!(status.visibility[index].field, field);
621 assert_eq!(status.visibility[index].visibility, visibility);
622 }
623 }
624
625 #[test]
626 fn runtime_status_projects_empty_receipt_capacity() {
627 IntentStoreOps::reset_for_tests();
628
629 let status = RuntimeIntrospectionApi::runtime_status_for(
630 Principal::anonymous(),
631 100,
632 "test-canister",
633 "1.2.3",
634 "0.96.6",
635 7,
636 );
637 let capacity = status.receipt_capacity.expect("receipt capacity");
638
639 assert_eq!(capacity.status, RuntimeCheckStatus::Pass);
640 assert_eq!(capacity.receipt_records, 0);
641 assert_eq!(
642 capacity.receipt_record_limit,
643 RECEIPT_BACKED_INTENT_RECORD_LIMIT
644 );
645 assert_eq!(capacity.resource_total_records, 0);
646 assert_eq!(
647 capacity.resource_total_record_limit,
648 INTENT_RESOURCE_TOTAL_RECORD_LIMIT
649 );
650 assert_eq!(
651 capacity.warning_headroom_threshold,
652 RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD
653 );
654 assert_eq!(capacity.source, "intent_storage");
655 }
656
657 #[test]
658 fn receipt_capacity_condition_has_exact_warning_and_failure_boundaries() {
659 assert_eq!(
660 receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD + 1, u64::MAX,),
661 RuntimeCheckStatus::Pass
662 );
663 assert_eq!(
664 receipt_capacity_condition(RECEIPT_CAPACITY_WARNING_HEADROOM_THRESHOLD, u64::MAX),
665 RuntimeCheckStatus::Warn
666 );
667 assert_eq!(
668 receipt_capacity_condition(u64::MAX, 1),
669 RuntimeCheckStatus::Warn
670 );
671 assert_eq!(
672 receipt_capacity_condition(u64::MAX, 0),
673 RuntimeCheckStatus::Fail
674 );
675 assert_eq!(
676 aggregate_runtime_status(ReadinessStatus::Ready, Some(RuntimeCheckStatus::Warn)),
677 RuntimeStatus::Degraded
678 );
679 assert_eq!(
680 aggregate_runtime_status(ReadinessStatus::Ready, None),
681 RuntimeStatus::Failing
682 );
683 }
684
685 #[test]
686 fn runtime_status_fails_closed_with_typed_capacity_diagnostic() {
687 IntentStoreOps::reset_for_tests();
688 RecentFailureOps::reset();
689 for value in 0..=INTENT_RESOURCE_TOTAL_RECORD_LIMIT {
690 IntentStore::set_totals(
691 IntentResourceKey::new(format!("runtime-capacity:{value}")),
692 IntentResourceTotalsRecord {
693 reserved_qty: 0,
694 committed_qty: 1,
695 pending_count: 0,
696 },
697 );
698 }
699
700 let status = RuntimeIntrospectionApi::runtime_status_for(
701 Principal::anonymous(),
702 100,
703 "test-canister",
704 "1.2.3",
705 "0.96.6",
706 7,
707 );
708
709 assert_eq!(status.status, RuntimeStatus::Failing);
710 assert!(status.receipt_capacity.is_none());
711 let failure = status
712 .recent_failures
713 .first()
714 .expect("current capacity failure diagnostic");
715 assert_eq!(failure.subsystem, "intent_capacity");
716 assert_eq!(failure.code, "receipt_capacity_unavailable");
717 assert_eq!(failure.severity, FailureSeverity::Error);
718 assert!(
719 failure
720 .summary
721 .contains("resource-total record limit exceeded")
722 );
723 assert!(RecentFailureOps::snapshot().is_empty());
724
725 IntentStoreOps::reset_for_tests();
726 RecentFailureOps::reset();
727 }
728
729 #[test]
730 fn runtime_status_reports_compile_features_deterministically() {
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 assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
740 for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
741 assert_eq!(status.features[index].name, name);
742 assert_eq!(status.features[index].enabled, enabled);
743 assert_eq!(
744 status.features[index].visibility,
745 RuntimeFieldVisibility::OperatorOnly
746 );
747 assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
748 }
749 }
750
751 #[test]
752 fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
753 let status = RuntimeIntrospectionApi::runtime_status_for(
754 Principal::anonymous(),
755 100,
756 "test-canister",
757 "1.2.3",
758 "0.81.0",
759 7,
760 );
761
762 let auth = status.auth.expect("auth feature summary");
763 assert!(
764 auth.auth_features
765 .windows(2)
766 .all(|features| features[0].name <= features[1].name)
767 );
768 assert_runtime_feature(
769 &auth.auth_features,
770 "auth-chain-key-ecdsa",
771 cfg!(feature = "auth-chain-key-ecdsa"),
772 );
773 assert_runtime_feature(
774 &auth.auth_features,
775 "auth-delegated-token-verify",
776 cfg!(feature = "auth-delegated-token-verify"),
777 );
778 assert_runtime_feature(
779 &auth.auth_features,
780 "auth-issuer-canister-sig-create",
781 cfg!(feature = "auth-issuer-canister-sig-create"),
782 );
783
784 if cfg!(any(
785 feature = "blob-storage",
786 feature = "blob-storage-billing"
787 )) {
788 let blob_storage = status.blob_storage.expect("blob-storage feature summary");
789 assert_runtime_feature(
790 &blob_storage.blob_storage_features,
791 "blob-storage",
792 cfg!(feature = "blob-storage"),
793 );
794 assert_runtime_feature(
795 &blob_storage.blob_storage_features,
796 "blob-storage-billing",
797 cfg!(feature = "blob-storage-billing"),
798 );
799 } else {
800 assert!(status.blob_storage.is_none());
801 }
802 }
803
804 fn assert_runtime_feature(
805 features: &[RuntimeFeatureStatus],
806 name: &str,
807 expected_enabled: bool,
808 ) {
809 let feature = features
810 .iter()
811 .find(|feature| feature.name == name)
812 .unwrap_or_else(|| panic!("expected runtime feature {name}"));
813
814 assert_eq!(feature.enabled, expected_enabled);
815 assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
816 assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
817 }
818
819 #[test]
820 fn runtime_status_projects_live_registration_and_process_condition() {
821 let statuses = timer_statuses_from(vec![timer_snapshot("cycles:topup")]);
822
823 assert_eq!(statuses.len(), 1);
824 assert_eq!(statuses[0].subsystem, "cycles");
825 assert_eq!(statuses[0].name, "topup");
826 assert_eq!(statuses[0].registration, TimerRegistrationStatus::Scheduled);
827 assert_eq!(statuses[0].condition, TimerProcessCondition::Active);
828 assert_eq!(
829 statuses[0].last_outcome,
830 Some(TimerExecutionOutcome::Success)
831 );
832 assert_eq!(statuses[0].schedules_since_runtime_start, 3);
833 }
834
835 #[test]
836 fn runtime_status_bounds_timer_labels() {
837 let label = format!("{}\n:{}\n", "subsystem".repeat(12), "timer_name".repeat(16));
838 let status = timer_statuses_from(vec![timer_snapshot(&label)]);
839
840 assert_eq!(status.len(), 1);
841 assert!(status[0].subsystem.len() <= MAX_TIMER_SUBSYSTEM_BYTES);
842 assert!(status[0].name.len() <= MAX_TIMER_NAME_BYTES);
843 assert!(!status[0].subsystem.contains('\n'));
844 assert!(!status[0].name.contains('\n'));
845 }
846
847 fn timer_snapshot(label: &str) -> TimerRuntimeSnapshot {
848 TimerRuntimeSnapshot {
849 label: label.to_string(),
850 scheduling_mode: TimerSchedulingMode::AfterCompletion,
851 registration: TimerRegistrationStatus::Scheduled,
852 condition: TimerProcessCondition::Active,
853 enabled: true,
854 generation: 4,
855 next_due_at_ns: Some(500),
856 last_outcome: Some(TimerExecutionOutcome::Success),
857 last_work_count: 2,
858 last_success_at_ns: Some(400),
859 last_failure_at_ns: None,
860 consecutive_expected_failures: 0,
861 schedules_since_runtime_start: 3,
862 executions_since_runtime_start: 2,
863 successes_since_runtime_start: 2,
864 expected_failures_since_runtime_start: 0,
865 invariant_failures_since_runtime_start: 0,
866 stale_callbacks_since_runtime_start: 0,
867 }
868 }
869
870 #[test]
871 fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
872 let summary = state_summary_for_memory_ids(
873 Some("root"),
874 &std::collections::BTreeSet::from([
875 crate::role_contract::allocation::memory::env::ENV_ID,
876 ]),
877 )
878 .expect("runtime state declarations");
879
880 assert_eq!(
881 summary.manifest_schema_version,
882 u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
883 );
884 assert!(summary.total_stable_memory_pages.is_none());
885 assert!(summary.domains.iter().any(|domain| {
886 domain.domain == "env"
887 && domain.storage == "stable_memory"
888 && domain.status == RuntimeStateDomainStatus::Ok
889 }));
890 assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
891 }
892
893 #[test]
894 fn runtime_status_includes_recent_failure_snapshot() {
895 RecentFailureOps::reset();
896 RecentFailureOps::record(RecentFailureInput {
897 occurred_at_ns: 77,
898 subsystem: "runtime".to_string(),
899 code: "readiness_failed".to_string(),
900 severity: FailureSeverity::Error,
901 summary: "bounded failure summary".to_string(),
902 correlation_id: Some("runtime-check".to_string()),
903 });
904
905 let status = RuntimeIntrospectionApi::runtime_status_for(
906 Principal::anonymous(),
907 100,
908 "test-canister",
909 "1.2.3",
910 "0.81.0",
911 7,
912 );
913
914 assert_eq!(status.recent_failures.len(), 1);
915 assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
916 assert_eq!(status.recent_failures[0].subsystem, "runtime");
917 assert_eq!(status.recent_failures[0].code, "readiness_failed");
918
919 RecentFailureOps::reset();
920 }
921
922 #[test]
923 fn runtime_status_includes_bootstrap_failure_metadata() {
924 RecentFailureOps::reset();
925 BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
926 BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
927
928 let status = RuntimeIntrospectionApi::runtime_status_for(
929 Principal::anonymous(),
930 100,
931 "test-canister",
932 "1.2.3",
933 "0.81.0",
934 7,
935 );
936
937 let failure = status
938 .recent_failures
939 .iter()
940 .find(|failure| failure.code == "bootstrap_failed")
941 .expect("bootstrap failure metadata");
942
943 assert_eq!(failure.subsystem, "runtime_bootstrap");
944 assert_eq!(failure.severity, FailureSeverity::Error);
945 assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
946 assert!(
947 !failure.summary.contains("raw bootstrap failure detail"),
948 "runtime status recent failures should not mirror raw bootstrap errors"
949 );
950
951 RecentFailureOps::reset();
952 }
953}