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