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