Skip to main content

ic_memory/
diagnostics.rs

1use crate::{
2    constants::WASM_PAGE_SIZE_BYTES,
3    declaration::AllocationDeclaration,
4    ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
5    physical::CommitStoreDiagnostic,
6    policy::PolicyIdentity,
7    registry::SealedDeclarationFingerprint,
8    slot::{AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority},
9};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13///
14/// DiagnosticExport
15///
16/// Read-only machine-readable allocation ledger export.
17#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[serde(deny_unknown_fields)]
19pub struct DiagnosticExport {
20    /// Current committed generation.
21    pub current_generation: u64,
22    /// Ledger anchor descriptor.
23    pub ledger_anchor: AllocationSlotDescriptor,
24    /// Allocation records.
25    pub records: Vec<DiagnosticRecord>,
26    /// Generation records.
27    pub generations: Vec<DiagnosticGeneration>,
28    /// Optional protected commit recovery diagnostic.
29    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
30    pub commit_recovery: Option<CommitStoreDiagnostic>,
31}
32
33///
34/// DiagnosticRuntimeBinding
35///
36/// Operator-facing view of the policy identity and declaration snapshot bound
37/// to one successful memory-runtime bootstrap.
38///
39
40#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(deny_unknown_fields)]
42pub struct DiagnosticRuntimeBinding {
43    /// Bounded semantic identity of the bootstrap policy.
44    pub policy_identity: PolicyIdentity,
45    /// Deterministic fingerprint of the sealed declaration snapshot.
46    pub declaration_fingerprint: SealedDeclarationFingerprint,
47}
48
49impl DiagnosticRuntimeBinding {
50    /// Build one runtime binding diagnostic.
51    #[must_use]
52    pub const fn new(
53        policy_identity: PolicyIdentity,
54        declaration_fingerprint: SealedDeclarationFingerprint,
55    ) -> Self {
56        Self {
57            policy_identity,
58            declaration_fingerprint,
59        }
60    }
61}
62
63///
64/// MemoryRuntimeDoctorReport
65///
66/// Preflight and runtime diagnostic report for one concrete
67/// [`crate::MemoryRuntime`].
68///
69/// This report is intended for operator-facing diagnostics. Recoverable
70/// runtime problems, such as corrupt stable-cell bytes or commit recovery
71/// failure, are represented as fields instead of aborting report construction.
72///
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct MemoryRuntimeDoctorReport {
77    /// Whether this runtime has completed bootstrap validation.
78    pub bootstrapped: bool,
79    /// Policy identity supplied for this diagnostic evaluation.
80    pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
81    /// Sealed declaration fingerprint supplied for this diagnostic evaluation.
82    pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
83    /// Binding published by this runtime's successful bootstrap, when present.
84    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
85    pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
86    /// Whether the tested identity and declarations match the established
87    /// bootstrap binding.
88    pub bootstrap_binding: DiagnosticCheck,
89    /// Ledger anchor descriptor used by this runtime.
90    pub ledger_anchor: AllocationSlotDescriptor,
91    /// Stable-cell ledger storage status.
92    pub stable_cell: DiagnosticStableCell,
93    /// Protected commit recovery status when a ledger record was readable.
94    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
95    pub commit_recovery: Option<CommitStoreDiagnostic>,
96    /// Recovered allocation ledger export when protected recovery succeeded.
97    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
98    pub ledger: Option<DiagnosticExport>,
99    /// Static declarations registered by linked crates.
100    pub registered_declarations: Vec<DiagnosticDeclaration>,
101    /// Static range authority registered by linked crates and the effective
102    /// authority table supplied to this runtime.
103    pub range_authority: DiagnosticRangeAuthority,
104    /// Declaration validation result under the tested caller-supplied policy.
105    pub validation: DiagnosticCheck,
106}
107
108///
109/// DiagnosticDeclaration
110///
111/// Read-only diagnostic view of one static allocation declaration.
112///
113
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
115#[serde(deny_unknown_fields)]
116pub struct DiagnosticDeclaration {
117    /// Crate or integration authority that registered the declaration.
118    pub authority: String,
119    /// Allocation declaration registered by that authority.
120    pub declaration: AllocationDeclaration,
121}
122
123impl DiagnosticDeclaration {
124    /// Build a diagnostic declaration record.
125    #[must_use]
126    pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
127        Self {
128            authority: authority.into(),
129            declaration,
130        }
131    }
132}
133
134///
135/// DiagnosticCode
136///
137/// Stable machine-readable category for an operator diagnostic failure.
138///
139
140#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub enum DiagnosticCode {
142    /// Stable-cell storage could not be decoded.
143    #[serde(rename = "stable_cell")]
144    StableCell,
145    /// Persisted bytes use a recognized but unsupported durable format.
146    #[serde(rename = "unsupported_format")]
147    UnsupportedFormat,
148    /// Protected ledger recovery failed.
149    #[serde(rename = "ledger_recovery")]
150    LedgerRecovery,
151    /// An empty current-format genesis ledger could not be constructed.
152    #[serde(rename = "genesis_ledger")]
153    GenesisLedger,
154    /// Current declarations failed allocation validation.
155    #[serde(rename = "allocation_validation")]
156    AllocationValidation,
157    /// Runtime bootstrap policy identity was invalid.
158    #[serde(rename = "policy_identity")]
159    PolicyIdentity,
160    /// Tested bootstrap identity or declarations differ from runtime state.
161    #[serde(rename = "runtime_binding")]
162    RuntimeBinding,
163    /// Live memory size could not be measured for one allocation.
164    #[serde(rename = "memory_size")]
165    MemorySize,
166}
167
168///
169/// DiagnosticFailure
170///
171/// Machine-readable diagnostic code paired with an operator-facing message.
172///
173
174#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
175#[serde(deny_unknown_fields)]
176pub struct DiagnosticFailure {
177    /// Stable diagnostic category.
178    pub code: DiagnosticCode,
179    /// Human-readable failure detail.
180    pub message: String,
181}
182
183impl DiagnosticFailure {
184    /// Build a coded diagnostic failure.
185    #[must_use]
186    pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
187        Self {
188            code,
189            message: message.into(),
190        }
191    }
192}
193
194///
195/// DiagnosticRangeAuthority
196///
197/// Read-only diagnostic view of registered and effective range authority.
198///
199
200#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(deny_unknown_fields)]
202pub struct DiagnosticRangeAuthority {
203    /// Range records registered directly by linked crates.
204    pub registered_records: Vec<MemoryManagerAuthorityRecord>,
205    /// Validated effective range authority from the sealed declarations.
206    pub effective_authority: MemoryManagerRangeAuthority,
207}
208
209impl DiagnosticRangeAuthority {
210    /// Build a range-authority diagnostic.
211    #[must_use]
212    pub const fn new(
213        registered_records: Vec<MemoryManagerAuthorityRecord>,
214        effective_authority: MemoryManagerRangeAuthority,
215    ) -> Self {
216        Self {
217            registered_records,
218            effective_authority,
219        }
220    }
221}
222
223///
224/// DiagnosticStableCell
225///
226/// Read-only diagnostic view of the stable-cell ledger storage envelope.
227///
228
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct DiagnosticStableCell {
232    /// Stable-cell status.
233    pub status: DiagnosticStableCellStatus,
234    /// Backing memory size for the ledger cell.
235    pub memory_size: DiagnosticMemorySize,
236}
237
238impl DiagnosticStableCell {
239    /// Build a stable-cell diagnostic.
240    #[must_use]
241    pub const fn new(
242        status: DiagnosticStableCellStatus,
243        memory_size: DiagnosticMemorySize,
244    ) -> Self {
245        Self {
246            status,
247            memory_size,
248        }
249    }
250}
251
252///
253/// DiagnosticStableCellStatus
254///
255/// Stable-cell ledger storage status.
256///
257
258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
259#[serde(deny_unknown_fields)]
260pub enum DiagnosticStableCellStatus {
261    /// The ledger memory is empty and can be initialized.
262    Empty,
263    /// The stable-cell envelope and ledger record decoded successfully.
264    Readable,
265    /// The ledger memory is present but could not be decoded as the expected
266    /// stable-cell ledger record.
267    Corrupt {
268        /// Stable-cell envelope or ledger-record decode failure.
269        failure: DiagnosticFailure,
270    },
271}
272
273///
274/// DiagnosticCheck
275///
276/// Read-only diagnostic status for a preflight check.
277///
278
279#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
280#[serde(deny_unknown_fields)]
281pub enum DiagnosticCheck {
282    /// The check could not run because prerequisite state was unavailable.
283    NotRun {
284        /// Stable diagnostic category.
285        code: DiagnosticCode,
286        /// Reason the check could not run.
287        message: String,
288    },
289    /// The check completed successfully.
290    Passed,
291    /// The check ran and found a problem.
292    Failed {
293        /// Stable diagnostic category.
294        code: DiagnosticCode,
295        /// Validation failure.
296        message: String,
297    },
298}
299
300impl DiagnosticCheck {
301    /// Build a passed diagnostic check.
302    #[must_use]
303    pub const fn passed() -> Self {
304        Self::Passed
305    }
306
307    /// Build a failed diagnostic check.
308    #[must_use]
309    pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
310        Self::Failed {
311            code,
312            message: message.into(),
313        }
314    }
315
316    /// Build a skipped diagnostic check.
317    #[must_use]
318    pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
319        Self::NotRun {
320            code,
321            message: message.into(),
322        }
323    }
324}
325
326impl DiagnosticExport {
327    /// Build a read-only diagnostic export from an allocation ledger.
328    #[must_use]
329    pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
330        Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
331    }
332
333    /// Build a read-only diagnostic export with protected commit recovery state.
334    #[must_use]
335    pub fn from_ledger_with_commit_recovery(
336        ledger: &AllocationLedger,
337        ledger_anchor: AllocationSlotDescriptor,
338        commit_recovery: Option<CommitStoreDiagnostic>,
339    ) -> Self {
340        Self::from_ledger_with_commit_recovery_and_memory_sizes(
341            ledger,
342            ledger_anchor,
343            commit_recovery,
344            std::iter::empty(),
345        )
346    }
347
348    /// Build a read-only diagnostic export with live memory sizes.
349    #[must_use]
350    pub fn from_ledger_with_memory_sizes(
351        ledger: &AllocationLedger,
352        ledger_anchor: AllocationSlotDescriptor,
353        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
354    ) -> Self {
355        Self::from_ledger_with_commit_recovery_and_memory_sizes(
356            ledger,
357            ledger_anchor,
358            None,
359            memory_sizes,
360        )
361    }
362
363    /// Build a read-only diagnostic export with protected recovery state and live memory sizes.
364    #[must_use]
365    pub fn from_ledger_with_commit_recovery_and_memory_sizes(
366        ledger: &AllocationLedger,
367        ledger_anchor: AllocationSlotDescriptor,
368        commit_recovery: Option<CommitStoreDiagnostic>,
369        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
370    ) -> Self {
371        Self::from_ledger_with_commit_recovery_and_memory_size_outcomes(
372            ledger,
373            ledger_anchor,
374            commit_recovery,
375            memory_sizes
376                .into_iter()
377                .map(|(slot, size)| (slot, DiagnosticMemorySizeOutcome::Measured(size))),
378        )
379    }
380
381    pub(crate) fn from_ledger_with_commit_recovery_and_memory_size_outcomes(
382        ledger: &AllocationLedger,
383        ledger_anchor: AllocationSlotDescriptor,
384        commit_recovery: Option<CommitStoreDiagnostic>,
385        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)>,
386    ) -> Self {
387        let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
388        Self {
389            current_generation: ledger.current_generation,
390            ledger_anchor,
391            records: ledger
392                .allocation_history()
393                .records()
394                .iter()
395                .cloned()
396                .map(|allocation| {
397                    let memory_size = memory_sizes.get(allocation.slot()).cloned();
398                    DiagnosticRecord {
399                        allocation,
400                        memory_size,
401                    }
402                })
403                .collect(),
404            generations: ledger
405                .allocation_history()
406                .generations()
407                .iter()
408                .cloned()
409                .map(|generation| DiagnosticGeneration { generation })
410                .collect(),
411            commit_recovery,
412        }
413    }
414}
415
416///
417/// DiagnosticRecord
418///
419/// Read-only diagnostic allocation record.
420#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
421#[serde(deny_unknown_fields)]
422pub struct DiagnosticRecord {
423    /// Allocation record.
424    pub allocation: AllocationRecord,
425    /// Live backing memory size, when the exporter measured one.
426    ///
427    /// This is allocation size reported by the backing memory, not logical user
428    /// payload size inside the stable structure.
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub memory_size: Option<DiagnosticMemorySizeOutcome>,
431}
432
433///
434/// DiagnosticMemorySizeOutcome
435///
436/// Per-allocation result of measuring live backing-memory size.
437///
438/// Diagnostic DTO producers can report measurement failures. Runtime reports
439/// measure only allocations whose slots passed ledger recovery; invalid slots
440/// are recovery failures rather than per-allocation measurement outcomes.
441///
442
443#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
444pub enum DiagnosticMemorySizeOutcome {
445    /// The backing memory reported a live size.
446    Measured(DiagnosticMemorySize),
447    /// The allocation slot could not be measured.
448    Failed(DiagnosticFailure),
449}
450
451///
452/// DiagnosticMemorySize
453///
454/// Live size reported by a backing stable memory.
455///
456
457#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
458#[serde(deny_unknown_fields)]
459pub struct DiagnosticMemorySize {
460    /// WebAssembly pages reported by the memory.
461    pub wasm_pages: u64,
462    /// Bytes represented by the page count.
463    pub bytes: u64,
464}
465
466impl DiagnosticMemorySize {
467    /// Build a size from a WebAssembly page count.
468    #[must_use]
469    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
470        Self {
471            wasm_pages,
472            bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
473        }
474    }
475}
476
477///
478/// DiagnosticGeneration
479///
480/// Read-only diagnostic generation record.
481#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct DiagnosticGeneration {
484    /// Generation record.
485    pub generation: GenerationRecord,
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::{
492        declaration::AllocationDeclaration,
493        ledger::{AllocationHistory, AllocationRecord},
494        physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
495        schema::SchemaMetadata,
496    };
497
498    #[test]
499    fn diagnostic_export_copies_ledger_records() {
500        let declaration = AllocationDeclaration::new(
501            "app.users.v1",
502            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
503            None,
504            SchemaMetadata::default(),
505        )
506        .expect("declaration");
507        let ledger = AllocationLedger {
508            current_generation: 3,
509            allocation_history: AllocationHistory::from_parts(
510                vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
511                vec![GenerationRecord {
512                    generation: 3,
513                    parent_generation: 2,
514                    runtime_fingerprint: Some("wasm:abc123".to_string()),
515                    declaration_count: 1,
516                    committed_at: None,
517                }],
518            ),
519        };
520
521        let export = DiagnosticExport::from_ledger(
522            &ledger,
523            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
524        );
525
526        assert_eq!(export.current_generation, 3);
527        assert_eq!(export.records.len(), 1);
528        assert_eq!(export.records[0].memory_size, None);
529        assert_eq!(export.generations.len(), 1);
530        assert_eq!(
531            export.ledger_anchor,
532            AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
533        );
534        assert_eq!(export.commit_recovery, None);
535    }
536
537    #[test]
538    fn diagnostic_export_rejects_unknown_top_level_fields() {
539        use crate::test_cbor::Value;
540
541        let export = DiagnosticExport {
542            current_generation: 0,
543            ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
544            records: Vec::new(),
545            generations: Vec::new(),
546            commit_recovery: None,
547        };
548        let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
549        else {
550            panic!("diagnostic export encodes as a map");
551        };
552        crate::test_cbor::map_insert(
553            &mut map,
554            Value::Text("future_field".to_string()),
555            Value::Bool(true),
556        );
557        let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
558
559        let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
560            .expect_err("unknown diagnostic field must fail closed");
561
562        assert!(err.to_string().contains("future_field"));
563    }
564
565    #[test]
566    fn diagnostic_outcome_states_round_trip() {
567        let stable_cell = DiagnosticStableCell::new(
568            DiagnosticStableCellStatus::Corrupt {
569                failure: DiagnosticFailure::new(
570                    DiagnosticCode::StableCell,
571                    "bad stable-cell record",
572                ),
573            },
574            DiagnosticMemorySize::from_wasm_pages(1),
575        );
576        let range_authority =
577            DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
578        let check = DiagnosticCheck::failed(
579            DiagnosticCode::AllocationValidation,
580            "duplicate declaration",
581        );
582
583        for value in [DiagnosticCheck::passed(), check] {
584            let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
585            let decoded: DiagnosticCheck =
586                crate::test_cbor::from_slice(&bytes).expect("check round trip");
587            assert_eq!(decoded, value);
588        }
589
590        let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
591        let decoded: DiagnosticStableCell =
592            crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
593        assert_eq!(decoded, stable_cell);
594
595        let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
596        let decoded: DiagnosticRangeAuthority =
597            crate::test_cbor::from_slice(&bytes).expect("range round trip");
598        assert_eq!(decoded, range_authority);
599    }
600
601    #[test]
602    fn diagnostic_codes_have_stable_wire_names() {
603        let cases = [
604            (DiagnosticCode::StableCell, "stable_cell"),
605            (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
606            (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
607            (DiagnosticCode::GenesisLedger, "genesis_ledger"),
608            (
609                DiagnosticCode::AllocationValidation,
610                "allocation_validation",
611            ),
612            (DiagnosticCode::PolicyIdentity, "policy_identity"),
613            (DiagnosticCode::RuntimeBinding, "runtime_binding"),
614            (DiagnosticCode::MemorySize, "memory_size"),
615        ];
616
617        for (code, expected) in cases {
618            assert_eq!(
619                crate::test_cbor::to_value(code).expect("diagnostic code value"),
620                crate::test_cbor::Value::Text(expected.to_string())
621            );
622        }
623    }
624
625    #[test]
626    fn diagnostic_export_can_include_commit_recovery_state() {
627        let ledger = AllocationLedger {
628            current_generation: 3,
629            allocation_history: AllocationHistory::default(),
630        };
631        let commit_recovery = CommitStoreDiagnostic {
632            slot0: CommitSlotDiagnostic::Valid { generation: 3 },
633            slot1: CommitSlotDiagnostic::Empty,
634            recovery: Ok(3),
635        };
636
637        let export = DiagnosticExport::from_ledger_with_commit_recovery(
638            &ledger,
639            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
640            Some(commit_recovery),
641        );
642
643        assert_eq!(export.commit_recovery, Some(commit_recovery));
644    }
645
646    #[test]
647    fn diagnostic_export_can_include_memory_sizes() {
648        let declaration = AllocationDeclaration::new(
649            "app.users.v1",
650            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
651            None,
652            SchemaMetadata::default(),
653        )
654        .expect("declaration");
655        let ledger = AllocationLedger {
656            current_generation: 3,
657            allocation_history: AllocationHistory::from_parts(
658                vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
659                Vec::new(),
660            ),
661        };
662
663        let export = DiagnosticExport::from_ledger_with_memory_sizes(
664            &ledger,
665            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
666            [(
667                AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
668                DiagnosticMemorySize::from_wasm_pages(2),
669            )],
670        );
671
672        assert_eq!(
673            export.records[0].memory_size,
674            Some(DiagnosticMemorySizeOutcome::Measured(
675                DiagnosticMemorySize {
676                    wasm_pages: 2,
677                    bytes: 131_072,
678                }
679            ))
680        );
681    }
682
683    #[test]
684    fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
685        let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
686            .expect("users declaration");
687        let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
688            .expect("orders declaration");
689        let ledger = AllocationLedger {
690            current_generation: 3,
691            allocation_history: AllocationHistory::from_parts(
692                vec![
693                    AllocationRecord::active(3, users).expect("users record"),
694                    AllocationRecord::active(3, orders).expect("orders record"),
695                ],
696                Vec::new(),
697            ),
698        };
699        let size_failure =
700            DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
701
702        let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
703            &ledger,
704            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
705            None,
706            [
707                (
708                    AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
709                    DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
710                ),
711                (
712                    AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
713                    DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
714                ),
715            ],
716        );
717
718        assert_eq!(
719            export.records[0].memory_size,
720            Some(DiagnosticMemorySizeOutcome::Measured(
721                DiagnosticMemorySize::from_wasm_pages(2)
722            ))
723        );
724        assert_eq!(
725            export.records[1].memory_size,
726            Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
727        );
728    }
729
730    #[test]
731    fn diagnostic_export_can_report_recovery_failure() {
732        let ledger = AllocationLedger {
733            current_generation: 0,
734            allocation_history: AllocationHistory::default(),
735        };
736        let commit_recovery = CommitStoreDiagnostic {
737            slot0: CommitSlotDiagnostic::Empty,
738            slot1: CommitSlotDiagnostic::Empty,
739            recovery: Err(CommitRecoveryError::NoValidGeneration),
740        };
741
742        let export = DiagnosticExport::from_ledger_with_commit_recovery(
743            &ledger,
744            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
745            Some(commit_recovery),
746        );
747
748        assert_eq!(
749            export.commit_recovery.expect("commit recovery").recovery,
750            Err(CommitRecoveryError::NoValidGeneration)
751        );
752    }
753}