Skip to main content

ic_memory/ledger/
mod.rs

1pub(crate) mod claim;
2mod error;
3mod integrity;
4mod record;
5mod stage;
6
7use crate::physical::{CommitRecoveryError, DualCommitStore};
8use serde::{Deserialize, Serialize};
9
10pub use error::{
11    AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
12    LedgerCompatibilityError, LedgerIntegrityError,
13};
14pub use record::{
15    AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
16    CURRENT_LEDGER_SCHEMA_VERSION, CURRENT_PHYSICAL_FORMAT_ID, GenerationRecord,
17    LedgerCompatibility, SchemaMetadataRecord,
18};
19
20///
21/// LedgerCodec
22///
23/// Integration-supplied encoding for persisted allocation ledgers.
24///
25/// Decoding returns an untrusted durable DTO. Callers should recover ledgers
26/// through [`LedgerCommitStore`], which checks physical/logical generation,
27/// compatibility, and committed ledger integrity before returning authoritative
28/// state.
29///
30
31pub trait LedgerCodec {
32    /// Encoding or decoding error type.
33    type Error;
34
35    /// Encode a logical allocation ledger into durable bytes.
36    fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
37
38    /// Decode durable bytes into a logical allocation ledger.
39    fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
40}
41
42///
43/// CborLedgerCodec
44///
45/// Native CBOR ledger codec for persisted [`AllocationLedger`] payloads.
46///
47/// This is the default codec for the native IC stack:
48/// `MemoryManager` ID 0 stores an `ic-stable-structures::Cell` containing a
49/// [`crate::StableCellLedgerRecord`], whose [`LedgerCommitStore`] contains
50/// dual protected CBOR-encoded ledger generations.
51///
52
53#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54pub struct CborLedgerCodec;
55
56impl LedgerCodec for CborLedgerCodec {
57    type Error = serde_cbor::Error;
58
59    fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
60        serde_cbor::to_vec(ledger)
61    }
62
63    fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
64        serde_cbor::from_slice(bytes)
65    }
66}
67
68///
69/// LedgerCommitStore
70///
71/// Generation-scoped allocation ledger commit store.
72///
73/// This type owns the generic commit lifecycle. It deliberately does not own
74/// serialization or stable-memory IO; those remain substrate/integration
75/// responsibilities.
76///
77/// This store commits allocation ledger generations. It does not open
78/// stable-memory handles and does not allocate application slots.
79///
80/// TODO: Move commit/recovery behavior to `ledger::commit` once the staging
81/// split is also mechanical, so the commit tests can move with their fixtures.
82///
83
84#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
85pub struct LedgerCommitStore {
86    /// Protected physical commit slots.
87    physical: DualCommitStore,
88}
89
90impl LedgerCommitStore {
91    /// Borrow the protected physical commit store for diagnostics.
92    #[must_use]
93    pub const fn physical(&self) -> &DualCommitStore {
94        &self.physical
95    }
96
97    #[cfg(test)]
98    pub(crate) const fn physical_mut(&mut self) -> &mut DualCommitStore {
99        &mut self.physical
100    }
101
102    /// Recover the authoritative allocation ledger using `codec`.
103    pub fn recover<C: LedgerCodec>(
104        &self,
105        codec: &C,
106    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
107        self.recover_with_compatibility(codec, LedgerCompatibility::current())
108    }
109
110    /// Recover the authoritative allocation ledger using explicit compatibility rules.
111    pub fn recover_with_compatibility<C: LedgerCodec>(
112        &self,
113        codec: &C,
114        compatibility: LedgerCompatibility,
115    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
116        let committed = self
117            .physical
118            .authoritative()
119            .map_err(LedgerCommitError::Recovery)?;
120        let ledger = codec
121            .decode(committed.payload())
122            .map_err(LedgerCommitError::Codec)?;
123        if committed.generation() != ledger.current_generation {
124            return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
125                physical_generation: committed.generation(),
126                logical_generation: ledger.current_generation,
127            });
128        }
129        compatibility
130            .validate(&ledger)
131            .map_err(LedgerCommitError::Compatibility)?;
132        ledger
133            .validate_committed_integrity()
134            .map_err(LedgerCommitError::Integrity)?;
135        Ok(ledger)
136    }
137
138    /// Recover the authoritative ledger, or explicitly initialize an empty store.
139    ///
140    /// Initialization is allowed only when no physical commit slot has ever
141    /// been written. Corrupt or partially written stores fail closed even when
142    /// a genesis ledger is supplied.
143    pub fn recover_or_initialize<C: LedgerCodec>(
144        &mut self,
145        codec: &C,
146        genesis: &AllocationLedger,
147    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
148        self.recover_or_initialize_with_compatibility(
149            codec,
150            genesis,
151            LedgerCompatibility::current(),
152        )
153    }
154
155    /// Recover the authoritative ledger, or initialize an empty store with explicit compatibility.
156    pub fn recover_or_initialize_with_compatibility<C: LedgerCodec>(
157        &mut self,
158        codec: &C,
159        genesis: &AllocationLedger,
160        compatibility: LedgerCompatibility,
161    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
162        match self.recover_with_compatibility(codec, compatibility) {
163            Ok(ledger) => Ok(ledger),
164            Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration))
165                if self.physical.is_uninitialized() =>
166            {
167                self.commit_with_compatibility(genesis, codec, compatibility)
168            }
169            Err(err) => Err(err),
170        }
171    }
172
173    /// Commit one logical allocation ledger generation through `codec`.
174    pub fn commit<C: LedgerCodec>(
175        &mut self,
176        ledger: &AllocationLedger,
177        codec: &C,
178    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
179        self.commit_with_compatibility(ledger, codec, LedgerCompatibility::current())
180    }
181
182    /// Commit one logical allocation ledger generation through explicit compatibility.
183    pub fn commit_with_compatibility<C: LedgerCodec>(
184        &mut self,
185        ledger: &AllocationLedger,
186        codec: &C,
187        compatibility: LedgerCompatibility,
188    ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
189        compatibility
190            .validate(ledger)
191            .map_err(LedgerCommitError::Compatibility)?;
192        ledger
193            .validate_committed_integrity()
194            .map_err(LedgerCommitError::Integrity)?;
195        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
196        self.physical
197            .commit_payload_at_generation(ledger.current_generation, payload)
198            .map_err(LedgerCommitError::Recovery)?;
199        self.recover_with_compatibility(codec, compatibility)
200    }
201
202    /// Simulate a torn write of a logical ledger payload into the inactive slot.
203    #[cfg(test)]
204    pub fn write_corrupt_inactive_ledger<C: LedgerCodec>(
205        &mut self,
206        ledger: &AllocationLedger,
207        codec: &C,
208    ) -> Result<(), LedgerCommitError<C::Error>> {
209        let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
210        self.physical
211            .write_corrupt_inactive_slot(ledger.current_generation, payload);
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::{
220        declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
221        key::StableKey,
222        physical::CommittedGenerationBytes,
223        schema::{SchemaMetadata, SchemaMetadataError},
224        slot::AllocationSlotDescriptor,
225    };
226    use std::cell::RefCell;
227
228    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
229    struct TestCodec;
230
231    impl LedgerCodec for TestCodec {
232        type Error = &'static str;
233
234        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
235            let mut bytes = Vec::with_capacity(16);
236            bytes.extend_from_slice(&ledger.ledger_schema_version.to_le_bytes());
237            bytes.extend_from_slice(&ledger.physical_format_id.to_le_bytes());
238            bytes.extend_from_slice(&ledger.current_generation.to_le_bytes());
239            Ok(bytes)
240        }
241
242        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
243            let bytes = <[u8; 16]>::try_from(bytes).map_err(|_| "invalid ledger")?;
244            let ledger_schema_version =
245                u32::from_le_bytes(bytes[0..4].try_into().map_err(|_| "invalid schema")?);
246            let physical_format_id =
247                u32::from_le_bytes(bytes[4..8].try_into().map_err(|_| "invalid format")?);
248            let current_generation =
249                u64::from_le_bytes(bytes[8..16].try_into().map_err(|_| "invalid generation")?);
250            let mut ledger = committed_ledger(current_generation);
251            ledger.ledger_schema_version = ledger_schema_version;
252            ledger.physical_format_id = physical_format_id;
253            Ok(ledger)
254        }
255    }
256
257    #[derive(Debug, Default)]
258    struct FullLedgerCodec {
259        ledgers: RefCell<Vec<AllocationLedger>>,
260    }
261
262    impl LedgerCodec for FullLedgerCodec {
263        type Error = &'static str;
264
265        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
266            let mut ledgers = self.ledgers.borrow_mut();
267            let index = u64::try_from(ledgers.len()).map_err(|_| "too many ledgers")?;
268            ledgers.push(ledger.clone());
269            Ok(index.to_le_bytes().to_vec())
270        }
271
272        fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
273            let bytes = <[u8; 8]>::try_from(bytes).map_err(|_| "invalid ledger index")?;
274            let index =
275                usize::try_from(u64::from_le_bytes(bytes)).map_err(|_| "invalid ledger index")?;
276            self.ledgers
277                .borrow()
278                .get(index)
279                .cloned()
280                .ok_or("unknown ledger index")
281        }
282    }
283
284    fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
285        AllocationDeclaration::new(
286            key,
287            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
288            None,
289            SchemaMetadata {
290                schema_version,
291                schema_fingerprint: None,
292            },
293        )
294        .expect("declaration")
295    }
296
297    fn invalid_schema_metadata() -> SchemaMetadata {
298        SchemaMetadata {
299            schema_version: Some(0),
300            schema_fingerprint: None,
301        }
302    }
303
304    fn declaration_with_invalid_schema(key: &str, id: u8) -> AllocationDeclaration {
305        let mut declaration = declaration(key, id, Some(1));
306        declaration.schema = invalid_schema_metadata();
307        declaration
308    }
309
310    fn ledger() -> AllocationLedger {
311        AllocationLedger {
312            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
313            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
314            current_generation: 3,
315            allocation_history: AllocationHistory::default(),
316        }
317    }
318
319    fn committed_ledger(current_generation: u64) -> AllocationLedger {
320        AllocationLedger {
321            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
322            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
323            current_generation,
324            allocation_history: AllocationHistory::from_parts(
325                Vec::new(),
326                (1..=current_generation)
327                    .map(|generation| GenerationRecord {
328                        generation,
329                        parent_generation: if generation == 1 {
330                            Some(0)
331                        } else {
332                            Some(generation - 1)
333                        },
334                        runtime_fingerprint: None,
335                        declaration_count: 0,
336                        committed_at: None,
337                    })
338                    .collect(),
339            ),
340        }
341    }
342
343    fn active_record(key: &str, id: u8) -> AllocationRecord {
344        AllocationRecord::from_declaration(1, declaration(key, id, None), AllocationState::Active)
345    }
346
347    fn validated(
348        generation: u64,
349        declarations: Vec<AllocationDeclaration>,
350    ) -> crate::session::ValidatedAllocations {
351        crate::session::ValidatedAllocations::new(generation, declarations, None)
352    }
353
354    fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
355        ledger
356            .allocation_history
357            .records()
358            .iter()
359            .find(|record| record.stable_key.as_str() == key)
360            .expect("allocation record")
361    }
362
363    #[test]
364    fn allocation_history_accessors_expose_read_only_views() {
365        let history = AllocationHistory::from_parts(
366            vec![active_record("app.users.v1", 100)],
367            vec![GenerationRecord::new(1, Some(0), None, 1, Some(42)).expect("generation record")],
368        );
369
370        assert!(!history.is_empty());
371        assert_eq!(history.records().len(), 1);
372        assert_eq!(history.generations().len(), 1);
373        assert_eq!(history.generations()[0].committed_at(), Some(42));
374    }
375
376    #[test]
377    fn record_constructors_validate_metadata() {
378        let schema_err = SchemaMetadataRecord::new(1, invalid_schema_metadata())
379            .expect_err("invalid schema must fail");
380        assert_eq!(schema_err, SchemaMetadataError::InvalidVersion);
381
382        let generation_err = GenerationRecord::new(1, Some(0), Some(String::new()), 0, None)
383            .expect_err("empty fingerprint must fail");
384        assert_eq!(
385            generation_err,
386            DeclarationSnapshotError::EmptyRuntimeFingerprint
387        );
388    }
389
390    #[test]
391    fn cbor_ledger_codec_round_trips_allocation_ledger() {
392        let ledger = committed_ledger(2);
393        let codec = CborLedgerCodec;
394
395        let encoded = codec.encode(&ledger).expect("encode ledger");
396        let decoded = codec.decode(&encoded).expect("decode ledger");
397
398        assert_eq!(decoded, ledger);
399    }
400
401    #[test]
402    fn stage_validated_generation_records_new_allocations() {
403        let declarations = vec![declaration("app.users.v1", 100, Some(1))];
404        let validated = validated(3, declarations);
405
406        let staged = ledger()
407            .stage_validated_generation(&validated, Some(42))
408            .expect("staged generation");
409
410        assert_eq!(staged.current_generation, 4);
411        assert_eq!(staged.allocation_history.records().len(), 1);
412        assert_eq!(staged.allocation_history.records()[0].first_generation, 4);
413        assert_eq!(staged.allocation_history.generations()[0].generation, 4);
414        assert_eq!(
415            staged.allocation_history.generations()[0].committed_at,
416            Some(42)
417        );
418    }
419
420    #[test]
421    fn stage_validated_generation_allows_empty_generation_boundary() {
422        let validated = crate::session::ValidatedAllocations::new(
423            3,
424            Vec::new(),
425            Some("test-runtime".to_string()),
426        );
427
428        let staged = ledger()
429            .stage_validated_generation(&validated, Some(42))
430            .expect("empty validated generation");
431
432        assert_eq!(staged.current_generation, 4);
433        assert!(staged.allocation_history.records().is_empty());
434        assert_eq!(staged.allocation_history.generations().len(), 1);
435        assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
436        assert_eq!(
437            staged.allocation_history.generations()[0].parent_generation(),
438            Some(3)
439        );
440        assert_eq!(
441            staged.allocation_history.generations()[0].runtime_fingerprint(),
442            Some("test-runtime")
443        );
444        assert_eq!(
445            staged.allocation_history.generations()[0].declaration_count(),
446            0
447        );
448        assert_eq!(
449            staged.allocation_history.generations()[0].committed_at(),
450            Some(42)
451        );
452    }
453
454    #[test]
455    fn stage_validated_generation_rejects_stale_validated_allocations() {
456        let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
457
458        let err = ledger()
459            .stage_validated_generation(&validated, None)
460            .expect_err("stale validated allocations");
461
462        assert_eq!(
463            err,
464            AllocationStageError::StaleValidatedAllocations {
465                validated_generation: 2,
466                ledger_generation: 3
467            }
468        );
469    }
470
471    #[test]
472    fn stage_validated_generation_rejects_invalid_schema_metadata() {
473        let validated = crate::session::ValidatedAllocations::new(
474            3,
475            vec![declaration_with_invalid_schema("app.users.v1", 100)],
476            None,
477        );
478
479        let err = ledger()
480            .stage_validated_generation(&validated, None)
481            .expect_err("invalid schema metadata");
482
483        assert_eq!(
484            err,
485            AllocationStageError::InvalidSchemaMetadata {
486                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
487                error: SchemaMetadataError::InvalidVersion,
488            }
489        );
490    }
491
492    #[test]
493    fn stage_validated_generation_rejects_generation_overflow() {
494        let ledger = AllocationLedger {
495            current_generation: u64::MAX,
496            ..ledger()
497        };
498        let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
499
500        let err = ledger
501            .stage_validated_generation(&validated, None)
502            .expect_err("overflow must fail");
503
504        assert_eq!(
505            err,
506            AllocationStageError::GenerationOverflow {
507                generation: u64::MAX
508            }
509        );
510    }
511
512    #[test]
513    fn stage_validated_generation_rejects_same_key_different_slot() {
514        let mut ledger = ledger();
515        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
516        let validated = validated(3, vec![declaration("app.users.v1", 101, None)]);
517
518        let err = ledger
519            .stage_validated_generation(&validated, None)
520            .expect_err("stable key cannot move slots");
521
522        assert!(matches!(
523            err,
524            AllocationStageError::StableKeySlotConflict { .. }
525        ));
526    }
527
528    #[test]
529    fn stage_validated_generation_rejects_same_slot_different_key() {
530        let mut ledger = ledger();
531        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
532        let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
533
534        let err = ledger
535            .stage_validated_generation(&validated, None)
536            .expect_err("slot cannot be reused by another key");
537
538        assert!(matches!(
539            err,
540            AllocationStageError::SlotStableKeyConflict { .. }
541        ));
542    }
543
544    #[test]
545    fn stage_validated_generation_rejects_retired_redeclaration() {
546        let mut ledger = ledger();
547        let mut record = active_record("app.users.v1", 100);
548        record.state = AllocationState::Retired;
549        record.retired_generation = Some(3);
550        *ledger.allocation_history.records_mut() = vec![record];
551        let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
552
553        let err = ledger
554            .stage_validated_generation(&validated, None)
555            .expect_err("retired allocation cannot be redeclared");
556
557        assert!(matches!(
558            err,
559            AllocationStageError::RetiredAllocation { .. }
560        ));
561    }
562
563    #[test]
564    fn stage_validated_generation_preserves_omitted_records() {
565        let first = validated(
566            3,
567            vec![
568                declaration("app.users.v1", 100, Some(1)),
569                declaration("app.orders.v1", 101, Some(1)),
570            ],
571        );
572        let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
573
574        let staged = ledger()
575            .stage_validated_generation(&first, None)
576            .expect("first generation");
577        let staged = staged
578            .stage_validated_generation(&second, None)
579            .expect("second generation");
580
581        assert_eq!(staged.current_generation, 5);
582        assert_eq!(staged.allocation_history.records().len(), 2);
583        let omitted = staged
584            .allocation_history
585            .records()
586            .iter()
587            .find(|record| record.stable_key.as_str() == "app.orders.v1")
588            .expect("omitted record");
589        assert_eq!(omitted.state, AllocationState::Active);
590        assert_eq!(omitted.last_seen_generation, 4);
591    }
592
593    #[test]
594    fn stage_validated_generation_records_schema_metadata_history() {
595        let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
596        let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
597
598        let staged = ledger()
599            .stage_validated_generation(&first, None)
600            .expect("first generation");
601        let staged = staged
602            .stage_validated_generation(&second, None)
603            .expect("second generation");
604        let record = &staged.allocation_history.records()[0];
605
606        assert_eq!(record.schema_history.len(), 2);
607        assert_eq!(record.schema_history[0].generation, 4);
608        assert_eq!(record.schema_history[1].generation, 5);
609    }
610
611    #[test]
612    fn stage_reservation_generation_records_reserved_allocations() {
613        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
614
615        let staged = ledger()
616            .stage_reservation_generation(&reservations, Some(42))
617            .expect("reserved generation");
618
619        assert_eq!(staged.current_generation, 4);
620        assert_eq!(staged.allocation_history.records().len(), 1);
621        assert_eq!(
622            staged.allocation_history.records()[0].state,
623            AllocationState::Reserved
624        );
625        assert_eq!(
626            staged.allocation_history.generations()[0].declaration_count,
627            1
628        );
629    }
630
631    #[test]
632    fn stage_reservation_generation_allows_empty_generation_boundary() {
633        let reservations = Vec::new();
634
635        let staged = ledger()
636            .stage_reservation_generation(&reservations, Some(42))
637            .expect("empty reservation generation");
638
639        assert_eq!(staged.current_generation, 4);
640        assert!(staged.allocation_history.records().is_empty());
641        assert_eq!(staged.allocation_history.generations().len(), 1);
642        assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
643        assert_eq!(
644            staged.allocation_history.generations()[0].declaration_count(),
645            0
646        );
647        assert_eq!(
648            staged.allocation_history.generations()[0].committed_at(),
649            Some(42)
650        );
651    }
652
653    #[test]
654    fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
655        let first = vec![declaration("app.future_store.v1", 100, Some(1))];
656        let staged = ledger()
657            .stage_reservation_generation(&first, Some(42))
658            .expect("first reservation generation");
659
660        let second = vec![declaration("app.future_store.v1", 100, Some(2))];
661        let staged = staged
662            .stage_reservation_generation(&second, Some(43))
663            .expect("reservation refresh");
664        let record = record(&staged, "app.future_store.v1");
665
666        assert_eq!(record.state(), AllocationState::Reserved);
667        assert_eq!(record.first_generation(), 4);
668        assert_eq!(record.last_seen_generation(), 5);
669        assert_eq!(record.schema_history().len(), 2);
670        assert_eq!(record.schema_history()[1].generation(), 5);
671        assert_eq!(
672            staged.allocation_history.generations()[1].declaration_count(),
673            1
674        );
675    }
676
677    #[test]
678    fn stage_reservation_generation_rejects_generation_overflow() {
679        let ledger = AllocationLedger {
680            current_generation: u64::MAX,
681            ..ledger()
682        };
683        let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
684
685        let err = ledger
686            .stage_reservation_generation(&reservations, None)
687            .expect_err("overflow must fail");
688
689        assert_eq!(
690            err,
691            AllocationReservationError::GenerationOverflow {
692                generation: u64::MAX
693            }
694        );
695    }
696
697    #[test]
698    fn stage_reservation_generation_rejects_invalid_schema_metadata() {
699        let reservations = vec![declaration_with_invalid_schema(
700            "ic_memory.generation_log.v1",
701            1,
702        )];
703
704        let err = ledger()
705            .stage_reservation_generation(&reservations, None)
706            .expect_err("invalid reservation schema metadata");
707
708        assert_eq!(
709            err,
710            AllocationReservationError::InvalidSchemaMetadata {
711                stable_key: StableKey::parse("ic_memory.generation_log.v1").expect("stable key"),
712                error: SchemaMetadataError::InvalidVersion,
713            }
714        );
715    }
716
717    #[test]
718    fn stage_reservation_generation_rejects_same_key_different_slot() {
719        let mut ledger = ledger();
720        *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
721            3,
722            declaration("app.future_store.v1", 100, None),
723        )];
724        let reservations = vec![declaration("app.future_store.v1", 101, None)];
725
726        let err = ledger
727            .stage_reservation_generation(&reservations, None)
728            .expect_err("reservation key cannot move slots");
729
730        assert!(matches!(
731            err,
732            AllocationReservationError::StableKeySlotConflict { .. }
733        ));
734    }
735
736    #[test]
737    fn stage_reservation_generation_rejects_same_slot_different_key() {
738        let mut ledger = ledger();
739        *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
740            3,
741            declaration("app.future_store.v1", 100, None),
742        )];
743        let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
744
745        let err = ledger
746            .stage_reservation_generation(&reservations, None)
747            .expect_err("reservation slot cannot be reused by another key");
748
749        assert!(matches!(
750            err,
751            AllocationReservationError::SlotStableKeyConflict { .. }
752        ));
753    }
754
755    #[test]
756    fn stage_reservation_generation_rejects_active_allocation() {
757        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
758        let staged = ledger()
759            .stage_validated_generation(&active, None)
760            .expect("active generation");
761        let reservations = vec![declaration("app.users.v1", 100, None)];
762
763        let err = staged
764            .stage_reservation_generation(&reservations, None)
765            .expect_err("active cannot become reserved");
766
767        assert!(matches!(
768            err,
769            AllocationReservationError::ActiveAllocation { .. }
770        ));
771    }
772
773    #[test]
774    fn stage_reservation_generation_rejects_retired_allocation() {
775        let mut ledger = ledger();
776        let mut record = active_record("app.users.v1", 100);
777        record.state = AllocationState::Retired;
778        record.retired_generation = Some(3);
779        *ledger.allocation_history.records_mut() = vec![record];
780        let reservations = vec![declaration("app.users.v1", 100, None)];
781
782        let err = ledger
783            .stage_reservation_generation(&reservations, None)
784            .expect_err("retired cannot revive");
785
786        assert!(matches!(
787            err,
788            AllocationReservationError::RetiredAllocation { .. }
789        ));
790    }
791
792    #[test]
793    fn stage_validated_generation_activates_reserved_record() {
794        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
795        let staged = ledger()
796            .stage_reservation_generation(&reservations, None)
797            .expect("reserved generation");
798        let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
799
800        let staged = staged
801            .stage_validated_generation(&active, None)
802            .expect("active generation");
803        let record = &staged.allocation_history.records()[0];
804
805        assert_eq!(record.state, AllocationState::Active);
806        assert_eq!(record.first_generation, 4);
807        assert_eq!(record.last_seen_generation, 5);
808        assert_eq!(record.schema_history.len(), 2);
809    }
810
811    #[test]
812    fn stage_retirement_generation_tombstones_named_allocation() {
813        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
814        let staged = ledger()
815            .stage_validated_generation(&active, None)
816            .expect("active generation");
817        let retirement = AllocationRetirement::new(
818            "app.users.v1",
819            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
820        )
821        .expect("retirement");
822
823        let staged = staged
824            .stage_retirement_generation(&retirement, Some(42))
825            .expect("retired generation");
826        let record = &staged.allocation_history.records()[0];
827
828        assert_eq!(staged.current_generation, 5);
829        assert_eq!(record.state, AllocationState::Retired);
830        assert_eq!(record.retired_generation, Some(5));
831        assert_eq!(
832            staged.allocation_history.generations()[1].declaration_count,
833            0
834        );
835    }
836
837    #[test]
838    fn stage_retirement_generation_tombstones_reserved_allocation() {
839        let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
840        let staged = ledger()
841            .stage_reservation_generation(&reservations, None)
842            .expect("reserved generation");
843        let retirement = AllocationRetirement::new(
844            "app.future_store.v1",
845            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
846        )
847        .expect("retirement");
848
849        let staged = staged
850            .stage_retirement_generation(&retirement, Some(42))
851            .expect("reserved retirement generation");
852        let record = &staged.allocation_history.records()[0];
853
854        assert_eq!(staged.current_generation, 5);
855        assert_eq!(record.state, AllocationState::Retired);
856        assert_eq!(record.first_generation, 4);
857        assert_eq!(record.retired_generation, Some(5));
858        assert_eq!(
859            staged.allocation_history.generations()[1].declaration_count(),
860            0
861        );
862    }
863
864    #[test]
865    fn stage_retirement_generation_rejects_generation_overflow() {
866        let mut ledger = ledger();
867        ledger.current_generation = u64::MAX;
868        *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
869        let retirement = AllocationRetirement::new(
870            "app.users.v1",
871            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
872        )
873        .expect("retirement");
874
875        let err = ledger
876            .stage_retirement_generation(&retirement, None)
877            .expect_err("overflow must fail");
878
879        assert_eq!(
880            err,
881            AllocationRetirementError::GenerationOverflow {
882                generation: u64::MAX
883            }
884        );
885    }
886
887    #[test]
888    fn stage_retirement_generation_requires_matching_slot() {
889        let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
890        let staged = ledger()
891            .stage_validated_generation(&active, None)
892            .expect("active generation");
893        let retirement = AllocationRetirement::new(
894            "app.users.v1",
895            AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
896        )
897        .expect("retirement");
898
899        let err = staged
900            .stage_retirement_generation(&retirement, None)
901            .expect_err("slot mismatch");
902
903        assert!(matches!(
904            err,
905            AllocationRetirementError::SlotMismatch { .. }
906        ));
907    }
908
909    #[test]
910    fn snapshot_can_feed_validated_generation() {
911        let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
912            .expect("snapshot");
913        let (declarations, runtime_fingerprint) = snapshot.into_parts();
914        let validated =
915            crate::session::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
916
917        let staged = ledger()
918            .stage_validated_generation(&validated, None)
919            .expect("validated generation");
920
921        assert_eq!(staged.allocation_history.records().len(), 1);
922    }
923
924    #[test]
925    fn stage_validated_generation_records_runtime_fingerprint() {
926        let validated = crate::session::ValidatedAllocations::new(
927            3,
928            vec![declaration("app.users.v1", 100, None)],
929            Some("wasm:abc123".to_string()),
930        );
931
932        let staged = ledger()
933            .stage_validated_generation(&validated, None)
934            .expect("validated generation");
935
936        assert_eq!(
937            staged.allocation_history.generations()[0].runtime_fingerprint,
938            Some("wasm:abc123".to_string())
939        );
940    }
941
942    #[test]
943    fn strict_committed_integrity_accepts_full_lifecycle() {
944        let mut ledger = committed_ledger(0);
945        ledger
946            .validate_committed_integrity()
947            .expect("genesis ledger with no history");
948
949        ledger = ledger
950            .stage_validated_generation(
951                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
952                Some(1),
953            )
954            .expect("first real commit after genesis");
955        ledger
956            .validate_committed_integrity()
957            .expect("first real commit");
958
959        ledger = ledger
960            .stage_validated_generation(
961                &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
962                Some(2),
963            )
964            .expect("repeated active declaration");
965        ledger
966            .validate_committed_integrity()
967            .expect("repeated active declaration");
968        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
969
970        ledger = ledger
971            .stage_validated_generation(
972                &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
973                Some(3),
974            )
975            .expect("schema drift");
976        ledger
977            .validate_committed_integrity()
978            .expect("schema metadata drift");
979        assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
980
981        ledger = ledger
982            .stage_reservation_generation(
983                &[declaration("app.future_store.v1", 101, Some(1))],
984                Some(4),
985            )
986            .expect("reservation-only generation");
987        ledger
988            .validate_committed_integrity()
989            .expect("reservation-only generation");
990        assert_eq!(
991            record(&ledger, "app.future_store.v1").state,
992            AllocationState::Reserved
993        );
994
995        ledger = ledger
996            .stage_validated_generation(
997                &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
998                Some(5),
999            )
1000            .expect("reservation activation");
1001        ledger
1002            .validate_committed_integrity()
1003            .expect("reservation activation");
1004        assert_eq!(
1005            record(&ledger, "app.future_store.v1").state,
1006            AllocationState::Active
1007        );
1008
1009        let retirement = AllocationRetirement::new(
1010            "app.users.v1",
1011            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1012        )
1013        .expect("retirement");
1014        ledger = ledger
1015            .stage_retirement_generation(&retirement, Some(6))
1016            .expect("retirement generation");
1017        ledger
1018            .validate_committed_integrity()
1019            .expect("retirement generation");
1020        assert_eq!(ledger.current_generation, 6);
1021        assert_eq!(
1022            record(&ledger, "app.users.v1").state,
1023            AllocationState::Retired
1024        );
1025        assert_eq!(
1026            record(&ledger, "app.future_store.v1").last_seen_generation,
1027            5
1028        );
1029    }
1030
1031    #[test]
1032    fn new_committed_requires_strict_generation_history() {
1033        let structurally_valid = AllocationLedger::new(
1034            CURRENT_LEDGER_SCHEMA_VERSION,
1035            CURRENT_PHYSICAL_FORMAT_ID,
1036            3,
1037            AllocationHistory::default(),
1038        )
1039        .expect("structurally valid DTO");
1040
1041        assert_eq!(structurally_valid.current_generation, 3);
1042
1043        let err = AllocationLedger::new_committed(
1044            CURRENT_LEDGER_SCHEMA_VERSION,
1045            CURRENT_PHYSICAL_FORMAT_ID,
1046            3,
1047            AllocationHistory::default(),
1048        )
1049        .expect_err("committed ledger needs generation history");
1050
1051        assert_eq!(
1052            err,
1053            LedgerIntegrityError::MissingCurrentGenerationRecord {
1054                current_generation: 3
1055            }
1056        );
1057    }
1058
1059    #[test]
1060    fn validate_integrity_rejects_duplicate_stable_keys() {
1061        let mut ledger = ledger();
1062        *ledger.allocation_history.records_mut() = vec![
1063            active_record("app.users.v1", 100),
1064            active_record("app.users.v1", 101),
1065        ];
1066
1067        let err = ledger.validate_integrity().expect_err("duplicate key");
1068
1069        assert!(matches!(
1070            err,
1071            LedgerIntegrityError::DuplicateStableKey { .. }
1072        ));
1073    }
1074
1075    #[test]
1076    fn validate_integrity_rejects_duplicate_slots() {
1077        let mut ledger = ledger();
1078        *ledger.allocation_history.records_mut() = vec![
1079            active_record("app.users.v1", 100),
1080            active_record("app.orders.v1", 100),
1081        ];
1082
1083        let err = ledger.validate_integrity().expect_err("duplicate slot");
1084
1085        assert!(matches!(err, LedgerIntegrityError::DuplicateSlot { .. }));
1086    }
1087
1088    #[test]
1089    fn validate_integrity_rejects_retired_record_without_retired_generation() {
1090        let mut ledger = ledger();
1091        let mut record = active_record("app.users.v1", 100);
1092        record.state = AllocationState::Retired;
1093        *ledger.allocation_history.records_mut() = vec![record];
1094
1095        let err = ledger
1096            .validate_integrity()
1097            .expect_err("missing retired generation");
1098
1099        assert!(matches!(
1100            err,
1101            LedgerIntegrityError::MissingRetiredGeneration { .. }
1102        ));
1103    }
1104
1105    #[test]
1106    fn validate_integrity_rejects_non_retired_record_with_retired_generation() {
1107        let mut ledger = ledger();
1108        let mut record = active_record("app.users.v1", 100);
1109        record.retired_generation = Some(2);
1110        *ledger.allocation_history.records_mut() = vec![record];
1111
1112        let err = ledger
1113            .validate_integrity()
1114            .expect_err("unexpected retired generation");
1115
1116        assert!(matches!(
1117            err,
1118            LedgerIntegrityError::UnexpectedRetiredGeneration { .. }
1119        ));
1120    }
1121
1122    #[test]
1123    fn validate_integrity_rejects_non_increasing_schema_history() {
1124        let mut ledger = ledger();
1125        let mut record = active_record("app.users.v1", 100);
1126        record.schema_history.push(SchemaMetadataRecord {
1127            generation: 1,
1128            schema: SchemaMetadata::default(),
1129        });
1130        *ledger.allocation_history.records_mut() = vec![record];
1131
1132        let err = ledger
1133            .validate_integrity()
1134            .expect_err("non-increasing schema history");
1135
1136        assert!(matches!(
1137            err,
1138            LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1139        ));
1140    }
1141
1142    #[test]
1143    fn validate_integrity_rejects_invalid_schema_metadata_history() {
1144        let mut ledger = committed_ledger(1);
1145        let mut record = active_record("app.users.v1", 100);
1146        record.schema_history[0].schema = invalid_schema_metadata();
1147        *ledger.allocation_history.records_mut() = vec![record];
1148
1149        let err = ledger
1150            .validate_committed_integrity()
1151            .expect_err("invalid committed schema metadata");
1152
1153        assert_eq!(
1154            err,
1155            LedgerIntegrityError::InvalidSchemaMetadata {
1156                stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1157                generation: 1,
1158                error: SchemaMetadataError::InvalidVersion,
1159            }
1160        );
1161    }
1162
1163    #[test]
1164    fn validate_committed_integrity_requires_current_generation_record() {
1165        let err = ledger()
1166            .validate_committed_integrity()
1167            .expect_err("missing current generation");
1168
1169        assert_eq!(
1170            err,
1171            LedgerIntegrityError::MissingCurrentGenerationRecord {
1172                current_generation: 3
1173            }
1174        );
1175    }
1176
1177    #[test]
1178    fn validate_committed_integrity_rejects_generation_history_gaps() {
1179        let mut ledger = committed_ledger(3);
1180        ledger.allocation_history.generations_mut().remove(1);
1181
1182        let err = ledger
1183            .validate_committed_integrity()
1184            .expect_err("generation history gap");
1185
1186        assert!(matches!(
1187            err,
1188            LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1189        ));
1190    }
1191
1192    #[test]
1193    fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1194        let mut store = LedgerCommitStore::default();
1195        let codec = TestCodec;
1196        let mut invalid = ledger();
1197        *invalid.allocation_history.records_mut() = vec![
1198            active_record("app.users.v1", 100),
1199            active_record("app.orders.v1", 100),
1200        ];
1201
1202        let err = store.commit(&invalid, &codec).expect_err("invalid ledger");
1203
1204        assert!(matches!(
1205            err,
1206            LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1207        ));
1208        assert!(store.physical().is_uninitialized());
1209    }
1210
1211    #[test]
1212    fn ledger_commit_store_recovers_latest_committed_ledger() {
1213        let mut store = LedgerCommitStore::default();
1214        let codec = TestCodec;
1215        let first = committed_ledger(1);
1216        let second = committed_ledger(2);
1217
1218        store.commit(&first, &codec).expect("first commit");
1219        store.commit(&second, &codec).expect("second commit");
1220        let recovered = store.recover(&codec).expect("recovered ledger");
1221
1222        assert_eq!(recovered.current_generation, 2);
1223    }
1224
1225    #[test]
1226    fn ledger_commit_store_recovers_compatible_genesis_and_first_real_commit() {
1227        let mut store = LedgerCommitStore::default();
1228        let codec = FullLedgerCodec::default();
1229        let genesis = committed_ledger(0);
1230
1231        let recovered = store
1232            .recover_or_initialize(&codec, &genesis)
1233            .expect("compatible genesis ledger");
1234        assert_eq!(recovered.current_generation, 0);
1235        assert!(recovered.allocation_history.generations().is_empty());
1236
1237        let first = recovered
1238            .stage_validated_generation(
1239                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1240                None,
1241            )
1242            .expect("first real generation");
1243        let recovered = store.commit(&first, &codec).expect("first commit");
1244
1245        assert_eq!(recovered.current_generation, 1);
1246        assert_eq!(recovered.allocation_history.generations()[0].generation, 1);
1247        assert_eq!(record(&recovered, "app.users.v1").first_generation, 1);
1248    }
1249
1250    #[test]
1251    fn ledger_commit_store_recovers_full_payload_after_corrupt_latest_slot() {
1252        let mut store = LedgerCommitStore::default();
1253        let codec = FullLedgerCodec::default();
1254        let genesis = committed_ledger(0);
1255        store.commit(&genesis, &codec).expect("genesis commit");
1256        let first = genesis
1257            .stage_validated_generation(
1258                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1259                None,
1260            )
1261            .expect("first generation");
1262        let first = store.commit(&first, &codec).expect("first commit");
1263        let second = first
1264            .stage_validated_generation(
1265                &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1266                None,
1267            )
1268            .expect("second generation");
1269
1270        store
1271            .write_corrupt_inactive_ledger(&second, &codec)
1272            .expect("corrupt latest");
1273        let recovered = store.recover(&codec).expect("recover prior generation");
1274
1275        assert_eq!(recovered.current_generation, 1);
1276        assert_eq!(record(&recovered, "app.users.v1").schema_history.len(), 1);
1277    }
1278
1279    #[test]
1280    fn ledger_commit_store_recovers_identical_duplicate_slots() {
1281        let codec = FullLedgerCodec::default();
1282        let ledger = committed_ledger(0)
1283            .stage_validated_generation(
1284                &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1285                None,
1286            )
1287            .expect("first generation");
1288        let payload = codec.encode(&ledger).expect("payload");
1289        let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1290        let store = LedgerCommitStore {
1291            physical: DualCommitStore {
1292                slot0: Some(committed.clone()),
1293                slot1: Some(committed),
1294            },
1295        };
1296
1297        let recovered = store.recover(&codec).expect("recovered");
1298
1299        assert_eq!(recovered, ledger);
1300    }
1301
1302    #[test]
1303    fn ledger_commit_store_ignores_corrupt_inactive_ledger() {
1304        let mut store = LedgerCommitStore::default();
1305        let codec = TestCodec;
1306        let first = committed_ledger(1);
1307        let second = committed_ledger(2);
1308
1309        store.commit(&first, &codec).expect("first commit");
1310        store
1311            .write_corrupt_inactive_ledger(&second, &codec)
1312            .expect("corrupt write");
1313        let recovered = store.recover(&codec).expect("recovered ledger");
1314
1315        assert_eq!(recovered.current_generation, 1);
1316    }
1317
1318    #[test]
1319    fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1320        let store = LedgerCommitStore {
1321            physical: DualCommitStore {
1322                slot0: Some(CommittedGenerationBytes::new(
1323                    7,
1324                    TestCodec.encode(&committed_ledger(6)).expect("payload"),
1325                )),
1326                slot1: None,
1327            },
1328        };
1329        let codec = TestCodec;
1330
1331        let err = store.recover(&codec).expect_err("mismatch");
1332
1333        assert_eq!(
1334            err,
1335            LedgerCommitError::PhysicalLogicalGenerationMismatch {
1336                physical_generation: 7,
1337                logical_generation: 6
1338            }
1339        );
1340    }
1341
1342    #[test]
1343    fn ledger_commit_store_rejects_non_next_logical_generation() {
1344        let mut store = LedgerCommitStore::default();
1345        let codec = TestCodec;
1346        store
1347            .commit(&committed_ledger(1), &codec)
1348            .expect("first commit");
1349
1350        let err = store
1351            .commit(&committed_ledger(3), &codec)
1352            .expect_err("skipped generation");
1353
1354        assert_eq!(
1355            err,
1356            LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1357                expected: 2,
1358                actual: 3
1359            })
1360        );
1361    }
1362
1363    #[test]
1364    fn ledger_commit_store_initializes_empty_store_explicitly() {
1365        let mut store = LedgerCommitStore::default();
1366        let codec = TestCodec;
1367        let genesis = committed_ledger(3);
1368
1369        let recovered = store
1370            .recover_or_initialize(&codec, &genesis)
1371            .expect("initialized ledger");
1372
1373        assert_eq!(recovered.current_generation, 3);
1374        assert!(!store.physical().is_uninitialized());
1375    }
1376
1377    #[test]
1378    fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
1379        let mut store = LedgerCommitStore::default();
1380        let codec = TestCodec;
1381        store
1382            .write_corrupt_inactive_ledger(&ledger(), &codec)
1383            .expect("corrupt write");
1384
1385        let err = store
1386            .recover_or_initialize(&codec, &ledger())
1387            .expect_err("corrupt state");
1388
1389        assert!(matches!(
1390            err,
1391            LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)
1392        ));
1393    }
1394
1395    #[test]
1396    fn ledger_commit_store_rejects_incompatible_schema_before_write() {
1397        let mut store = LedgerCommitStore::default();
1398        let codec = TestCodec;
1399        let incompatible = AllocationLedger {
1400            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1401            ..committed_ledger(0)
1402        };
1403
1404        let err = store
1405            .commit(&incompatible, &codec)
1406            .expect_err("incompatible schema");
1407
1408        assert!(matches!(
1409            err,
1410            LedgerCommitError::Compatibility(
1411                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1412            )
1413        ));
1414        assert!(store.physical().is_uninitialized());
1415    }
1416
1417    #[test]
1418    fn ledger_commit_store_rejects_incompatible_schema_on_recovery() {
1419        let mut store = LedgerCommitStore::default();
1420        let codec = TestCodec;
1421        let incompatible = AllocationLedger {
1422            ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1423            ..committed_ledger(3)
1424        };
1425        let payload = codec.encode(&incompatible).expect("payload");
1426        store
1427            .physical_mut()
1428            .commit_payload_at_generation(incompatible.current_generation, payload)
1429            .expect("physical commit");
1430
1431        let err = store.recover(&codec).expect_err("incompatible schema");
1432
1433        assert!(matches!(
1434            err,
1435            LedgerCommitError::Compatibility(
1436                LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1437            )
1438        ));
1439    }
1440
1441    #[test]
1442    fn ledger_commit_store_rejects_incompatible_physical_format() {
1443        let mut store = LedgerCommitStore::default();
1444        let codec = TestCodec;
1445        let incompatible = AllocationLedger {
1446            physical_format_id: CURRENT_PHYSICAL_FORMAT_ID + 1,
1447            ..committed_ledger(0)
1448        };
1449
1450        let err = store
1451            .recover_or_initialize(&codec, &incompatible)
1452            .expect_err("incompatible format");
1453
1454        assert!(matches!(
1455            err,
1456            LedgerCommitError::Compatibility(
1457                LedgerCompatibilityError::UnsupportedPhysicalFormat { .. }
1458            )
1459        ));
1460        assert!(store.physical().is_uninitialized());
1461    }
1462}