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