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