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