Skip to main content

ic_memory/ledger/
mod.rs

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