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