1pub mod install;
2
3use crate::{
4 cdk::types::Principal,
5 domain::runtime::{
6 FailureSeverity, HealthStatus, ReadinessStatus, RuntimeCheckStatus,
7 RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus, RuntimeStatus,
8 },
9 dto::{
10 error::Error,
11 runtime::{
12 CanicHealthStatus, CanicReadinessStatus, CanicRuntimeStatus, CanicTimerStatus,
13 RUNTIME_INTROSPECTION_SCHEMA_VERSION, RuntimeAuthStatusSummary,
14 RuntimeBlobStorageStatusSummary, RuntimeBuildInfo, RuntimeCheck, RuntimeDiagnostic,
15 RuntimeFeatureStatus, RuntimeStateDomainSummary, RuntimeStateSummary,
16 RuntimeTopologyStatus, RuntimeVisibilityEntry,
17 },
18 },
19 ops::{
20 ic::{IcOps, build_network::BuildNetworkOps},
21 runtime::{
22 env::EnvOps,
23 memory::MemoryRegistryOps,
24 ready::ReadyOps,
25 recent_failure::{RecentFailureInput, RecentFailureOps},
26 },
27 },
28 state_contract::{STATE_MANIFEST_SCHEMA_VERSION, canic_state_descriptors},
29 workflow::runtime::timer::{TimerRuntimeSnapshot, TimerWorkflow},
30};
31
32const MAX_TIMER_SUBSYSTEM_BYTES: usize = 64;
33const MAX_TIMER_NAME_BYTES: usize = 96;
34const RUNTIME_FEATURE_SOURCE: &str = "compile_feature";
35const RUNTIME_FEATURE_FLAGS: [(&str, bool); 10] = [
36 (
37 "auth-chain-key-ecdsa",
38 cfg!(feature = "auth-chain-key-ecdsa"),
39 ),
40 (
41 "auth-chain-key-root-sign",
42 cfg!(feature = "auth-chain-key-root-sign"),
43 ),
44 (
45 "auth-delegated-token-verify",
46 cfg!(feature = "auth-delegated-token-verify"),
47 ),
48 (
49 "auth-issuer-canister-sig-create",
50 cfg!(feature = "auth-issuer-canister-sig-create"),
51 ),
52 (
53 "auth-issuer-canister-sig-verify",
54 cfg!(feature = "auth-issuer-canister-sig-verify"),
55 ),
56 (
57 "auth-root-canister-sig-create",
58 cfg!(feature = "auth-root-canister-sig-create"),
59 ),
60 (
61 "auth-root-canister-sig-verify",
62 cfg!(feature = "auth-root-canister-sig-verify"),
63 ),
64 ("blob-storage", cfg!(feature = "blob-storage")),
65 (
66 "blob-storage-billing",
67 cfg!(feature = "blob-storage-billing"),
68 ),
69 ("sharding", cfg!(feature = "sharding")),
70];
71
72pub struct MemoryRuntimeApi;
77
78impl MemoryRuntimeApi {
79 pub fn bootstrap_registry() -> Result<(), Error> {
81 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
82
83 Ok(())
84 }
85}
86
87pub struct RuntimeIntrospectionApi;
92
93impl RuntimeIntrospectionApi {
94 pub fn record_recent_failure(
96 occurred_at_ns: u64,
97 subsystem: impl Into<String>,
98 code: impl Into<String>,
99 severity: FailureSeverity,
100 summary: impl Into<String>,
101 correlation_id: Option<String>,
102 ) {
103 RecentFailureOps::record(RecentFailureInput {
104 occurred_at_ns,
105 subsystem: subsystem.into(),
106 code: code.into(),
107 severity,
108 summary: summary.into(),
109 correlation_id,
110 });
111 }
112
113 #[must_use]
115 pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
116 CanicHealthStatus {
117 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
118 status: HealthStatus::Healthy,
119 observed_at_ns,
120 checks: vec![RuntimeCheck {
121 category: "health".to_string(),
122 code: "canister_responsive".to_string(),
123 status: RuntimeCheckStatus::Pass,
124 subject: "canister".to_string(),
125 detail: "canister returned a health response".to_string(),
126 next: None,
127 source: "runtime_observed".to_string(),
128 }],
129 }
130 }
131
132 #[must_use]
134 pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
135 let ready = ReadyOps::is_ready();
136 let role = EnvOps::canister_role()
137 .ok()
138 .map(crate::ids::CanisterRole::into_string);
139
140 let (status, check_status, detail, next) = if ready {
141 (
142 ReadinessStatus::Ready,
143 RuntimeCheckStatus::Pass,
144 "runtime readiness barrier is marked ready",
145 None,
146 )
147 } else {
148 (
149 ReadinessStatus::NotReady,
150 RuntimeCheckStatus::Fail,
151 "runtime readiness barrier is not ready",
152 Some("wait for bootstrap to complete or inspect canic_bootstrap_status"),
153 )
154 };
155
156 let readiness_check = RuntimeCheck {
157 category: "readiness".to_string(),
158 code: "runtime_ready_barrier".to_string(),
159 status: check_status,
160 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
161 detail: detail.to_string(),
162 next: next.map(str::to_string),
163 source: "runtime_observed".to_string(),
164 };
165
166 let blockers = if ready {
167 Vec::new()
168 } else {
169 vec![RuntimeDiagnostic {
170 category: "readiness".to_string(),
171 code: "runtime_not_ready".to_string(),
172 severity: RuntimeDiagnosticSeverity::Blocked,
173 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
174 detail: "runtime readiness barrier has not completed".to_string(),
175 next: Some(
176 "inspect bootstrap status before treating the role as ready".to_string(),
177 ),
178 source: "runtime_observed".to_string(),
179 }]
180 };
181
182 CanicReadinessStatus {
183 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
184 role,
185 status,
186 observed_at_ns,
187 checks: vec![readiness_check],
188 blockers,
189 warnings: Vec::new(),
190 }
191 }
192
193 #[must_use]
195 pub fn runtime_status_for(
196 canister_id: Principal,
197 observed_at_ns: u64,
198 package_name: &str,
199 package_version: &str,
200 canic_version: &str,
201 canister_version: u64,
202 ) -> CanicRuntimeStatus {
203 let readiness = Self::readiness(observed_at_ns);
204 let role = readiness.role.clone();
205 let state = state_summary(role.as_deref());
206 let root = EnvOps::root_pid().ok();
207 let parent = EnvOps::parent_pid().ok();
208 let subnet = EnvOps::subnet_pid().ok();
209 let status = match readiness.status {
210 ReadinessStatus::Ready => RuntimeStatus::Ok,
211 ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated => RuntimeStatus::Degraded,
212 ReadinessStatus::NotReady => RuntimeStatus::Failing,
213 };
214
215 CanicRuntimeStatus {
216 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
217 observed_at_ns,
218 canister_id,
219 role,
220 root,
221 build_network: BuildNetworkOps::build_network(),
222 build: RuntimeBuildInfo {
223 package_name: package_name.to_string(),
224 package_version: package_version.to_string(),
225 canic_version: canic_version.to_string(),
226 canister_version,
227 },
228 features: runtime_features(),
229 topology: Some(RuntimeTopologyStatus {
230 root,
231 parent,
232 subnet,
233 source: "runtime_observed".to_string(),
234 }),
235 timers: timer_statuses(),
236 state,
237 auth: Some(runtime_auth_status()),
238 blob_storage: runtime_blob_storage_status(),
239 recent_failures: RecentFailureOps::snapshot(),
240 visibility: runtime_visibility(),
241 readiness,
242 status,
243 }
244 }
245
246 #[must_use]
248 pub fn runtime_status(
249 observed_at_ns: u64,
250 package_name: &str,
251 package_version: &str,
252 canic_version: &str,
253 canister_version: u64,
254 ) -> CanicRuntimeStatus {
255 Self::runtime_status_for(
256 IcOps::canister_self(),
257 observed_at_ns,
258 package_name,
259 package_version,
260 canic_version,
261 canister_version,
262 )
263 }
264}
265
266fn runtime_features() -> Vec<RuntimeFeatureStatus> {
267 RUNTIME_FEATURE_FLAGS
268 .into_iter()
269 .map(|(name, enabled)| runtime_feature_status(name, enabled))
270 .collect()
271}
272
273fn runtime_feature_status(name: &str, enabled: bool) -> RuntimeFeatureStatus {
274 RuntimeFeatureStatus {
275 name: name.to_string(),
276 enabled,
277 visibility: RuntimeFieldVisibility::OperatorOnly,
278 source: RUNTIME_FEATURE_SOURCE.to_string(),
279 }
280}
281
282fn runtime_auth_status() -> RuntimeAuthStatusSummary {
283 RuntimeAuthStatusSummary {
284 auth_features: RUNTIME_FEATURE_FLAGS
285 .into_iter()
286 .filter(|(name, _)| name.starts_with("auth-"))
287 .map(|(name, enabled)| runtime_feature_status(name, enabled))
288 .collect(),
289 }
290}
291
292fn runtime_blob_storage_status() -> Option<RuntimeBlobStorageStatusSummary> {
293 let blob_storage_enabled = cfg!(feature = "blob-storage");
294 let billing_enabled = cfg!(feature = "blob-storage-billing");
295
296 (blob_storage_enabled || billing_enabled).then(|| RuntimeBlobStorageStatusSummary {
297 blob_storage_features: [
298 ("blob-storage", blob_storage_enabled),
299 ("blob-storage-billing", billing_enabled),
300 ]
301 .into_iter()
302 .map(|(name, enabled)| runtime_feature_status(name, enabled))
303 .collect(),
304 })
305}
306
307fn timer_statuses() -> Vec<CanicTimerStatus> {
308 timer_statuses_from(TimerWorkflow::statuses())
309}
310
311fn timer_statuses_from(snapshots: Vec<TimerRuntimeSnapshot>) -> Vec<CanicTimerStatus> {
312 let mut timers = snapshots
313 .into_iter()
314 .map(|snapshot| {
315 let (subsystem, name) = split_timer_label(&snapshot.label);
316 CanicTimerStatus {
317 name,
318 subsystem,
319 scheduling_mode: snapshot.scheduling_mode,
320 registration: snapshot.registration,
321 condition: snapshot.condition,
322 enabled: snapshot.enabled,
323 generation: snapshot.generation,
324 next_due_at_ns: snapshot.next_due_at_ns,
325 last_outcome: snapshot.last_outcome,
326 last_work_count: snapshot.last_work_count,
327 last_success_at_ns: snapshot.last_success_at_ns,
328 last_failure_at_ns: snapshot.last_failure_at_ns,
329 consecutive_expected_failures: snapshot.consecutive_expected_failures,
330 schedules_since_runtime_start: snapshot.schedules_since_runtime_start,
331 executions_since_runtime_start: snapshot.executions_since_runtime_start,
332 successes_since_runtime_start: snapshot.successes_since_runtime_start,
333 expected_failures_since_runtime_start: snapshot
334 .expected_failures_since_runtime_start,
335 invariant_failures_since_runtime_start: snapshot
336 .invariant_failures_since_runtime_start,
337 stale_callbacks_since_runtime_start: snapshot.stale_callbacks_since_runtime_start,
338 }
339 })
340 .collect::<Vec<_>>();
341 timers.sort_by(|left, right| {
342 left.subsystem
343 .cmp(&right.subsystem)
344 .then_with(|| left.name.cmp(&right.name))
345 });
346 timers
347}
348
349fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
350 let memory_ids = MemoryRegistryOps::ledger_snapshot()
351 .ok()?
352 .memories
353 .into_iter()
354 .map(|memory| memory.memory_manager_id)
355 .collect::<std::collections::BTreeSet<_>>();
356 state_summary_for_memory_ids(role, &memory_ids)
357}
358
359fn state_summary_for_memory_ids(
360 role: Option<&str>,
361 memory_ids: &std::collections::BTreeSet<u8>,
362) -> Option<RuntimeStateSummary> {
363 role?;
364 let mut domains = canic_state_descriptors()
365 .into_iter()
366 .flat_map(|descriptor| descriptor.state)
367 .filter(|domain| domain.memory_id.is_some_and(|id| memory_ids.contains(&id)))
368 .map(|domain| RuntimeStateDomainSummary {
369 domain: domain.domain,
370 version: domain.version,
371 storage: domain.storage.as_str().to_string(),
372 memory_id: domain.memory_id,
373 status: RuntimeStateDomainStatus::Ok,
374 })
375 .collect::<Vec<_>>();
376 domains.sort_by(|left, right| left.domain.cmp(&right.domain));
377
378 if domains.is_empty() {
379 return None;
380 }
381
382 Some(RuntimeStateSummary {
383 manifest_schema_version: u32::from(STATE_MANIFEST_SCHEMA_VERSION),
384 domains,
385 total_stable_memory_pages: None,
386 })
387}
388
389fn split_timer_label(label: &str) -> (String, String) {
390 label
391 .split_once("::")
392 .or_else(|| label.split_once(':'))
393 .map_or_else(
394 || {
395 (
396 "runtime".to_string(),
397 bounded_runtime_text(label, MAX_TIMER_NAME_BYTES),
398 )
399 },
400 |(subsystem, name)| {
401 (
402 bounded_runtime_text(subsystem, MAX_TIMER_SUBSYSTEM_BYTES),
403 bounded_runtime_text(name, MAX_TIMER_NAME_BYTES),
404 )
405 },
406 )
407}
408
409fn bounded_runtime_text(value: &str, max_bytes: usize) -> String {
410 let sanitized = value
411 .chars()
412 .map(|character| {
413 if character.is_control() {
414 ' '
415 } else {
416 character
417 }
418 })
419 .collect::<String>();
420
421 if sanitized.len() <= max_bytes {
422 return sanitized;
423 }
424
425 let mut end = 0;
426 for (index, character) in sanitized.char_indices() {
427 let next = index + character.len_utf8();
428 if next > max_bytes {
429 break;
430 }
431 end = next;
432 }
433
434 sanitized[..end].to_string()
435}
436
437fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
438 [
439 ("schema_version", RuntimeFieldVisibility::PublicSafe),
440 ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
441 ("canister_id", RuntimeFieldVisibility::OperatorOnly),
442 ("role", RuntimeFieldVisibility::OperatorOnly),
443 ("root", RuntimeFieldVisibility::OperatorOnly),
444 ("build_network", RuntimeFieldVisibility::OperatorOnly),
445 ("build", RuntimeFieldVisibility::OperatorOnly),
446 ("features", RuntimeFieldVisibility::OperatorOnly),
447 ("topology", RuntimeFieldVisibility::ControllerOnly),
448 ("timers", RuntimeFieldVisibility::OperatorOnly),
449 ("state", RuntimeFieldVisibility::OperatorOnly),
450 ("auth", RuntimeFieldVisibility::OperatorOnly),
451 ("blob_storage", RuntimeFieldVisibility::FeatureGated),
452 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
453 ("readiness", RuntimeFieldVisibility::OperatorOnly),
454 ("status", RuntimeFieldVisibility::OperatorOnly),
455 ("visibility", RuntimeFieldVisibility::OperatorOnly),
456 ]
457 .into_iter()
458 .map(|(field, visibility)| RuntimeVisibilityEntry {
459 field: field.to_string(),
460 visibility,
461 })
462 .collect()
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::domain::runtime::{
469 TimerExecutionOutcome, TimerProcessCondition, TimerRegistrationStatus, TimerSchedulingMode,
470 };
471 use crate::ops::runtime::bootstrap::{BootstrapPhaseLabel, BootstrapStatusOps};
472 use crate::ops::runtime::recent_failure::RecentFailureOps;
473
474 #[test]
475 fn health_is_minimal_and_schema_versioned() {
476 let health = RuntimeIntrospectionApi::health(Some(42));
477
478 assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
479 assert_eq!(health.status, HealthStatus::Healthy);
480 assert_eq!(health.observed_at_ns, Some(42));
481 assert_eq!(health.checks.len(), 1);
482 assert_eq!(health.checks[0].code, "canister_responsive");
483 }
484
485 #[test]
486 fn runtime_status_embeds_guarded_readiness_and_build_info() {
487 let status = RuntimeIntrospectionApi::runtime_status_for(
488 Principal::anonymous(),
489 100,
490 "test-canister",
491 "1.2.3",
492 "0.81.0",
493 7,
494 );
495
496 assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
497 assert_eq!(status.observed_at_ns, 100);
498 assert_eq!(status.canister_id, Principal::anonymous());
499 assert_eq!(status.build_network, BuildNetworkOps::build_network());
500 assert_eq!(status.build.package_name, "test-canister");
501 assert_eq!(status.build.package_version, "1.2.3");
502 assert_eq!(status.build.canic_version, "0.81.0");
503 assert_eq!(status.build.canister_version, 7);
504 assert_eq!(status.readiness.observed_at_ns, 100);
505 assert!(
506 status
507 .visibility
508 .iter()
509 .any(|entry| entry.field == "topology"
510 && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
511 );
512 }
513
514 #[test]
515 fn runtime_status_classifies_each_top_level_field_visibility() {
516 let status = RuntimeIntrospectionApi::runtime_status_for(
517 Principal::anonymous(),
518 100,
519 "test-canister",
520 "1.2.3",
521 "0.81.0",
522 7,
523 );
524 let expected = [
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 ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
539 ("readiness", RuntimeFieldVisibility::OperatorOnly),
540 ("status", RuntimeFieldVisibility::OperatorOnly),
541 ("visibility", RuntimeFieldVisibility::OperatorOnly),
542 ];
543
544 assert_eq!(status.visibility.len(), expected.len());
545 for (index, (field, visibility)) in expected.into_iter().enumerate() {
546 assert_eq!(status.visibility[index].field, field);
547 assert_eq!(status.visibility[index].visibility, visibility);
548 }
549 }
550
551 #[test]
552 fn runtime_status_reports_compile_features_deterministically() {
553 let status = RuntimeIntrospectionApi::runtime_status_for(
554 Principal::anonymous(),
555 100,
556 "test-canister",
557 "1.2.3",
558 "0.81.0",
559 7,
560 );
561 assert_eq!(status.features.len(), RUNTIME_FEATURE_FLAGS.len());
562 for (index, (name, enabled)) in RUNTIME_FEATURE_FLAGS.into_iter().enumerate() {
563 assert_eq!(status.features[index].name, name);
564 assert_eq!(status.features[index].enabled, enabled);
565 assert_eq!(
566 status.features[index].visibility,
567 RuntimeFieldVisibility::OperatorOnly
568 );
569 assert_eq!(status.features[index].source, RUNTIME_FEATURE_SOURCE);
570 }
571 }
572
573 #[test]
574 fn runtime_status_reports_auth_and_blob_storage_feature_summaries() {
575 let status = RuntimeIntrospectionApi::runtime_status_for(
576 Principal::anonymous(),
577 100,
578 "test-canister",
579 "1.2.3",
580 "0.81.0",
581 7,
582 );
583
584 let auth = status.auth.expect("auth feature summary");
585 assert!(
586 auth.auth_features
587 .windows(2)
588 .all(|features| features[0].name <= features[1].name)
589 );
590 assert_runtime_feature(
591 &auth.auth_features,
592 "auth-chain-key-ecdsa",
593 cfg!(feature = "auth-chain-key-ecdsa"),
594 );
595 assert_runtime_feature(
596 &auth.auth_features,
597 "auth-delegated-token-verify",
598 cfg!(feature = "auth-delegated-token-verify"),
599 );
600 assert_runtime_feature(
601 &auth.auth_features,
602 "auth-issuer-canister-sig-create",
603 cfg!(feature = "auth-issuer-canister-sig-create"),
604 );
605
606 if cfg!(any(
607 feature = "blob-storage",
608 feature = "blob-storage-billing"
609 )) {
610 let blob_storage = status.blob_storage.expect("blob-storage feature summary");
611 assert_runtime_feature(
612 &blob_storage.blob_storage_features,
613 "blob-storage",
614 cfg!(feature = "blob-storage"),
615 );
616 assert_runtime_feature(
617 &blob_storage.blob_storage_features,
618 "blob-storage-billing",
619 cfg!(feature = "blob-storage-billing"),
620 );
621 } else {
622 assert!(status.blob_storage.is_none());
623 }
624 }
625
626 fn assert_runtime_feature(
627 features: &[RuntimeFeatureStatus],
628 name: &str,
629 expected_enabled: bool,
630 ) {
631 let feature = features
632 .iter()
633 .find(|feature| feature.name == name)
634 .unwrap_or_else(|| panic!("expected runtime feature {name}"));
635
636 assert_eq!(feature.enabled, expected_enabled);
637 assert_eq!(feature.visibility, RuntimeFieldVisibility::OperatorOnly);
638 assert_eq!(feature.source, RUNTIME_FEATURE_SOURCE);
639 }
640
641 #[test]
642 fn runtime_status_projects_live_registration_and_process_condition() {
643 let statuses = timer_statuses_from(vec![timer_snapshot("cycles:tracking")]);
644
645 assert_eq!(statuses.len(), 1);
646 assert_eq!(statuses[0].subsystem, "cycles");
647 assert_eq!(statuses[0].name, "tracking");
648 assert_eq!(statuses[0].registration, TimerRegistrationStatus::Scheduled);
649 assert_eq!(statuses[0].condition, TimerProcessCondition::Active);
650 assert_eq!(
651 statuses[0].last_outcome,
652 Some(TimerExecutionOutcome::Success)
653 );
654 assert_eq!(statuses[0].schedules_since_runtime_start, 3);
655 }
656
657 #[test]
658 fn runtime_status_bounds_timer_labels() {
659 let label = format!("{}\n:{}\n", "subsystem".repeat(12), "timer_name".repeat(16));
660 let status = timer_statuses_from(vec![timer_snapshot(&label)]);
661
662 assert_eq!(status.len(), 1);
663 assert!(status[0].subsystem.len() <= MAX_TIMER_SUBSYSTEM_BYTES);
664 assert!(status[0].name.len() <= MAX_TIMER_NAME_BYTES);
665 assert!(!status[0].subsystem.contains('\n'));
666 assert!(!status[0].name.contains('\n'));
667 }
668
669 fn timer_snapshot(label: &str) -> TimerRuntimeSnapshot {
670 TimerRuntimeSnapshot {
671 label: label.to_string(),
672 scheduling_mode: TimerSchedulingMode::AfterCompletion,
673 registration: TimerRegistrationStatus::Scheduled,
674 condition: TimerProcessCondition::Active,
675 enabled: true,
676 generation: 4,
677 next_due_at_ns: Some(500),
678 last_outcome: Some(TimerExecutionOutcome::Success),
679 last_work_count: 2,
680 last_success_at_ns: Some(400),
681 last_failure_at_ns: None,
682 consecutive_expected_failures: 0,
683 schedules_since_runtime_start: 3,
684 executions_since_runtime_start: 2,
685 successes_since_runtime_start: 2,
686 expected_failures_since_runtime_start: 0,
687 invariant_failures_since_runtime_start: 0,
688 stale_callbacks_since_runtime_start: 0,
689 }
690 }
691
692 #[test]
693 fn state_summary_joins_runtime_memory_ids_to_owner_metadata() {
694 let summary = state_summary_for_memory_ids(
695 Some("root"),
696 &std::collections::BTreeSet::from([
697 crate::role_contract::allocation::memory::env::ENV_ID,
698 ]),
699 )
700 .expect("runtime state declarations");
701
702 assert_eq!(
703 summary.manifest_schema_version,
704 u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
705 );
706 assert!(summary.total_stable_memory_pages.is_none());
707 assert!(summary.domains.iter().any(|domain| {
708 domain.domain == "env"
709 && domain.storage == "stable_memory"
710 && domain.status == RuntimeStateDomainStatus::Ok
711 }));
712 assert!(state_summary_for_memory_ids(None, &std::collections::BTreeSet::new()).is_none());
713 }
714
715 #[test]
716 fn runtime_status_includes_recent_failure_snapshot() {
717 RecentFailureOps::reset();
718 RuntimeIntrospectionApi::record_recent_failure(
719 77,
720 "runtime",
721 "readiness_failed",
722 FailureSeverity::Error,
723 "bounded failure summary",
724 Some("runtime-check".to_string()),
725 );
726
727 let status = RuntimeIntrospectionApi::runtime_status_for(
728 Principal::anonymous(),
729 100,
730 "test-canister",
731 "1.2.3",
732 "0.81.0",
733 7,
734 );
735
736 assert_eq!(status.recent_failures.len(), 1);
737 assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
738 assert_eq!(status.recent_failures[0].subsystem, "runtime");
739 assert_eq!(status.recent_failures[0].code, "readiness_failed");
740
741 RecentFailureOps::reset();
742 }
743
744 #[test]
745 fn runtime_status_includes_bootstrap_failure_metadata() {
746 RecentFailureOps::reset();
747 BootstrapStatusOps::set_phase(BootstrapPhaseLabel::ROOT_INIT);
748 BootstrapStatusOps::mark_failed("raw bootstrap failure detail");
749
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
759 let failure = status
760 .recent_failures
761 .iter()
762 .find(|failure| failure.code == "bootstrap_failed")
763 .expect("bootstrap failure metadata");
764
765 assert_eq!(failure.subsystem, "runtime_bootstrap");
766 assert_eq!(failure.severity, FailureSeverity::Error);
767 assert_eq!(failure.correlation_id.as_deref(), Some("root:init"));
768 assert!(
769 !failure.summary.contains("raw bootstrap failure detail"),
770 "runtime status recent failures should not mirror raw bootstrap errors"
771 );
772
773 RecentFailureOps::reset();
774 }
775}