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