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    /// Current declarations failed allocation validation.
152    #[serde(rename = "allocation_validation")]
153    AllocationValidation,
154    /// Runtime bootstrap policy identity was invalid.
155    #[serde(rename = "policy_identity")]
156    PolicyIdentity,
157    /// Tested bootstrap identity or declarations differ from runtime state.
158    #[serde(rename = "runtime_binding")]
159    RuntimeBinding,
160    /// Live memory size could not be measured for one allocation.
161    #[serde(rename = "memory_size")]
162    MemorySize,
163}
164
165///
166/// DiagnosticFailure
167///
168/// Machine-readable diagnostic code paired with an operator-facing message.
169///
170
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct DiagnosticFailure {
174    /// Stable diagnostic category.
175    pub code: DiagnosticCode,
176    /// Human-readable failure detail.
177    pub message: String,
178}
179
180impl DiagnosticFailure {
181    /// Build a coded diagnostic failure.
182    #[must_use]
183    pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
184        Self {
185            code,
186            message: message.into(),
187        }
188    }
189}
190
191///
192/// DiagnosticRangeAuthority
193///
194/// Read-only diagnostic view of registered and effective range authority.
195///
196
197#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198#[serde(deny_unknown_fields)]
199pub struct DiagnosticRangeAuthority {
200    /// Range records registered directly by linked crates.
201    pub registered_records: Vec<MemoryManagerAuthorityRecord>,
202    /// Validated effective range authority from the sealed declarations.
203    pub effective_authority: MemoryManagerRangeAuthority,
204}
205
206impl DiagnosticRangeAuthority {
207    /// Build a range-authority diagnostic.
208    #[must_use]
209    pub const fn new(
210        registered_records: Vec<MemoryManagerAuthorityRecord>,
211        effective_authority: MemoryManagerRangeAuthority,
212    ) -> Self {
213        Self {
214            registered_records,
215            effective_authority,
216        }
217    }
218}
219
220///
221/// DiagnosticStableCell
222///
223/// Read-only diagnostic view of the stable-cell ledger storage envelope.
224///
225
226#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub struct DiagnosticStableCell {
229    /// Stable-cell status.
230    pub status: DiagnosticStableCellStatus,
231    /// Backing memory size for the ledger cell.
232    pub memory_size: DiagnosticMemorySize,
233}
234
235impl DiagnosticStableCell {
236    /// Build a stable-cell diagnostic.
237    #[must_use]
238    pub const fn new(
239        status: DiagnosticStableCellStatus,
240        memory_size: DiagnosticMemorySize,
241    ) -> Self {
242        Self {
243            status,
244            memory_size,
245        }
246    }
247}
248
249///
250/// DiagnosticStableCellStatus
251///
252/// Stable-cell ledger storage status.
253///
254
255#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
256#[serde(deny_unknown_fields)]
257pub enum DiagnosticStableCellStatus {
258    /// The ledger memory is empty and can be initialized.
259    Empty,
260    /// The stable-cell envelope and ledger record decoded successfully.
261    Readable,
262    /// The ledger memory is present but could not be decoded as the expected
263    /// stable-cell ledger record.
264    Corrupt {
265        /// Stable-cell envelope or ledger-record decode failure.
266        failure: DiagnosticFailure,
267    },
268}
269
270///
271/// DiagnosticCheck
272///
273/// Read-only diagnostic status for a preflight check.
274///
275
276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
277#[serde(deny_unknown_fields)]
278pub enum DiagnosticCheck {
279    /// The check could not run because prerequisite state was unavailable.
280    NotRun {
281        /// Stable diagnostic category.
282        code: DiagnosticCode,
283        /// Reason the check could not run.
284        message: String,
285    },
286    /// The check completed successfully.
287    Passed,
288    /// The check ran and found a problem.
289    Failed {
290        /// Stable diagnostic category.
291        code: DiagnosticCode,
292        /// Validation failure.
293        message: String,
294    },
295}
296
297impl DiagnosticCheck {
298    /// Build a passed diagnostic check.
299    #[must_use]
300    pub const fn passed() -> Self {
301        Self::Passed
302    }
303
304    /// Build a failed diagnostic check.
305    #[must_use]
306    pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
307        Self::Failed {
308            code,
309            message: message.into(),
310        }
311    }
312
313    /// Build a skipped diagnostic check.
314    #[must_use]
315    pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
316        Self::NotRun {
317            code,
318            message: message.into(),
319        }
320    }
321}
322
323impl DiagnosticExport {
324    /// Build a read-only diagnostic export from an allocation ledger.
325    #[must_use]
326    pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
327        Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
328    }
329
330    /// Build a read-only diagnostic export with protected commit recovery state.
331    #[must_use]
332    pub fn from_ledger_with_commit_recovery(
333        ledger: &AllocationLedger,
334        ledger_anchor: AllocationSlotDescriptor,
335        commit_recovery: Option<CommitStoreDiagnostic>,
336    ) -> Self {
337        Self::from_ledger_with_commit_recovery_and_memory_sizes(
338            ledger,
339            ledger_anchor,
340            commit_recovery,
341            std::iter::empty(),
342        )
343    }
344
345    /// Build a read-only diagnostic export with live memory sizes.
346    #[must_use]
347    pub fn from_ledger_with_memory_sizes(
348        ledger: &AllocationLedger,
349        ledger_anchor: AllocationSlotDescriptor,
350        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
351    ) -> Self {
352        Self::from_ledger_with_commit_recovery_and_memory_sizes(
353            ledger,
354            ledger_anchor,
355            None,
356            memory_sizes,
357        )
358    }
359
360    /// Build a read-only diagnostic export with protected recovery state and live memory sizes.
361    #[must_use]
362    pub fn from_ledger_with_commit_recovery_and_memory_sizes(
363        ledger: &AllocationLedger,
364        ledger_anchor: AllocationSlotDescriptor,
365        commit_recovery: Option<CommitStoreDiagnostic>,
366        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
367    ) -> Self {
368        Self::from_ledger_with_commit_recovery_and_memory_size_outcomes(
369            ledger,
370            ledger_anchor,
371            commit_recovery,
372            memory_sizes
373                .into_iter()
374                .map(|(slot, size)| (slot, DiagnosticMemorySizeOutcome::Measured(size))),
375        )
376    }
377
378    pub(crate) fn from_ledger_with_commit_recovery_and_memory_size_outcomes(
379        ledger: &AllocationLedger,
380        ledger_anchor: AllocationSlotDescriptor,
381        commit_recovery: Option<CommitStoreDiagnostic>,
382        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySizeOutcome)>,
383    ) -> Self {
384        let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
385        Self {
386            current_generation: ledger.current_generation,
387            ledger_anchor,
388            records: ledger
389                .allocation_history()
390                .records()
391                .iter()
392                .cloned()
393                .map(|allocation| {
394                    let memory_size = memory_sizes.get(allocation.slot()).cloned();
395                    DiagnosticRecord {
396                        allocation,
397                        memory_size,
398                    }
399                })
400                .collect(),
401            generations: ledger
402                .allocation_history()
403                .generations()
404                .iter()
405                .cloned()
406                .map(|generation| DiagnosticGeneration { generation })
407                .collect(),
408            commit_recovery,
409        }
410    }
411}
412
413///
414/// DiagnosticRecord
415///
416/// Read-only diagnostic allocation record.
417#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct DiagnosticRecord {
420    /// Allocation record.
421    pub allocation: AllocationRecord,
422    /// Live backing memory size, when the exporter measured one.
423    ///
424    /// This is allocation size reported by the backing memory, not logical user
425    /// payload size inside the stable structure.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub memory_size: Option<DiagnosticMemorySizeOutcome>,
428}
429
430///
431/// DiagnosticMemorySizeOutcome
432///
433/// Per-allocation result of measuring live backing-memory size.
434///
435/// Diagnostic DTO producers can report measurement failures. Runtime reports
436/// measure only allocations whose slots passed ledger recovery; invalid slots
437/// are recovery failures rather than per-allocation measurement outcomes.
438///
439
440#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
441pub enum DiagnosticMemorySizeOutcome {
442    /// The backing memory reported a live size.
443    Measured(DiagnosticMemorySize),
444    /// The allocation slot could not be measured.
445    Failed(DiagnosticFailure),
446}
447
448///
449/// DiagnosticMemorySize
450///
451/// Live size reported by a backing stable memory.
452///
453
454#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
455#[serde(deny_unknown_fields)]
456pub struct DiagnosticMemorySize {
457    /// WebAssembly pages reported by the memory.
458    pub wasm_pages: u64,
459    /// Bytes represented by the page count.
460    pub bytes: u64,
461}
462
463impl DiagnosticMemorySize {
464    /// Build a size from a WebAssembly page count.
465    #[must_use]
466    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
467        Self {
468            wasm_pages,
469            bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
470        }
471    }
472}
473
474///
475/// DiagnosticGeneration
476///
477/// Read-only diagnostic generation record.
478#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
479#[serde(deny_unknown_fields)]
480pub struct DiagnosticGeneration {
481    /// Generation record.
482    pub generation: GenerationRecord,
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use crate::{
489        declaration::AllocationDeclaration,
490        ledger::{AllocationHistory, AllocationRecord},
491        physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
492        schema::SchemaMetadata,
493    };
494
495    #[test]
496    fn diagnostic_export_copies_ledger_records() {
497        let declaration = AllocationDeclaration::new(
498            "app.users.v1",
499            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
500            None,
501            SchemaMetadata::default(),
502        )
503        .expect("declaration");
504        let ledger = AllocationLedger {
505            current_generation: 3,
506            allocation_history: AllocationHistory::from_parts(
507                vec![AllocationRecord::active(3, declaration)],
508                vec![GenerationRecord {
509                    generation: 3,
510                    parent_generation: 2,
511                    runtime_fingerprint: Some("wasm:abc123".to_string()),
512                    declaration_count: 1,
513                    committed_at: None,
514                }],
515            ),
516        };
517
518        let export = DiagnosticExport::from_ledger(
519            &ledger,
520            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
521        );
522
523        assert_eq!(export.current_generation, 3);
524        assert_eq!(export.records.len(), 1);
525        assert_eq!(export.records[0].memory_size, None);
526        assert_eq!(export.generations.len(), 1);
527        assert_eq!(
528            export.ledger_anchor,
529            AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
530        );
531        assert_eq!(export.commit_recovery, None);
532    }
533
534    #[test]
535    fn diagnostic_export_rejects_unknown_top_level_fields() {
536        use crate::test_cbor::Value;
537
538        let export = DiagnosticExport {
539            current_generation: 0,
540            ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
541            records: Vec::new(),
542            generations: Vec::new(),
543            commit_recovery: None,
544        };
545        let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
546        else {
547            panic!("diagnostic export encodes as a map");
548        };
549        crate::test_cbor::map_insert(
550            &mut map,
551            Value::Text("future_field".to_string()),
552            Value::Bool(true),
553        );
554        let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
555
556        let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
557            .expect_err("unknown diagnostic field must fail closed");
558
559        assert!(err.to_string().contains("future_field"));
560    }
561
562    #[test]
563    fn diagnostic_outcome_states_round_trip() {
564        let stable_cell = DiagnosticStableCell::new(
565            DiagnosticStableCellStatus::Corrupt {
566                failure: DiagnosticFailure::new(
567                    DiagnosticCode::StableCell,
568                    "bad stable-cell record",
569                ),
570            },
571            DiagnosticMemorySize::from_wasm_pages(1),
572        );
573        let range_authority =
574            DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
575        let check = DiagnosticCheck::failed(
576            DiagnosticCode::AllocationValidation,
577            "duplicate declaration",
578        );
579
580        for value in [DiagnosticCheck::passed(), check] {
581            let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
582            let decoded: DiagnosticCheck =
583                crate::test_cbor::from_slice(&bytes).expect("check round trip");
584            assert_eq!(decoded, value);
585        }
586
587        let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
588        let decoded: DiagnosticStableCell =
589            crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
590        assert_eq!(decoded, stable_cell);
591
592        let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
593        let decoded: DiagnosticRangeAuthority =
594            crate::test_cbor::from_slice(&bytes).expect("range round trip");
595        assert_eq!(decoded, range_authority);
596    }
597
598    #[test]
599    fn diagnostic_codes_have_stable_wire_names() {
600        let cases = [
601            (DiagnosticCode::StableCell, "stable_cell"),
602            (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
603            (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
604            (
605                DiagnosticCode::AllocationValidation,
606                "allocation_validation",
607            ),
608            (DiagnosticCode::PolicyIdentity, "policy_identity"),
609            (DiagnosticCode::RuntimeBinding, "runtime_binding"),
610            (DiagnosticCode::MemorySize, "memory_size"),
611        ];
612
613        for (code, expected) in cases {
614            assert_eq!(
615                crate::test_cbor::to_value(code).expect("diagnostic code value"),
616                crate::test_cbor::Value::Text(expected.to_string())
617            );
618        }
619    }
620
621    #[test]
622    fn diagnostic_export_can_include_commit_recovery_state() {
623        let ledger = AllocationLedger {
624            current_generation: 3,
625            allocation_history: AllocationHistory::default(),
626        };
627        let commit_recovery = CommitStoreDiagnostic {
628            slot0: CommitSlotDiagnostic::Valid { generation: 3 },
629            slot1: CommitSlotDiagnostic::Empty,
630            recovery: Ok(3),
631        };
632
633        let export = DiagnosticExport::from_ledger_with_commit_recovery(
634            &ledger,
635            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
636            Some(commit_recovery),
637        );
638
639        assert_eq!(export.commit_recovery, Some(commit_recovery));
640    }
641
642    #[test]
643    fn diagnostic_export_can_include_memory_sizes() {
644        let declaration = AllocationDeclaration::new(
645            "app.users.v1",
646            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
647            None,
648            SchemaMetadata::default(),
649        )
650        .expect("declaration");
651        let ledger = AllocationLedger {
652            current_generation: 3,
653            allocation_history: AllocationHistory::from_parts(
654                vec![AllocationRecord::active(3, declaration)],
655                Vec::new(),
656            ),
657        };
658
659        let export = DiagnosticExport::from_ledger_with_memory_sizes(
660            &ledger,
661            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
662            [(
663                AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
664                DiagnosticMemorySize::from_wasm_pages(2),
665            )],
666        );
667
668        assert_eq!(
669            export.records[0].memory_size,
670            Some(DiagnosticMemorySizeOutcome::Measured(
671                DiagnosticMemorySize {
672                    wasm_pages: 2,
673                    bytes: 131_072,
674                }
675            ))
676        );
677    }
678
679    #[test]
680    fn diagnostic_export_preserves_per_slot_size_successes_and_failures() {
681        let users = AllocationDeclaration::memory_manager("app.users.v1", 100, "users")
682            .expect("users declaration");
683        let orders = AllocationDeclaration::memory_manager("app.orders.v1", 101, "orders")
684            .expect("orders declaration");
685        let ledger = AllocationLedger {
686            current_generation: 3,
687            allocation_history: AllocationHistory::from_parts(
688                vec![
689                    AllocationRecord::active(3, users),
690                    AllocationRecord::active(3, orders),
691                ],
692                Vec::new(),
693            ),
694        };
695        let size_failure =
696            DiagnosticFailure::new(DiagnosticCode::MemorySize, "slot could not be measured");
697
698        let export = DiagnosticExport::from_ledger_with_commit_recovery_and_memory_size_outcomes(
699            &ledger,
700            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
701            None,
702            [
703                (
704                    AllocationSlotDescriptor::memory_manager(100).expect("users slot"),
705                    DiagnosticMemorySizeOutcome::Measured(DiagnosticMemorySize::from_wasm_pages(2)),
706                ),
707                (
708                    AllocationSlotDescriptor::memory_manager(101).expect("orders slot"),
709                    DiagnosticMemorySizeOutcome::Failed(size_failure.clone()),
710                ),
711            ],
712        );
713
714        assert_eq!(
715            export.records[0].memory_size,
716            Some(DiagnosticMemorySizeOutcome::Measured(
717                DiagnosticMemorySize::from_wasm_pages(2)
718            ))
719        );
720        assert_eq!(
721            export.records[1].memory_size,
722            Some(DiagnosticMemorySizeOutcome::Failed(size_failure))
723        );
724    }
725
726    #[test]
727    fn diagnostic_export_can_report_recovery_failure() {
728        let ledger = AllocationLedger {
729            current_generation: 0,
730            allocation_history: AllocationHistory::default(),
731        };
732        let commit_recovery = CommitStoreDiagnostic {
733            slot0: CommitSlotDiagnostic::Empty,
734            slot1: CommitSlotDiagnostic::Empty,
735            recovery: Err(CommitRecoveryError::NoValidGeneration),
736        };
737
738        let export = DiagnosticExport::from_ledger_with_commit_recovery(
739            &ledger,
740            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
741            Some(commit_recovery),
742        );
743
744        assert_eq!(
745            export.commit_recovery.expect("commit recovery").recovery,
746            Err(CommitRecoveryError::NoValidGeneration)
747        );
748    }
749}