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
20pub trait LedgerCodec {
32 type Error;
34
35 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
37
38 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
40}
41
42#[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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
85pub struct LedgerCommitStore {
86 physical: DualCommitStore,
88}
89
90impl LedgerCommitStore {
91 #[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 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 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 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 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 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 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 #[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, MEMORY_MANAGER_INVALID_ID, MemoryManagerSlotError},
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_committed_integrity_rejects_decoded_invalid_stable_key() {
1090 let mut ledger = committed_ledger(1);
1091 ledger
1092 .allocation_history
1093 .records
1094 .push(active_record("app.users.v1", 100));
1095 let mut bytes = serde_cbor::to_vec(&ledger).expect("encode ledger");
1096 let key_start = bytes
1097 .windows(b"app.users.v1".len())
1098 .position(|window| window == b"app.users.v1")
1099 .expect("encoded stable key");
1100 bytes[key_start] = b'A';
1101 let decoded: AllocationLedger = serde_cbor::from_slice(&bytes).expect("decode ledger");
1102
1103 let err = decoded
1104 .validate_committed_integrity()
1105 .expect_err("invalid decoded key must fail");
1106
1107 assert!(matches!(err, LedgerIntegrityError::InvalidStableKey(_)));
1108 }
1109
1110 #[test]
1111 fn validate_committed_integrity_rejects_decoded_invalid_memory_manager_slot() {
1112 let mut ledger = committed_ledger(1);
1113 let mut record = active_record("app.users.v1", 100);
1114 record.slot = AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_INVALID_ID);
1115 ledger.allocation_history.records.push(record);
1116
1117 let err = ledger
1118 .validate_committed_integrity()
1119 .expect_err("invalid decoded slot must fail");
1120
1121 assert!(matches!(
1122 err,
1123 LedgerIntegrityError::InvalidSlotDescriptor(
1124 crate::slot::AllocationSlotDescriptorError::MemoryManager(
1125 MemoryManagerSlotError::InvalidMemoryManagerId { id }
1126 )
1127 ) if id == MEMORY_MANAGER_INVALID_ID
1128 ));
1129 }
1130
1131 #[test]
1132 fn validate_integrity_rejects_retired_record_without_retired_generation() {
1133 let mut ledger = ledger();
1134 let mut record = active_record("app.users.v1", 100);
1135 record.state = AllocationState::Retired;
1136 *ledger.allocation_history.records_mut() = vec![record];
1137
1138 let err = ledger
1139 .validate_integrity()
1140 .expect_err("missing retired generation");
1141
1142 assert!(matches!(
1143 err,
1144 LedgerIntegrityError::MissingRetiredGeneration { .. }
1145 ));
1146 }
1147
1148 #[test]
1149 fn validate_integrity_rejects_non_retired_record_with_retired_generation() {
1150 let mut ledger = ledger();
1151 let mut record = active_record("app.users.v1", 100);
1152 record.retired_generation = Some(2);
1153 *ledger.allocation_history.records_mut() = vec![record];
1154
1155 let err = ledger
1156 .validate_integrity()
1157 .expect_err("unexpected retired generation");
1158
1159 assert!(matches!(
1160 err,
1161 LedgerIntegrityError::UnexpectedRetiredGeneration { .. }
1162 ));
1163 }
1164
1165 #[test]
1166 fn validate_integrity_rejects_non_increasing_schema_history() {
1167 let mut ledger = ledger();
1168 let mut record = active_record("app.users.v1", 100);
1169 record.schema_history.push(SchemaMetadataRecord {
1170 generation: 1,
1171 schema: SchemaMetadata::default(),
1172 });
1173 *ledger.allocation_history.records_mut() = vec![record];
1174
1175 let err = ledger
1176 .validate_integrity()
1177 .expect_err("non-increasing schema history");
1178
1179 assert!(matches!(
1180 err,
1181 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1182 ));
1183 }
1184
1185 #[test]
1186 fn validate_integrity_rejects_invalid_schema_metadata_history() {
1187 let mut ledger = committed_ledger(1);
1188 let mut record = active_record("app.users.v1", 100);
1189 record.schema_history[0].schema = invalid_schema_metadata();
1190 *ledger.allocation_history.records_mut() = vec![record];
1191
1192 let err = ledger
1193 .validate_committed_integrity()
1194 .expect_err("invalid committed schema metadata");
1195
1196 assert_eq!(
1197 err,
1198 LedgerIntegrityError::InvalidSchemaMetadata {
1199 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1200 generation: 1,
1201 error: SchemaMetadataError::InvalidVersion,
1202 }
1203 );
1204 }
1205
1206 #[test]
1207 fn validate_committed_integrity_requires_current_generation_record() {
1208 let err = ledger()
1209 .validate_committed_integrity()
1210 .expect_err("missing current generation");
1211
1212 assert_eq!(
1213 err,
1214 LedgerIntegrityError::MissingCurrentGenerationRecord {
1215 current_generation: 3
1216 }
1217 );
1218 }
1219
1220 #[test]
1221 fn validate_committed_integrity_rejects_generation_history_gaps() {
1222 let mut ledger = committed_ledger(3);
1223 ledger.allocation_history.generations_mut().remove(1);
1224
1225 let err = ledger
1226 .validate_committed_integrity()
1227 .expect_err("generation history gap");
1228
1229 assert!(matches!(
1230 err,
1231 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1232 ));
1233 }
1234
1235 #[test]
1236 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1237 let mut store = LedgerCommitStore::default();
1238 let codec = TestCodec;
1239 let mut invalid = ledger();
1240 *invalid.allocation_history.records_mut() = vec![
1241 active_record("app.users.v1", 100),
1242 active_record("app.orders.v1", 100),
1243 ];
1244
1245 let err = store.commit(&invalid, &codec).expect_err("invalid ledger");
1246
1247 assert!(matches!(
1248 err,
1249 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1250 ));
1251 assert!(store.physical().is_uninitialized());
1252 }
1253
1254 #[test]
1255 fn ledger_commit_store_recovers_latest_committed_ledger() {
1256 let mut store = LedgerCommitStore::default();
1257 let codec = TestCodec;
1258 let first = committed_ledger(1);
1259 let second = committed_ledger(2);
1260
1261 store.commit(&first, &codec).expect("first commit");
1262 store.commit(&second, &codec).expect("second commit");
1263 let recovered = store.recover(&codec).expect("recovered ledger");
1264
1265 assert_eq!(recovered.current_generation, 2);
1266 }
1267
1268 #[test]
1269 fn ledger_commit_store_recovers_compatible_genesis_and_first_real_commit() {
1270 let mut store = LedgerCommitStore::default();
1271 let codec = FullLedgerCodec::default();
1272 let genesis = committed_ledger(0);
1273
1274 let recovered = store
1275 .recover_or_initialize(&codec, &genesis)
1276 .expect("compatible genesis ledger");
1277 assert_eq!(recovered.current_generation, 0);
1278 assert!(recovered.allocation_history.generations().is_empty());
1279
1280 let first = recovered
1281 .stage_validated_generation(
1282 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1283 None,
1284 )
1285 .expect("first real generation");
1286 let recovered = store.commit(&first, &codec).expect("first commit");
1287
1288 assert_eq!(recovered.current_generation, 1);
1289 assert_eq!(recovered.allocation_history.generations()[0].generation, 1);
1290 assert_eq!(record(&recovered, "app.users.v1").first_generation, 1);
1291 }
1292
1293 #[test]
1294 fn ledger_commit_store_recovers_full_payload_after_corrupt_latest_slot() {
1295 let mut store = LedgerCommitStore::default();
1296 let codec = FullLedgerCodec::default();
1297 let genesis = committed_ledger(0);
1298 store.commit(&genesis, &codec).expect("genesis commit");
1299 let first = genesis
1300 .stage_validated_generation(
1301 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1302 None,
1303 )
1304 .expect("first generation");
1305 let first = store.commit(&first, &codec).expect("first commit");
1306 let second = first
1307 .stage_validated_generation(
1308 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1309 None,
1310 )
1311 .expect("second generation");
1312
1313 store
1314 .write_corrupt_inactive_ledger(&second, &codec)
1315 .expect("corrupt latest");
1316 let recovered = store.recover(&codec).expect("recover prior generation");
1317
1318 assert_eq!(recovered.current_generation, 1);
1319 assert_eq!(record(&recovered, "app.users.v1").schema_history.len(), 1);
1320 }
1321
1322 #[test]
1323 fn ledger_commit_store_recovers_identical_duplicate_slots() {
1324 let codec = FullLedgerCodec::default();
1325 let ledger = committed_ledger(0)
1326 .stage_validated_generation(
1327 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1328 None,
1329 )
1330 .expect("first generation");
1331 let payload = codec.encode(&ledger).expect("payload");
1332 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1333 let store = LedgerCommitStore {
1334 physical: DualCommitStore {
1335 slot0: Some(committed.clone()),
1336 slot1: Some(committed),
1337 },
1338 };
1339
1340 let recovered = store.recover(&codec).expect("recovered");
1341
1342 assert_eq!(recovered, ledger);
1343 }
1344
1345 #[test]
1346 fn ledger_commit_store_ignores_corrupt_inactive_ledger() {
1347 let mut store = LedgerCommitStore::default();
1348 let codec = TestCodec;
1349 let first = committed_ledger(1);
1350 let second = committed_ledger(2);
1351
1352 store.commit(&first, &codec).expect("first commit");
1353 store
1354 .write_corrupt_inactive_ledger(&second, &codec)
1355 .expect("corrupt write");
1356 let recovered = store.recover(&codec).expect("recovered ledger");
1357
1358 assert_eq!(recovered.current_generation, 1);
1359 }
1360
1361 #[test]
1362 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1363 let store = LedgerCommitStore {
1364 physical: DualCommitStore {
1365 slot0: Some(CommittedGenerationBytes::new(
1366 7,
1367 TestCodec.encode(&committed_ledger(6)).expect("payload"),
1368 )),
1369 slot1: None,
1370 },
1371 };
1372 let codec = TestCodec;
1373
1374 let err = store.recover(&codec).expect_err("mismatch");
1375
1376 assert_eq!(
1377 err,
1378 LedgerCommitError::PhysicalLogicalGenerationMismatch {
1379 physical_generation: 7,
1380 logical_generation: 6
1381 }
1382 );
1383 }
1384
1385 #[test]
1386 fn ledger_commit_store_rejects_non_next_logical_generation() {
1387 let mut store = LedgerCommitStore::default();
1388 let codec = TestCodec;
1389 store
1390 .commit(&committed_ledger(1), &codec)
1391 .expect("first commit");
1392
1393 let err = store
1394 .commit(&committed_ledger(3), &codec)
1395 .expect_err("skipped generation");
1396
1397 assert_eq!(
1398 err,
1399 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1400 expected: 2,
1401 actual: 3
1402 })
1403 );
1404 }
1405
1406 #[test]
1407 fn ledger_commit_store_initializes_empty_store_explicitly() {
1408 let mut store = LedgerCommitStore::default();
1409 let codec = TestCodec;
1410 let genesis = committed_ledger(3);
1411
1412 let recovered = store
1413 .recover_or_initialize(&codec, &genesis)
1414 .expect("initialized ledger");
1415
1416 assert_eq!(recovered.current_generation, 3);
1417 assert!(!store.physical().is_uninitialized());
1418 }
1419
1420 #[test]
1421 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
1422 let mut store = LedgerCommitStore::default();
1423 let codec = TestCodec;
1424 store
1425 .write_corrupt_inactive_ledger(&ledger(), &codec)
1426 .expect("corrupt write");
1427
1428 let err = store
1429 .recover_or_initialize(&codec, &ledger())
1430 .expect_err("corrupt state");
1431
1432 assert!(matches!(
1433 err,
1434 LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)
1435 ));
1436 }
1437
1438 #[test]
1439 fn ledger_commit_store_rejects_incompatible_schema_before_write() {
1440 let mut store = LedgerCommitStore::default();
1441 let codec = TestCodec;
1442 let incompatible = AllocationLedger {
1443 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1444 ..committed_ledger(0)
1445 };
1446
1447 let err = store
1448 .commit(&incompatible, &codec)
1449 .expect_err("incompatible schema");
1450
1451 assert!(matches!(
1452 err,
1453 LedgerCommitError::Compatibility(
1454 LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1455 )
1456 ));
1457 assert!(store.physical().is_uninitialized());
1458 }
1459
1460 #[test]
1461 fn ledger_commit_store_rejects_incompatible_schema_on_recovery() {
1462 let mut store = LedgerCommitStore::default();
1463 let codec = TestCodec;
1464 let incompatible = AllocationLedger {
1465 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1466 ..committed_ledger(3)
1467 };
1468 let payload = codec.encode(&incompatible).expect("payload");
1469 store
1470 .physical_mut()
1471 .commit_payload_at_generation(incompatible.current_generation, payload)
1472 .expect("physical commit");
1473
1474 let err = store.recover(&codec).expect_err("incompatible schema");
1475
1476 assert!(matches!(
1477 err,
1478 LedgerCommitError::Compatibility(
1479 LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1480 )
1481 ));
1482 }
1483
1484 #[test]
1485 fn ledger_commit_store_rejects_incompatible_physical_format() {
1486 let mut store = LedgerCommitStore::default();
1487 let codec = TestCodec;
1488 let incompatible = AllocationLedger {
1489 physical_format_id: CURRENT_PHYSICAL_FORMAT_ID + 1,
1490 ..committed_ledger(0)
1491 };
1492
1493 let err = store
1494 .recover_or_initialize(&codec, &incompatible)
1495 .expect_err("incompatible format");
1496
1497 assert!(matches!(
1498 err,
1499 LedgerCommitError::Compatibility(
1500 LedgerCompatibilityError::UnsupportedPhysicalFormat { .. }
1501 )
1502 ));
1503 assert!(store.physical().is_uninitialized());
1504 }
1505}