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