1mod claim;
2mod error;
3mod integrity;
4mod payload;
5mod record;
6mod stage;
7
8use crate::physical::{
9 CommitRecoveryError, CommitStoreDiagnostic, CommittedGenerationBytes, DualCommitStore,
10};
11use serde::{Deserialize, Serialize};
12
13pub use claim::{
14 ClaimConflict, ClaimOutcome, ReservationClaimConflict, validate_declaration_claim,
15 validate_reservation_claim,
16};
17pub use error::{
18 AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
19 LedgerIntegrityError,
20};
21pub use payload::{
22 LEDGER_PAYLOAD_FORMAT_VERSION, LedgerPayloadEnvelope, LedgerPayloadEnvelopeError,
23};
24pub use record::{
25 AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
26 GenerationRecord, RecoveredLedger, SchemaMetadataRecord,
27};
28pub use stage::checked_reservation_count;
29pub use stage::{
30 stage_reservation_generation, stage_retirement_generation, stage_validated_generation,
31};
32
33fn decode_ledger(bytes: &[u8]) -> Result<AllocationLedger, String> {
34 crate::cbor::from_slice_exact(bytes).map_err(|err| err.to_string())
35}
36
37#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
50#[serde(deny_unknown_fields)]
51pub struct LedgerCommitStore {
52 physical: DualCommitStore,
54}
55
56impl LedgerCommitStore {
57 #[must_use]
59 pub const fn physical(&self) -> &DualCommitStore {
60 &self.physical
61 }
62
63 pub fn recover(&self) -> Result<RecoveredLedger, LedgerCommitError> {
65 let committed = self
66 .physical
67 .authoritative()
68 .map_err(LedgerCommitError::Recovery)?;
69 Self::recover_committed(committed)
70 }
71
72 pub(crate) fn recover_with_diagnostic(
73 &self,
74 ) -> (
75 Result<RecoveredLedger, LedgerCommitError>,
76 CommitStoreDiagnostic,
77 ) {
78 let (committed, diagnostic) = self.physical.authoritative_with_diagnostic();
79 let recovered = committed
80 .map_err(LedgerCommitError::Recovery)
81 .and_then(Self::recover_committed);
82 (recovered, diagnostic)
83 }
84
85 fn recover_committed(
86 committed: &CommittedGenerationBytes,
87 ) -> Result<RecoveredLedger, LedgerCommitError> {
88 let payload = LedgerPayloadEnvelope::decode_payload(committed.payload())
89 .map_err(LedgerCommitError::PayloadEnvelope)?;
90 let ledger = decode_ledger(payload).map_err(LedgerCommitError::Codec)?;
91 if committed.generation() != ledger.current_generation {
92 return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
93 physical_generation: committed.generation(),
94 logical_generation: ledger.current_generation,
95 });
96 }
97 ledger
98 .validate_committed_integrity()
99 .map_err(LedgerCommitError::Integrity)?;
100 Ok(RecoveredLedger::from_trusted_ledger(ledger))
101 }
102
103 pub fn recover_or_initialize(
112 &mut self,
113 genesis: &AllocationLedger,
114 ) -> Result<RecoveredLedger, LedgerCommitError> {
115 match self.recover() {
116 Ok(ledger) => Ok(ledger),
117 Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)) => {
119 self.commit(genesis)
120 }
121 Err(err) => Err(err),
122 }
123 }
124
125 pub fn commit(
131 &mut self,
132 ledger: &AllocationLedger,
133 ) -> Result<RecoveredLedger, LedgerCommitError> {
134 self.commit_generation(ledger)?;
135 Ok(RecoveredLedger::from_trusted_ledger(ledger.clone()))
139 }
140
141 pub(crate) fn commit_generation(
144 &mut self,
145 ledger: &AllocationLedger,
146 ) -> Result<(), LedgerCommitError> {
147 ledger
148 .validate_committed_integrity()
149 .map_err(LedgerCommitError::Integrity)?;
150 let payload =
151 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
152 self.physical
153 .commit_payload_at_generation(ledger.current_generation, payload)
154 .map(|_| ())
155 .map_err(LedgerCommitError::Recovery)
156 }
157
158 #[cfg(test)]
160 pub fn write_corrupt_inactive_ledger(
161 &mut self,
162 ledger: &AllocationLedger,
163 ) -> Result<(), LedgerCommitError> {
164 let payload =
165 LedgerPayloadEnvelope::encode_ledger(ledger).map_err(LedgerCommitError::Integrity)?;
166 self.physical
167 .write_corrupt_inactive_slot(ledger.current_generation, payload);
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::test_cbor::hex_fixture;
176 use crate::{
177 declaration::{AllocationDeclaration, DeclarationSnapshot, DeclarationSnapshotError},
178 key::StableKey,
179 physical::CommittedGenerationBytes,
180 schema::SchemaMetadata,
181 slot::MemoryManagerSlot,
182 };
183 fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
184 AllocationDeclaration::new(
185 key,
186 MemoryManagerSlot::new(id).expect("usable slot"),
187 None,
188 SchemaMetadata::new(schema_version).expect("schema metadata"),
189 )
190 .expect("declaration")
191 }
192
193 fn ledger() -> AllocationLedger {
194 AllocationLedger {
195 current_generation: 3,
196 allocation_history: AllocationHistory::default(),
197 }
198 }
199
200 fn committed_ledger(current_generation: u64) -> AllocationLedger {
201 AllocationLedger {
202 current_generation,
203 allocation_history: AllocationHistory::from_parts(
204 Vec::new(),
205 (1..=current_generation)
206 .map(|generation| GenerationRecord {
207 generation,
208 parent_generation: if generation == 1 { 0 } else { generation - 1 },
209 runtime_fingerprint: None,
210 declaration_count: 0,
211 committed_at: None,
212 })
213 .collect(),
214 ),
215 }
216 }
217
218 fn active_record(key: &str, id: u8) -> AllocationRecord {
219 AllocationRecord::active(1, &declaration(key, id, None))
220 }
221
222 fn validated(
223 generation: u64,
224 declarations: Vec<AllocationDeclaration>,
225 ) -> crate::capability::ValidatedAllocations {
226 crate::capability::ValidatedAllocations::new(generation, declarations, None)
227 }
228
229 fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
230 ledger
231 .allocation_history
232 .records()
233 .iter()
234 .find(|record| record.stable_key.as_str() == key)
235 .expect("allocation record")
236 }
237
238 fn enveloped_payload(ledger: &AllocationLedger) -> Vec<u8> {
239 LedgerPayloadEnvelope::encode_ledger(ledger).expect("fixture envelope")
240 }
241
242 fn ledger_from_payload_fixture(contents: &str) -> AllocationLedger {
243 let bytes = hex_fixture(contents);
244 let envelope = LedgerPayloadEnvelope::decode(&bytes).expect("fixture envelope");
245
246 let ledger = decode_ledger(envelope.payload()).expect("fixture ledger");
247 ledger
248 .validate_committed_integrity()
249 .expect("fixture ledger integrity");
250 assert_eq!(bytes, enveloped_payload(&ledger));
251 ledger
252 }
253
254 fn store_from_fixture(contents: &str) -> LedgerCommitStore {
255 let bytes = hex_fixture(contents);
256 let store: LedgerCommitStore = crate::test_cbor::from_slice(&bytes).expect("fixture store");
257 assert_eq!(
258 bytes,
259 crate::test_cbor::to_vec(&store).expect("re-encoded fixture store")
260 );
261 store
262 }
263
264 fn active_committed_ledger() -> AllocationLedger {
265 AllocationLedger {
266 current_generation: 1,
267 allocation_history: AllocationHistory::from_parts(
268 vec![active_record("app.users.v1", 100)],
269 vec![GenerationRecord {
270 generation: 1,
271 parent_generation: 0,
272 runtime_fingerprint: None,
273 declaration_count: 1,
274 committed_at: None,
275 }],
276 ),
277 }
278 }
279
280 fn active_ledger_value() -> crate::test_cbor::Value {
281 crate::test_cbor::to_value(active_committed_ledger()).expect("ledger value")
282 }
283
284 #[test]
285 fn recovery_rejects_unknown_fields_inside_retirement_state() {
286 use crate::{AllocationRetirement, StableCellLedgerRecord};
287
288 let active = active_committed_ledger();
289 let retirement =
290 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
291 .unwrap();
292 let retired = active
293 .stage_retirement_generation(&retirement, None)
294 .unwrap();
295 let mut store = LedgerCommitStore::default();
296 let committed = store.commit(&retired).unwrap();
297 assert_eq!(committed, store.recover().unwrap());
298
299 let mut value = crate::test_cbor::to_value(&retired).unwrap();
300 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
301 let records = value_array_mut(map_field_mut(value_map_mut(history), "records"));
302 let state = map_field_mut(value_map_mut(&mut records[0]), "state");
303 let retired_state = map_field_mut(value_map_mut(state), "Retired");
304 value_map_mut(retired_state).push(("unexpected".into(), true.into()));
305 let bytes = crate::test_cbor::to_vec(&value).unwrap();
306 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
307 let mut physical = DualCommitStore::default();
308 physical.commit_payload_at_generation(2, payload).unwrap();
309 let record = StableCellLedgerRecord::new(LedgerCommitStore { physical });
310 let encoded = crate::test_cbor::to_vec(&record).unwrap();
311 let decoded = crate::decode_stable_cell_ledger_record(&encoded).unwrap();
312 let before = decoded.clone();
313 let error = decoded.store().recover().unwrap_err();
314 assert!(matches!(error, LedgerCommitError::Codec(_)));
315 assert!(error.to_string().contains("unexpected"));
316 assert_eq!(decoded, before);
317 }
318
319 fn value_map_mut(
320 value: &mut crate::test_cbor::Value,
321 ) -> &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)> {
322 let crate::test_cbor::Value::Map(map) = value else {
323 panic!("expected CBOR map");
324 };
325 map
326 }
327
328 fn map_field_mut<'map>(
329 map: &'map mut [(crate::test_cbor::Value, crate::test_cbor::Value)],
330 field: &str,
331 ) -> &'map mut crate::test_cbor::Value {
332 map.iter_mut()
333 .find(|(key, _)| key == &crate::test_cbor::Value::Text(field.to_string()))
334 .map(|(_, value)| value)
335 .expect("CBOR map field")
336 }
337
338 fn remove_map_field(
339 map: &mut Vec<(crate::test_cbor::Value, crate::test_cbor::Value)>,
340 field: &str,
341 ) {
342 let field = crate::test_cbor::Value::Text(field.to_string());
343 let index = map
344 .iter()
345 .position(|(key, _)| key == &field)
346 .expect("CBOR map field");
347 map.remove(index);
348 }
349
350 fn value_array_mut(value: &mut crate::test_cbor::Value) -> &mut Vec<crate::test_cbor::Value> {
351 let crate::test_cbor::Value::Array(values) = value else {
352 panic!("expected CBOR array");
353 };
354 values
355 }
356
357 fn decode_mutated_ledger(value: crate::test_cbor::Value) -> String {
358 let bytes = crate::test_cbor::to_vec(&value).expect("mutated ledger bytes");
359 decode_ledger(&bytes).expect_err("mutated ledger must fail closed")
360 }
361
362 #[test]
363 fn allocation_history_accessors_expose_read_only_views() {
364 let history = AllocationHistory::from_parts(
365 vec![active_record("app.users.v1", 100)],
366 vec![GenerationRecord::new(1, 0, None, 1, Some(42)).expect("generation record")],
367 );
368
369 assert!(!history.is_empty());
370 assert_eq!(history.records().len(), 1);
371 assert_eq!(history.generations().len(), 1);
372 assert_eq!(history.generations()[0].committed_at(), Some(42));
373 }
374
375 #[test]
376 fn generation_record_constructor_rejects_empty_runtime_fingerprint() {
377 let generation_err = GenerationRecord::new(1, 0, Some(String::new()), 0, None)
378 .expect_err("empty fingerprint must fail");
379 assert_eq!(
380 generation_err,
381 DeclarationSnapshotError::EmptyRuntimeFingerprint
382 );
383 }
384
385 #[test]
386 fn ledger_cbor_round_trips_allocation_ledger() {
387 let ledger = committed_ledger(2);
388
389 let encoded = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
390 let decoded = decode_ledger(&encoded).expect("decode ledger");
391
392 assert_eq!(decoded, ledger);
393 }
394
395 #[test]
396 fn ledger_cbor_rejects_trailing_bytes() {
397 let mut encoded = crate::test_cbor::to_vec(&committed_ledger(2)).expect("encode ledger");
398 encoded.push(0);
399
400 let err = decode_ledger(&encoded).expect_err("trailing bytes must fail closed");
401
402 assert!(err.contains("trailing bytes"));
403 }
404
405 #[test]
406 fn cbor_ledger_codec_rejects_unknown_top_level_fields() {
407 use crate::test_cbor::Value;
408
409 let map = vec![
410 (
411 Value::Text("current_generation".to_string()),
412 Value::Integer(0.into()),
413 ),
414 (
415 Value::Text("allocation_history".to_string()),
416 crate::test_cbor::to_value(AllocationHistory::default()).expect("history value"),
417 ),
418 (Value::Text("future_field".to_string()), Value::Bool(true)),
419 ];
420 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field ledger");
421
422 let err = decode_ledger(&bytes).expect_err("unknown ledger field must fail closed");
423
424 assert!(err.contains("future_field"));
425 }
426
427 #[test]
428 fn cbor_ledger_codec_rejects_unknown_nested_history_fields() {
429 let mut value = active_ledger_value();
430 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
431 value_map_mut(history).push((
432 crate::test_cbor::Value::Text("future_history_field".to_string()),
433 crate::test_cbor::Value::Bool(true),
434 ));
435
436 let err = decode_mutated_ledger(value);
437
438 assert!(err.contains("future_history_field"));
439 }
440
441 #[test]
442 fn cbor_ledger_codec_rejects_unknown_nested_record_fields() {
443 let mut value = active_ledger_value();
444 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
445 let records = map_field_mut(value_map_mut(history), "records");
446 let record = value_array_mut(records)
447 .first_mut()
448 .expect("allocation record");
449 value_map_mut(record).push((
450 crate::test_cbor::Value::Text("future_record_field".to_string()),
451 crate::test_cbor::Value::Bool(true),
452 ));
453
454 let err = decode_mutated_ledger(value);
455
456 assert!(err.contains("future_record_field"));
457 }
458
459 #[test]
460 fn cbor_ledger_codec_rejects_unknown_nested_slot_fields() {
461 let mut value = active_ledger_value();
462 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
463 let records = map_field_mut(value_map_mut(history), "records");
464 let record = value_array_mut(records)
465 .first_mut()
466 .expect("allocation record");
467 let slot = map_field_mut(value_map_mut(record), "slot");
468 value_map_mut(slot).push((
469 crate::test_cbor::Value::Text("future_slot_field".to_string()),
470 crate::test_cbor::Value::Bool(true),
471 ));
472
473 let err = decode_mutated_ledger(value);
474
475 assert!(err.contains("future_slot_field"));
476 }
477
478 #[test]
479 fn cbor_ledger_codec_rejects_unknown_nested_generation_fields() {
480 let mut value = active_ledger_value();
481 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
482 let generations = map_field_mut(value_map_mut(history), "generations");
483 let generation = value_array_mut(generations)
484 .first_mut()
485 .expect("generation record");
486 value_map_mut(generation).push((
487 crate::test_cbor::Value::Text("future_generation_field".to_string()),
488 crate::test_cbor::Value::Bool(true),
489 ));
490
491 let err = decode_mutated_ledger(value);
492
493 assert!(err.contains("future_generation_field"));
494 }
495
496 #[test]
497 fn cbor_ledger_codec_requires_generation_optional_fields() {
498 for field in ["runtime_fingerprint", "committed_at"] {
499 let mut value = active_ledger_value();
500 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
501 let generations = map_field_mut(value_map_mut(history), "generations");
502 let generation = value_array_mut(generations)
503 .first_mut()
504 .expect("generation record");
505 remove_map_field(value_map_mut(generation), field);
506
507 let err = decode_mutated_ledger(value);
508
509 assert!(err.contains(field));
510 }
511 }
512
513 #[test]
514 fn cbor_ledger_codec_requires_schema_version_field() {
515 let mut value = active_ledger_value();
516 let history = map_field_mut(value_map_mut(&mut value), "allocation_history");
517 let records = map_field_mut(value_map_mut(history), "records");
518 let record = value_array_mut(records)
519 .first_mut()
520 .expect("allocation record");
521 let schema_history = map_field_mut(value_map_mut(record), "schema_history");
522 let schema_record = value_array_mut(schema_history)
523 .first_mut()
524 .expect("schema record");
525 let schema = map_field_mut(value_map_mut(schema_record), "schema");
526 remove_map_field(value_map_mut(schema), "schema_version");
527
528 let err = decode_mutated_ledger(value);
529
530 assert!(err.contains("schema_version"));
531 }
532
533 #[test]
534 fn ledger_commit_store_rejects_unknown_top_level_fields() {
535 use crate::test_cbor::Value;
536
537 let map = vec![
538 (
539 Value::Text("physical".to_string()),
540 crate::test_cbor::to_value(DualCommitStore::default()).expect("physical value"),
541 ),
542 (Value::Text("future_field".to_string()), Value::Bool(true)),
543 ];
544 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field store");
545
546 let err = crate::test_cbor::from_slice::<LedgerCommitStore>(&bytes)
547 .expect_err("unknown store field must fail closed");
548
549 assert!(err.to_string().contains("future_field"));
550 }
551
552 #[test]
553 fn current_empty_genesis_payload_fixture_recovers() {
554 let ledger = ledger_from_payload_fixture(include_str!(
555 "../../fixtures/current/empty_genesis_payload_envelope.hex"
556 ));
557
558 assert_eq!(ledger.current_generation, 0);
559 assert_eq!(ledger.allocation_history.records(), []);
560 assert_eq!(ledger.allocation_history.generations(), []);
561 }
562
563 #[test]
564 fn current_single_active_payload_fixture_recovers() {
565 let ledger = ledger_from_payload_fixture(include_str!(
566 "../../fixtures/current/single_active_allocation_payload_envelope.hex"
567 ));
568 let record = record(&ledger, "app.users.v1");
569
570 assert_eq!(ledger.current_generation, 1);
571 assert_eq!(record.state(), AllocationState::Active);
572 assert_eq!(record.slot().id(), 100);
573 assert_eq!(record.first_generation(), 1);
574 assert_eq!(record.last_seen_generation(), 1);
575 }
576
577 #[test]
578 fn current_reserved_payload_fixture_recovers() {
579 let ledger = ledger_from_payload_fixture(include_str!(
580 "../../fixtures/current/reserved_allocation_payload_envelope.hex"
581 ));
582 let record = record(&ledger, "app.future_store.v1");
583
584 assert_eq!(ledger.current_generation, 1);
585 assert_eq!(record.state(), AllocationState::Reserved);
586 assert_eq!(record.slot().id(), 101);
587 }
588
589 #[test]
590 fn current_retired_payload_fixture_recovers() {
591 let ledger = ledger_from_payload_fixture(include_str!(
592 "../../fixtures/current/retired_allocation_payload_envelope.hex"
593 ));
594 let record = record(&ledger, "app.users.v1");
595
596 assert_eq!(ledger.current_generation, 2);
597 assert_eq!(record.state(), AllocationState::Retired { generation: 2 });
598 }
599
600 #[test]
601 fn current_memory_manager_slot_fixture_decodes() {
602 let bytes = hex_fixture(include_str!(
603 "../../fixtures/current/memory_manager_slot.cbor.hex"
604 ));
605 let slot: MemoryManagerSlot = crate::test_cbor::from_slice(&bytes).expect("slot fixture");
606
607 assert_eq!(slot.id(), 100);
608 assert_eq!(
609 bytes,
610 crate::test_cbor::to_vec(&slot).expect("re-encoded slot")
611 );
612 }
613
614 #[test]
615 fn current_ledger_commit_store_single_active_fixture_recovers() {
616 let store = store_from_fixture(include_str!(
617 "../../fixtures/current/ledger_commit_store_single_active.cbor.hex"
618 ));
619
620 let recovered = store.recover().expect("fixture store recovers");
621 assert_eq!(recovered.current_generation(), 1);
622 assert_eq!(record(recovered.ledger(), "app.users.v1").slot().id(), 100);
623 }
624
625 #[test]
626 fn current_dual_slot_valid_newer_fixture_recovers_newer() {
627 let store = store_from_fixture(include_str!(
628 "../../fixtures/current/dual_slot_store_valid_newer.cbor.hex"
629 ));
630
631 assert_eq!(store.physical().diagnostic().recovery, Ok(2));
632 let recovered = store.recover().expect("fixture store recovers");
633 assert_eq!(recovered.current_generation(), 2);
634 }
635
636 #[test]
637 fn current_dual_slot_corrupt_newer_fixture_fails_closed() {
638 let store = store_from_fixture(include_str!(
639 "../../fixtures/current/dual_slot_store_corrupt_newer.cbor.hex"
640 ));
641
642 let diagnostic = store.physical().diagnostic();
643 assert_eq!(
644 diagnostic.recovery,
645 Err(CommitRecoveryError::InvalidCommitSlots {
646 slot0_invalid: false,
647 slot1_invalid: true,
648 })
649 );
650 assert_eq!(
651 diagnostic.slot1,
652 crate::CommitSlotDiagnostic::Invalid { generation: 2 }
653 );
654 let err = store
655 .recover()
656 .expect_err("corrupt fixture must fail closed");
657 assert!(matches!(
658 err,
659 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
660 slot0_invalid: false,
661 slot1_invalid: true,
662 })
663 ));
664 }
665
666 #[test]
667 fn stage_validated_generation_records_new_allocations() {
668 let declarations = vec![declaration("app.users.v1", 100, Some(1))];
669 let validated = validated(3, declarations);
670
671 let staged = ledger()
672 .stage_validated_generation(&validated, Some(42))
673 .expect("staged generation");
674
675 assert_eq!(staged.current_generation, 4);
676 assert_eq!(staged.allocation_history.records().len(), 1);
677 assert_eq!(staged.allocation_history.records()[0].first_generation, 4);
678 assert_eq!(staged.allocation_history.generations()[0].generation, 4);
679 assert_eq!(
680 staged.allocation_history.generations()[0].committed_at,
681 Some(42)
682 );
683 }
684
685 #[test]
686 fn stage_validated_generation_allows_empty_generation_boundary() {
687 let validated = crate::capability::ValidatedAllocations::new(
688 3,
689 Vec::new(),
690 Some("test-runtime".to_string()),
691 );
692
693 let staged = ledger()
694 .stage_validated_generation(&validated, Some(42))
695 .expect("empty validated generation");
696
697 assert_eq!(staged.current_generation, 4);
698 assert_eq!(staged.allocation_history.records(), []);
699 assert_eq!(staged.allocation_history.generations().len(), 1);
700 assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
701 assert_eq!(
702 staged.allocation_history.generations()[0].parent_generation(),
703 3
704 );
705 assert_eq!(
706 staged.allocation_history.generations()[0].runtime_fingerprint(),
707 Some("test-runtime")
708 );
709 assert_eq!(
710 staged.allocation_history.generations()[0].declaration_count(),
711 0
712 );
713 assert_eq!(
714 staged.allocation_history.generations()[0].committed_at(),
715 Some(42)
716 );
717 }
718
719 #[test]
720 fn stage_validated_generation_rejects_stale_validated_allocations() {
721 let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
722
723 let err = ledger()
724 .stage_validated_generation(&validated, None)
725 .expect_err("stale validated allocations");
726
727 assert_eq!(
728 err,
729 AllocationStageError::StaleValidatedAllocations {
730 validated_generation: 2,
731 ledger_generation: 3
732 }
733 );
734 }
735
736 #[test]
737 fn stage_validated_generation_rejects_overflowing_receiver_as_stale() {
738 let mut store = LedgerCommitStore::default();
739 let recovered = store.commit(&committed_ledger(0)).unwrap();
740 let validated = crate::validate_allocations(
741 &recovered,
742 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, Some(1))]).unwrap(),
743 &crate::GenericRangePolicy,
744 )
745 .unwrap();
746 let ledger = AllocationLedger::new(u64::MAX, AllocationHistory::default()).unwrap();
747 let before = ledger.clone();
748
749 let err = ledger
750 .stage_validated_generation(&validated, None)
751 .expect_err("receiver cannot match a bounded recovered generation");
752
753 assert_eq!(
754 err,
755 AllocationStageError::StaleValidatedAllocations {
756 validated_generation: 0,
757 ledger_generation: u64::MAX,
758 }
759 );
760 assert_eq!(ledger, before);
761 }
762
763 #[test]
764 fn stage_validated_generation_rejects_same_key_different_slot() {
765 let mut ledger = committed_ledger(3);
766 ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
767 ledger.validate_committed_integrity().unwrap();
768 let mut other_store = LedgerCommitStore::default();
771 let other_recovered = other_store.commit(&committed_ledger(3)).unwrap();
772 let validated = crate::validate_allocations(
773 &other_recovered,
774 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101, None)]).unwrap(),
775 &crate::GenericRangePolicy,
776 )
777 .unwrap();
778 let before = ledger.clone();
779
780 let err = ledger
781 .stage_validated_generation(&validated, None)
782 .expect_err("stable key cannot move slots");
783
784 assert!(matches!(
785 err,
786 AllocationStageError::StableKeySlotConflict { .. }
787 ));
788 assert_eq!(ledger, before);
789 }
790
791 #[test]
792 fn stage_validated_generation_rejects_same_slot_different_key() {
793 let mut ledger = ledger();
794 ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
795 let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
796
797 let err = ledger
798 .stage_validated_generation(&validated, None)
799 .expect_err("slot cannot be reused by another key");
800
801 assert!(matches!(
802 err,
803 AllocationStageError::SlotStableKeyConflict { .. }
804 ));
805 }
806
807 #[test]
808 fn stage_validated_generation_rejects_retired_redeclaration() {
809 let mut ledger = ledger();
810 let mut record = active_record("app.users.v1", 100);
811 record.state = AllocationState::Retired { generation: 3 };
812 ledger.allocation_history.records = vec![record];
813 let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
814
815 let err = ledger
816 .stage_validated_generation(&validated, None)
817 .expect_err("retired allocation cannot be redeclared");
818
819 assert!(matches!(
820 err,
821 AllocationStageError::RetiredAllocation { .. }
822 ));
823 }
824
825 #[test]
826 fn stage_validated_generation_preserves_omitted_records() {
827 let first = validated(
828 3,
829 vec![
830 declaration("app.users.v1", 100, Some(1)),
831 declaration("app.orders.v1", 101, Some(1)),
832 ],
833 );
834 let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
835
836 let staged = ledger()
837 .stage_validated_generation(&first, None)
838 .expect("first generation");
839 let staged = staged
840 .stage_validated_generation(&second, None)
841 .expect("second generation");
842
843 assert_eq!(staged.current_generation, 5);
844 assert_eq!(staged.allocation_history.records().len(), 2);
845 let omitted = staged
846 .allocation_history
847 .records()
848 .iter()
849 .find(|record| record.stable_key.as_str() == "app.orders.v1")
850 .expect("omitted record");
851 assert_eq!(omitted.state, AllocationState::Active);
852 assert_eq!(omitted.last_seen_generation, 4);
853 }
854
855 #[test]
856 fn stage_validated_generation_records_schema_metadata_history() {
857 let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
858 let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
859
860 let staged = ledger()
861 .stage_validated_generation(&first, None)
862 .expect("first generation");
863 let staged = staged
864 .stage_validated_generation(&second, None)
865 .expect("second generation");
866 let record = &staged.allocation_history.records()[0];
867
868 assert_eq!(record.schema_history.len(), 2);
869 assert_eq!(record.schema_history[0].generation, 4);
870 assert_eq!(record.schema_history[1].generation, 5);
871 }
872
873 #[test]
874 fn stage_reservation_generation_records_reserved_allocations() {
875 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
876
877 let staged = ledger()
878 .stage_reservation_generation(&reservations, Some(42))
879 .expect("reserved generation");
880
881 assert_eq!(staged.current_generation, 4);
882 assert_eq!(staged.allocation_history.records().len(), 1);
883 assert_eq!(
884 staged.allocation_history.records()[0].state,
885 AllocationState::Reserved
886 );
887 assert_eq!(
888 staged.allocation_history.generations()[0].declaration_count,
889 1
890 );
891 }
892
893 #[test]
894 fn stage_reservation_generation_allows_empty_generation_boundary() {
895 let reservations = Vec::new();
896
897 let staged = ledger()
898 .stage_reservation_generation(&reservations, Some(42))
899 .expect("empty reservation generation");
900
901 assert_eq!(staged.current_generation, 4);
902 assert_eq!(staged.allocation_history.records(), []);
903 assert_eq!(staged.allocation_history.generations().len(), 1);
904 assert_eq!(staged.allocation_history.generations()[0].generation(), 4);
905 assert_eq!(
906 staged.allocation_history.generations()[0].declaration_count(),
907 0
908 );
909 assert_eq!(
910 staged.allocation_history.generations()[0].committed_at(),
911 Some(42)
912 );
913 }
914
915 #[test]
916 fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
917 let first = vec![declaration("app.future_store.v1", 100, Some(1))];
918 let staged = ledger()
919 .stage_reservation_generation(&first, Some(42))
920 .expect("first reservation generation");
921
922 let second = vec![declaration("app.future_store.v1", 100, Some(2))];
923 let staged = staged
924 .stage_reservation_generation(&second, Some(43))
925 .expect("reservation refresh");
926 let record = record(&staged, "app.future_store.v1");
927
928 assert_eq!(record.state(), AllocationState::Reserved);
929 assert_eq!(record.first_generation(), 4);
930 assert_eq!(record.last_seen_generation(), 5);
931 assert_eq!(record.schema_history().len(), 2);
932 assert_eq!(record.schema_history()[1].generation(), 5);
933 assert_eq!(
934 staged.allocation_history.generations()[1].declaration_count(),
935 1
936 );
937 }
938
939 #[test]
940 fn stage_reservation_generation_rejects_generation_overflow() {
941 let ledger = AllocationLedger {
942 current_generation: u64::MAX,
943 ..ledger()
944 };
945 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
946
947 let err = ledger
948 .stage_reservation_generation(&reservations, None)
949 .expect_err("overflow must fail");
950
951 assert_eq!(
952 err,
953 AllocationReservationError::GenerationOverflow {
954 generation: u64::MAX
955 }
956 );
957 }
958
959 #[test]
960 fn stage_reservation_generation_rejects_same_key_different_slot() {
961 let mut ledger = ledger();
962 ledger.allocation_history.records = vec![AllocationRecord::reserved(
963 3,
964 &declaration("app.future_store.v1", 100, None),
965 )];
966 let reservations = vec![declaration("app.future_store.v1", 101, None)];
967
968 let err = ledger
969 .stage_reservation_generation(&reservations, None)
970 .expect_err("reservation key cannot move slots");
971
972 assert!(matches!(
973 err,
974 AllocationReservationError::StableKeySlotConflict { .. }
975 ));
976 }
977
978 #[test]
979 fn stage_reservation_generation_rejects_same_slot_different_key() {
980 let mut ledger = ledger();
981 ledger.allocation_history.records = vec![AllocationRecord::reserved(
982 3,
983 &declaration("app.future_store.v1", 100, None),
984 )];
985 let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
986
987 let err = ledger
988 .stage_reservation_generation(&reservations, None)
989 .expect_err("reservation slot cannot be reused by another key");
990
991 assert!(matches!(
992 err,
993 AllocationReservationError::SlotStableKeyConflict { .. }
994 ));
995 }
996
997 #[test]
998 fn stage_reservation_generation_rejects_active_allocation() {
999 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1000 let staged = ledger()
1001 .stage_validated_generation(&active, None)
1002 .expect("active generation");
1003 let reservations = vec![declaration("app.users.v1", 100, None)];
1004
1005 let err = staged
1006 .stage_reservation_generation(&reservations, None)
1007 .expect_err("active cannot become reserved");
1008
1009 assert!(matches!(
1010 err,
1011 AllocationReservationError::ActiveAllocation { .. }
1012 ));
1013 }
1014
1015 #[test]
1016 fn stage_reservation_generation_rejects_retired_allocation() {
1017 let mut ledger = ledger();
1018 let mut record = active_record("app.users.v1", 100);
1019 record.state = AllocationState::Retired { generation: 3 };
1020 ledger.allocation_history.records = vec![record];
1021 let reservations = vec![declaration("app.users.v1", 100, None)];
1022
1023 let err = ledger
1024 .stage_reservation_generation(&reservations, None)
1025 .expect_err("retired cannot revive");
1026
1027 assert!(matches!(
1028 err,
1029 AllocationReservationError::RetiredAllocation { .. }
1030 ));
1031 }
1032
1033 #[test]
1034 fn stage_validated_generation_activates_reserved_record() {
1035 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1036 let staged = ledger()
1037 .stage_reservation_generation(&reservations, None)
1038 .expect("reserved generation");
1039 let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
1040
1041 let staged = staged
1042 .stage_validated_generation(&active, None)
1043 .expect("active generation");
1044 let record = &staged.allocation_history.records()[0];
1045
1046 assert_eq!(record.state, AllocationState::Active);
1047 assert_eq!(record.first_generation, 4);
1048 assert_eq!(record.last_seen_generation, 5);
1049 assert_eq!(record.schema_history.len(), 2);
1050 }
1051
1052 #[test]
1053 fn stage_retirement_generation_tombstones_named_allocation() {
1054 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1055 let staged = ledger()
1056 .stage_validated_generation(&active, None)
1057 .expect("active generation");
1058 let retirement = AllocationRetirement::new(
1059 "app.users.v1",
1060 MemoryManagerSlot::new(100).expect("usable slot"),
1061 )
1062 .expect("retirement");
1063
1064 let staged = staged
1065 .stage_retirement_generation(&retirement, Some(42))
1066 .expect("retired generation");
1067 let record = &staged.allocation_history.records()[0];
1068
1069 assert_eq!(staged.current_generation, 5);
1070 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1071 assert_eq!(
1072 staged.allocation_history.generations()[1].declaration_count,
1073 0
1074 );
1075 }
1076
1077 #[test]
1078 fn stage_retirement_generation_tombstones_reserved_allocation() {
1079 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
1080 let staged = ledger()
1081 .stage_reservation_generation(&reservations, None)
1082 .expect("reserved generation");
1083 let retirement = AllocationRetirement::new(
1084 "app.future_store.v1",
1085 MemoryManagerSlot::new(100).expect("usable slot"),
1086 )
1087 .expect("retirement");
1088
1089 let staged = staged
1090 .stage_retirement_generation(&retirement, Some(42))
1091 .expect("reserved retirement generation");
1092 let record = &staged.allocation_history.records()[0];
1093
1094 assert_eq!(staged.current_generation, 5);
1095 assert_eq!(record.state, AllocationState::Retired { generation: 5 });
1096 assert_eq!(record.first_generation, 4);
1097 assert_eq!(
1098 staged.allocation_history.generations()[1].declaration_count(),
1099 0
1100 );
1101 }
1102
1103 #[test]
1104 fn stage_retirement_generation_rejects_generation_overflow() {
1105 let mut ledger = ledger();
1106 ledger.current_generation = u64::MAX;
1107 ledger.allocation_history.records = vec![active_record("app.users.v1", 100)];
1108 let retirement = AllocationRetirement::new(
1109 "app.users.v1",
1110 MemoryManagerSlot::new(100).expect("usable slot"),
1111 )
1112 .expect("retirement");
1113
1114 let err = ledger
1115 .stage_retirement_generation(&retirement, None)
1116 .expect_err("overflow must fail");
1117
1118 assert_eq!(
1119 err,
1120 AllocationRetirementError::GenerationOverflow {
1121 generation: u64::MAX
1122 }
1123 );
1124 }
1125
1126 #[test]
1127 fn stage_retirement_generation_requires_matching_slot() {
1128 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1129 let staged = ledger()
1130 .stage_validated_generation(&active, None)
1131 .expect("active generation");
1132 let retirement = AllocationRetirement::new(
1133 "app.users.v1",
1134 MemoryManagerSlot::new(101).expect("usable slot"),
1135 )
1136 .expect("retirement");
1137
1138 let err = staged
1139 .stage_retirement_generation(&retirement, None)
1140 .expect_err("slot mismatch");
1141
1142 assert!(matches!(
1143 err,
1144 AllocationRetirementError::SlotMismatch { .. }
1145 ));
1146 }
1147
1148 #[test]
1149 fn retirement_rejections_preserve_source_and_slot_error_precedence() {
1150 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1151 let source = ledger().stage_validated_generation(&active, None).unwrap();
1152 let retirement =
1153 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1154 .unwrap();
1155 let source = source
1156 .stage_retirement_generation(&retirement, None)
1157 .unwrap();
1158 let before = source.clone();
1159
1160 assert!(matches!(
1161 source.stage_retirement_generation(&retirement, None),
1162 Err(AllocationRetirementError::AlreadyRetired { .. })
1163 ));
1164 let wrong_slot =
1165 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(101).unwrap())
1166 .unwrap();
1167 assert!(matches!(
1168 source.stage_retirement_generation(&wrong_slot, None),
1169 Err(AllocationRetirementError::SlotMismatch { .. })
1170 ));
1171 let unknown =
1172 AllocationRetirement::new("app.unknown.v1", MemoryManagerSlot::new(100).unwrap())
1173 .unwrap();
1174 assert!(matches!(
1175 source.stage_retirement_generation(&unknown, None),
1176 Err(AllocationRetirementError::UnknownStableKey(_))
1177 ));
1178 assert_eq!(source, before);
1179 }
1180
1181 #[test]
1182 fn allocation_retirement_constructor_rejects_invalid_key() {
1183 let err = AllocationRetirement::new("App.users.v1", MemoryManagerSlot::new(100).unwrap())
1184 .expect_err("invalid key must fail at construction");
1185 assert!(matches!(err, AllocationRetirementError::Key(_)));
1186 }
1187
1188 #[test]
1189 fn retirement_decode_rejects_unusable_memory_manager_slot() {
1190 let retirement =
1191 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
1192 .unwrap();
1193 let mut value = serde_json::to_value(retirement).unwrap();
1194 value["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
1195 assert!(serde_json::from_value::<AllocationRetirement>(value).is_err());
1196 }
1197
1198 #[test]
1199 fn retirement_decode_rejects_invalid_stable_key() {
1200 let retirement = AllocationRetirement::new(
1201 "app.users.v1",
1202 MemoryManagerSlot::new(100).expect("usable slot"),
1203 )
1204 .expect("retirement");
1205 let mut value = crate::test_cbor::to_value(retirement).expect("retirement value");
1206 *map_field_mut(value_map_mut(&mut value), "stable_key") =
1207 crate::test_cbor::Value::Text("App.users.v1".to_string());
1208 let bytes = crate::test_cbor::to_vec(&value).expect("retirement bytes");
1209 assert!(crate::test_cbor::from_slice::<AllocationRetirement>(&bytes).is_err());
1210 }
1211
1212 #[test]
1213 fn snapshot_can_feed_validated_generation() {
1214 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1215 .expect("snapshot");
1216 let (declarations, runtime_fingerprint) = snapshot.into_parts();
1217 let validated =
1218 crate::capability::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1219
1220 let staged = ledger()
1221 .stage_validated_generation(&validated, None)
1222 .expect("validated generation");
1223
1224 assert_eq!(staged.allocation_history.records().len(), 1);
1225 }
1226
1227 #[test]
1228 fn stage_validated_generation_records_runtime_fingerprint() {
1229 let validated = crate::capability::ValidatedAllocations::new(
1230 3,
1231 vec![declaration("app.users.v1", 100, None)],
1232 Some("wasm:abc123".to_string()),
1233 );
1234
1235 let staged = ledger()
1236 .stage_validated_generation(&validated, None)
1237 .expect("validated generation");
1238
1239 assert_eq!(
1240 staged.allocation_history.generations()[0].runtime_fingerprint,
1241 Some("wasm:abc123".to_string())
1242 );
1243 }
1244
1245 #[test]
1246 fn strict_committed_integrity_accepts_full_lifecycle() {
1247 let mut ledger = committed_ledger(0);
1248 ledger
1249 .validate_committed_integrity()
1250 .expect("genesis ledger with no history");
1251
1252 ledger = ledger
1253 .stage_validated_generation(
1254 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1255 Some(1),
1256 )
1257 .expect("first real commit after genesis");
1258 ledger
1259 .validate_committed_integrity()
1260 .expect("first real commit");
1261
1262 ledger = ledger
1263 .stage_validated_generation(
1264 &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1265 Some(2),
1266 )
1267 .expect("repeated active declaration");
1268 ledger
1269 .validate_committed_integrity()
1270 .expect("repeated active declaration");
1271 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1272
1273 ledger = ledger
1274 .stage_validated_generation(
1275 &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1276 Some(3),
1277 )
1278 .expect("schema drift");
1279 ledger
1280 .validate_committed_integrity()
1281 .expect("schema metadata drift");
1282 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1283
1284 ledger = ledger
1285 .stage_reservation_generation(
1286 &[declaration("app.future_store.v1", 101, Some(1))],
1287 Some(4),
1288 )
1289 .expect("reservation-only generation");
1290 ledger
1291 .validate_committed_integrity()
1292 .expect("reservation-only generation");
1293 assert_eq!(
1294 record(&ledger, "app.future_store.v1").state,
1295 AllocationState::Reserved
1296 );
1297
1298 ledger = ledger
1299 .stage_validated_generation(
1300 &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1301 Some(5),
1302 )
1303 .expect("reservation activation");
1304 ledger
1305 .validate_committed_integrity()
1306 .expect("reservation activation");
1307 assert_eq!(
1308 record(&ledger, "app.future_store.v1").state,
1309 AllocationState::Active
1310 );
1311
1312 let retirement = AllocationRetirement::new(
1313 "app.users.v1",
1314 MemoryManagerSlot::new(100).expect("usable slot"),
1315 )
1316 .expect("retirement");
1317 ledger = ledger
1318 .stage_retirement_generation(&retirement, Some(6))
1319 .expect("retirement generation");
1320 ledger
1321 .validate_committed_integrity()
1322 .expect("retirement generation");
1323 assert_eq!(ledger.current_generation, 6);
1324 assert_eq!(
1325 record(&ledger, "app.users.v1").state,
1326 AllocationState::Retired { generation: 6 }
1327 );
1328 assert_eq!(
1329 record(&ledger, "app.future_store.v1").last_seen_generation,
1330 5
1331 );
1332 }
1333
1334 #[derive(Clone, Copy, Debug)]
1335 enum Transition {
1336 DeclareUsers,
1337 DeclareOrders,
1338 ReserveAudit,
1339 ActivateAudit,
1340 RetireUsers,
1341 RetireAudit,
1342 EmptyValidated,
1343 EmptyReservation,
1344 }
1345
1346 const TRANSITIONS: [Transition; 8] = [
1347 Transition::DeclareUsers,
1348 Transition::DeclareOrders,
1349 Transition::ReserveAudit,
1350 Transition::ActivateAudit,
1351 Transition::RetireUsers,
1352 Transition::RetireAudit,
1353 Transition::EmptyValidated,
1354 Transition::EmptyReservation,
1355 ];
1356
1357 fn apply_transition(
1358 ledger: &AllocationLedger,
1359 transition: Transition,
1360 committed_at: u64,
1361 ) -> Result<AllocationLedger, String> {
1362 match transition {
1363 Transition::DeclareUsers => ledger
1364 .stage_validated_generation(
1365 &validated(
1366 ledger.current_generation,
1367 vec![declaration("app.users.v1", 100, Some(1))],
1368 ),
1369 Some(committed_at),
1370 )
1371 .map_err(|err| err.to_string()),
1372 Transition::DeclareOrders => ledger
1373 .stage_validated_generation(
1374 &validated(
1375 ledger.current_generation,
1376 vec![declaration("app.orders.v1", 101, Some(1))],
1377 ),
1378 Some(committed_at),
1379 )
1380 .map_err(|err| err.to_string()),
1381 Transition::ReserveAudit => ledger
1382 .stage_reservation_generation(
1383 &[declaration("app.audit.v1", 102, Some(1))],
1384 Some(committed_at),
1385 )
1386 .map_err(|err| err.to_string()),
1387 Transition::ActivateAudit => ledger
1388 .stage_validated_generation(
1389 &validated(
1390 ledger.current_generation,
1391 vec![declaration("app.audit.v1", 102, Some(1))],
1392 ),
1393 Some(committed_at),
1394 )
1395 .map_err(|err| err.to_string()),
1396 Transition::RetireUsers => ledger
1397 .stage_retirement_generation(
1398 &AllocationRetirement::new(
1399 "app.users.v1",
1400 MemoryManagerSlot::new(100).expect("usable slot"),
1401 )
1402 .expect("retirement"),
1403 Some(committed_at),
1404 )
1405 .map_err(|err| err.to_string()),
1406 Transition::RetireAudit => ledger
1407 .stage_retirement_generation(
1408 &AllocationRetirement::new(
1409 "app.audit.v1",
1410 MemoryManagerSlot::new(102).expect("usable slot"),
1411 )
1412 .expect("retirement"),
1413 Some(committed_at),
1414 )
1415 .map_err(|err| err.to_string()),
1416 Transition::EmptyValidated => ledger
1417 .stage_validated_generation(
1418 &validated(ledger.current_generation, Vec::new()),
1419 Some(committed_at),
1420 )
1421 .map_err(|err| err.to_string()),
1422 Transition::EmptyReservation => ledger
1423 .stage_reservation_generation(&[], Some(committed_at))
1424 .map_err(|err| err.to_string()),
1425 }
1426 }
1427
1428 fn check_transition_sequences(
1429 ledger: AllocationLedger,
1430 depth: usize,
1431 sequence: &mut Vec<Transition>,
1432 ) {
1433 ledger
1434 .validate_committed_integrity()
1435 .unwrap_or_else(|err| panic!("sequence {sequence:?} violated integrity: {err}"));
1436 if depth == 0 {
1437 return;
1438 }
1439
1440 for transition in TRANSITIONS {
1441 sequence.push(transition);
1442 let next = apply_transition(&ledger, transition, sequence.len() as u64)
1443 .unwrap_or_else(|_| ledger.clone());
1444 check_transition_sequences(next, depth - 1, sequence);
1445 sequence.pop();
1446 }
1447 }
1448
1449 #[test]
1450 fn transition_matrix_preserves_committed_invariants() {
1451 check_transition_sequences(committed_ledger(0), 4, &mut Vec::new());
1452 }
1453
1454 #[test]
1455 fn new_committed_requires_strict_generation_history() {
1456 let structurally_valid =
1457 AllocationLedger::new(3, AllocationHistory::default()).expect("structurally valid DTO");
1458
1459 assert_eq!(structurally_valid.current_generation, 3);
1460
1461 let err = AllocationLedger::new_committed(3, AllocationHistory::default())
1462 .expect_err("committed ledger needs generation history");
1463
1464 assert_eq!(
1465 err,
1466 LedgerIntegrityError::MissingCurrentGenerationRecord {
1467 current_generation: 3
1468 }
1469 );
1470 }
1471
1472 #[test]
1473 fn new_committed_preserves_structural_rejection_before_history_checks() {
1474 let history = AllocationHistory::from_parts(
1475 vec![
1476 active_record("app.users.v1", 100),
1477 active_record("app.users.v1", 100),
1478 ],
1479 Vec::new(),
1480 );
1481
1482 assert_eq!(
1485 AllocationLedger::new_committed(3, history).expect_err("duplicate records"),
1486 LedgerIntegrityError::DuplicateStableKey {
1487 stable_key: StableKey::parse("app.users.v1").unwrap(),
1488 }
1489 );
1490
1491 let valid = active_committed_ledger();
1492 assert_eq!(
1493 AllocationLedger::new_committed(
1494 valid.current_generation(),
1495 valid.allocation_history().clone(),
1496 )
1497 .expect("valid committed history"),
1498 valid
1499 );
1500 }
1501
1502 #[test]
1503 fn validate_integrity_rejects_duplicate_stable_keys() {
1504 let mut ledger = ledger();
1505 for second_id in [101, 100] {
1506 ledger.allocation_history.records = vec![
1507 active_record("app.users.v1", 100),
1508 active_record("app.users.v1", second_id),
1509 ];
1510
1511 let err = ledger
1512 .validate_integrity()
1513 .expect_err("duplicate key precedes duplicate slot");
1514
1515 assert_eq!(
1516 err,
1517 LedgerIntegrityError::DuplicateStableKey {
1518 stable_key: StableKey::parse("app.users.v1").unwrap(),
1519 }
1520 );
1521 }
1522 }
1523
1524 #[test]
1525 fn validate_integrity_rejects_duplicate_slots() {
1526 let mut ledger = ledger();
1527 ledger.allocation_history.records = vec![
1528 active_record("app.users.v1", 100),
1529 active_record("app.orders.v1", 100),
1530 ];
1531
1532 let err = ledger.validate_integrity().expect_err("duplicate slot");
1533
1534 assert_eq!(
1535 err,
1536 LedgerIntegrityError::DuplicateSlot {
1537 slot: MemoryManagerSlot::new(100).unwrap(),
1538 }
1539 );
1540 }
1541
1542 #[test]
1543 fn recovery_rejects_invalid_stable_key_during_ledger_decode() {
1544 let mut ledger = committed_ledger(1);
1545 ledger
1546 .allocation_history
1547 .records
1548 .push(active_record("app.users.v1", 100));
1549 let mut bytes = crate::test_cbor::to_vec(&ledger).expect("encode ledger");
1550 let key_start = bytes
1551 .windows(b"app.users.v1".len())
1552 .position(|window| window == b"app.users.v1")
1553 .expect("encoded stable key");
1554 bytes[key_start] = b'A';
1555 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1556 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1557 let mut physical = DualCommitStore::default();
1558 physical.commit_payload_at_generation(1, payload).unwrap();
1559 let store = LedgerCommitStore { physical };
1560 let before = store.clone();
1561 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1562 assert_eq!(store, before, "malformed-key recovery must preserve slots");
1563 }
1564
1565 #[test]
1566 fn ledger_decode_rejects_unusable_memory_manager_slot() {
1567 let mut ledger = committed_ledger(1);
1568 ledger
1569 .allocation_history
1570 .records
1571 .push(active_record("app.users.v1", 100));
1572 let mut value = serde_json::to_value(ledger).unwrap();
1573 value["allocation_history"]["records"][0]["slot"]["slot"]["MemoryManagerId"] =
1574 serde_json::json!(255);
1575 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1576 assert!(decode_ledger(&bytes).is_err());
1577 }
1578
1579 #[test]
1580 fn validate_integrity_rejects_retirement_before_last_observation() {
1581 let mut ledger = committed_ledger(5);
1582 let mut record = active_record("app.users.v1", 100);
1583 record.last_seen_generation = 4;
1584 record.state = AllocationState::Retired { generation: 3 };
1585 ledger.allocation_history.records = vec![record];
1586
1587 let err = ledger
1588 .validate_committed_integrity()
1589 .expect_err("retirement must follow the last observation");
1590
1591 assert!(matches!(
1592 err,
1593 LedgerIntegrityError::RetirementNotAfterLastSeen { .. }
1594 ));
1595 }
1596
1597 #[test]
1598 fn validate_integrity_rejects_retirement_before_allocation() {
1599 let mut ledger = committed_ledger(2);
1600 let mut record = active_record("app.users.v1", 100);
1601 record.state = AllocationState::Retired { generation: 0 };
1602 ledger.allocation_history.records = vec![record];
1603
1604 let err = ledger
1605 .validate_committed_integrity()
1606 .expect_err("retirement cannot predate allocation");
1607
1608 assert!(matches!(
1609 err,
1610 LedgerIntegrityError::RetiredBeforeFirstGeneration { .. }
1611 ));
1612 }
1613
1614 #[test]
1615 fn validate_integrity_requires_schema_history_at_first_generation() {
1616 let mut ledger = committed_ledger(2);
1617 let mut record = active_record("app.users.v1", 100);
1618 record.last_seen_generation = 2;
1619 record.schema_history[0].generation = 2;
1620 ledger.allocation_history.records = vec![record];
1621
1622 let err = ledger
1623 .validate_committed_integrity()
1624 .expect_err("schema history must begin with the allocation");
1625
1626 assert!(matches!(
1627 err,
1628 LedgerIntegrityError::SchemaHistoryStartMismatch { .. }
1629 ));
1630 }
1631
1632 #[test]
1633 fn validate_integrity_rejects_schema_history_after_last_observation() {
1634 for generation in [3, 4] {
1635 let mut ledger = committed_ledger(3);
1636 let mut record = active_record("app.users.v1", 100);
1637 record.last_seen_generation = 2;
1638 record.schema_history.push(SchemaMetadataRecord::new(
1639 generation,
1640 SchemaMetadata::new(Some(2)).expect("schema"),
1641 ));
1642 ledger.allocation_history.records = vec![record];
1643
1644 let err = ledger
1645 .validate_committed_integrity()
1646 .expect_err("schema metadata cannot postdate the last observation");
1647
1648 assert!(matches!(
1649 (generation, err),
1650 (
1651 3,
1652 LedgerIntegrityError::SchemaHistoryAfterLastSeen {
1653 generation: 3,
1654 last_seen_generation: 2,
1655 ..
1656 }
1657 ) | (
1658 4,
1659 LedgerIntegrityError::SchemaHistoryOutOfBounds { generation: 4, .. }
1660 )
1661 ));
1662 }
1663 }
1664
1665 #[test]
1666 fn validate_integrity_rejects_non_increasing_schema_history() {
1667 for generation in [0, 1] {
1668 let mut ledger = ledger();
1669 let mut record = active_record("app.users.v1", 100);
1670 record.schema_history.push(SchemaMetadataRecord {
1671 generation,
1672 schema: SchemaMetadata::default(),
1673 });
1674 ledger.allocation_history.records = vec![record];
1675
1676 let err = ledger
1677 .validate_integrity()
1678 .expect_err("non-increasing schema history");
1679
1680 assert!(matches!(
1681 err,
1682 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1683 ));
1684 }
1685 }
1686
1687 #[test]
1688 fn recovery_rejects_zero_schema_version_during_ledger_decode() {
1689 let mut ledger = committed_ledger(1);
1690 ledger
1691 .allocation_history
1692 .records
1693 .push(AllocationRecord::active(
1694 1,
1695 &declaration("app.users.v1", 100, Some(1)),
1696 ));
1697 let mut value = serde_json::to_value(&ledger).unwrap();
1698 value["allocation_history"]["records"][0]["schema_history"][0]["schema"]["schema_version"] =
1699 0.into();
1700 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1701 assert!(crate::test_cbor::from_slice::<AllocationLedger>(&bytes).is_err());
1702 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1703 let mut physical = DualCommitStore::default();
1704 physical.commit_payload_at_generation(1, payload).unwrap();
1705 let store = LedgerCommitStore { physical };
1706 let before = store.clone();
1707 assert!(matches!(store.recover(), Err(LedgerCommitError::Codec(_))));
1708 assert_eq!(
1709 store, before,
1710 "malformed-schema recovery must preserve slots"
1711 );
1712 }
1713
1714 #[test]
1715 fn malformed_generation_fingerprints_reject_as_codec_errors_and_preserve_slots() {
1716 for fingerprint in [
1717 String::new(),
1718 "x".repeat(257),
1719 "é".to_string(),
1720 "build\n".to_string(),
1721 ] {
1722 let mut value = serde_json::to_value(committed_ledger(1)).unwrap();
1723 value["allocation_history"]["generations"][0]["runtime_fingerprint"] =
1724 fingerprint.into();
1725 let bytes = crate::test_cbor::to_vec(&value).unwrap();
1726 let payload = LedgerPayloadEnvelope::current(bytes).encode();
1727 let invalid = CommittedGenerationBytes::new(1, payload);
1728 let genesis = committed_ledger(0);
1729 let valid = CommittedGenerationBytes::new(0, enveloped_payload(&genesis));
1730 for (slot0, slot1) in [
1731 (valid.clone(), invalid.clone()),
1732 (invalid.clone(), valid.clone()),
1733 ] {
1734 let mut store = LedgerCommitStore {
1735 physical: DualCommitStore {
1736 slot0: Some(slot0),
1737 slot1: Some(slot1),
1738 },
1739 };
1740 let before = store.clone();
1741 let error = store.recover().unwrap_err();
1742 assert!(matches!(error, LedgerCommitError::Codec(_)));
1743 let (recovered, diagnostic) = store.recover_with_diagnostic();
1744 assert_eq!(recovered, Err(error.clone()));
1745 assert!(diagnostic.recovery.is_ok(), "physical slots remain valid");
1746 assert_eq!(store.recover_or_initialize(&genesis), Err(error));
1747 assert_eq!(
1748 store, before,
1749 "all rejected paths preserve both protected slots"
1750 );
1751 }
1752 }
1753 }
1754
1755 #[test]
1756 fn validate_committed_integrity_requires_current_generation_record() {
1757 let err = ledger()
1758 .validate_committed_integrity()
1759 .expect_err("missing current generation");
1760
1761 assert_eq!(
1762 err,
1763 LedgerIntegrityError::MissingCurrentGenerationRecord {
1764 current_generation: 3
1765 }
1766 );
1767 }
1768
1769 #[test]
1770 fn committed_integrity_rejects_allocation_references_to_genesis() {
1771 for generation in [0, 1, 2] {
1772 let mut ledger = committed_ledger(generation);
1773 let mut record = AllocationRecord::active(0, &declaration("app.genesis.v1", 100, None));
1774 if generation == 2 {
1775 record.last_seen_generation = 1;
1776 record
1777 .schema_history
1778 .push(SchemaMetadataRecord::new(1, SchemaMetadata::default()));
1779 record.state = AllocationState::Retired { generation: 2 };
1780 }
1781 ledger.allocation_history.records.push(record);
1782 ledger.validate_integrity().expect("structurally valid DTO");
1783 assert_eq!(
1784 ledger.validate_committed_integrity(),
1785 Err(LedgerIntegrityError::UnknownRecordGeneration {
1786 stable_key: StableKey::parse("app.genesis.v1").unwrap(),
1787 generation: 0,
1788 })
1789 );
1790 }
1791 }
1792
1793 #[test]
1794 fn validate_committed_integrity_rejects_generation_history_gaps() {
1795 let mut ledger = committed_ledger(3);
1796 ledger.allocation_history.generations.remove(1);
1797 ledger
1798 .allocation_history
1799 .records
1800 .push(AllocationRecord::active(
1801 0,
1802 &declaration("app.genesis.v1", 100, None),
1803 ));
1804
1805 let err = ledger
1806 .validate_committed_integrity()
1807 .expect_err("generation history gap takes precedence over genesis reference");
1808
1809 assert!(matches!(
1810 err,
1811 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1812 ));
1813 }
1814
1815 #[test]
1816 fn committed_integrity_rejects_a_skipped_parent_in_contiguous_history() {
1817 let mut ledger = committed_ledger(3);
1818 ledger.allocation_history.generations[2].parent_generation = 1;
1819 ledger
1820 .validate_integrity()
1821 .expect("structurally valid history");
1822
1823 assert_eq!(
1824 ledger.validate_committed_integrity(),
1825 Err(LedgerIntegrityError::BrokenGenerationChain {
1826 generation: 3,
1827 expected_parent: 2,
1828 actual_parent: 1,
1829 })
1830 );
1831 }
1832
1833 #[test]
1834 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1835 let mut store = LedgerCommitStore::default();
1836 let mut invalid = ledger();
1837 invalid.allocation_history.records = vec![
1838 active_record("app.users.v1", 100),
1839 active_record("app.orders.v1", 100),
1840 ];
1841
1842 let err = store.commit(&invalid).expect_err("invalid ledger");
1843
1844 assert!(matches!(
1845 err,
1846 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1847 ));
1848 assert!(store.physical().is_uninitialized());
1849 }
1850
1851 #[test]
1852 fn ledger_commit_store_recovers_latest_committed_ledger() {
1853 let mut store = LedgerCommitStore::default();
1854 let first = committed_ledger(1);
1855 let second = committed_ledger(2);
1856
1857 store.commit(&first).expect("first commit");
1858 store.commit(&second).expect("second commit");
1859 let recovered = store.recover().expect("recovered ledger");
1860
1861 assert_eq!(recovered.current_generation(), 2);
1862 assert_eq!(recovered.physical_generation(), 2);
1863 }
1864
1865 #[test]
1866 fn ledger_commit_store_wraps_logical_payload_in_envelope() {
1867 let mut store = LedgerCommitStore::default();
1868 let ledger = committed_ledger(1);
1869
1870 store.commit(&ledger).expect("commit");
1871
1872 let committed = store.physical().authoritative().expect("authoritative");
1873 assert_eq!(
1874 committed.payload(),
1875 LedgerPayloadEnvelope::current(crate::test_cbor::to_vec(&ledger).unwrap())
1876 .try_encode()
1877 .unwrap(),
1878 "direct commit encoding preserves the complete public envelope bytes"
1879 );
1880 let envelope = LedgerPayloadEnvelope::decode(committed.payload()).expect("envelope");
1881 assert_eq!(
1882 envelope.payload(),
1883 crate::test_cbor::to_vec(&ledger).expect("encode ledger")
1884 );
1885 }
1886
1887 #[test]
1888 fn ledger_commit_store_rejects_bad_payload_envelope_before_ledger_decode() {
1889 let committed = CommittedGenerationBytes::new(1, vec![b'X'; 28]);
1890 let store = LedgerCommitStore {
1891 physical: DualCommitStore {
1892 slot0: Some(committed),
1893 slot1: None,
1894 },
1895 };
1896
1897 let err = store.recover().expect_err("bad envelope");
1898
1899 assert!(matches!(
1900 err,
1901 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::BadMagic { .. })
1902 ));
1903 }
1904
1905 #[test]
1906 fn ledger_commit_store_classifies_unknown_format_version_as_unsupported() {
1907 let mut payload = enveloped_payload(&committed_ledger(1));
1908 payload[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1909 let store = LedgerCommitStore {
1910 physical: DualCommitStore {
1911 slot0: Some(CommittedGenerationBytes::new(1, payload)),
1912 slot1: None,
1913 },
1914 };
1915
1916 let err = store
1917 .recover()
1918 .expect_err("unknown ledger format version must fail");
1919
1920 assert_eq!(
1921 err,
1922 LedgerCommitError::PayloadEnvelope(LedgerPayloadEnvelopeError::UnsupportedFormat {
1923 marker: *b"ICMF",
1924 version: Some(LEDGER_PAYLOAD_FORMAT_VERSION + 1),
1925 })
1926 );
1927 }
1928
1929 #[test]
1930 fn recovery_and_initialization_preserve_physical_diagnostics_and_logical_failures() {
1931 let genesis = enveloped_payload(&committed_ledger(0));
1932 let committed = CommittedGenerationBytes::new(0, genesis.clone());
1933 let mut corrupt = committed.clone();
1934 corrupt.checksum ^= 1;
1935 let mut unsupported = genesis.clone();
1936 unsupported[12..16].copy_from_slice(&(LEDGER_PAYLOAD_FORMAT_VERSION + 1).to_le_bytes());
1937 let cases = [
1938 ("empty", None, None),
1939 ("one slot", Some(committed.clone()), None),
1940 (
1941 "identical slots",
1942 Some(committed.clone()),
1943 Some(committed.clone()),
1944 ),
1945 (
1946 "corrupt inactive",
1947 Some(committed.clone()),
1948 Some(corrupt.clone()),
1949 ),
1950 ("corrupt active", Some(corrupt), Some(committed.clone())),
1951 (
1952 "ambiguous",
1953 Some(committed),
1954 Some(CommittedGenerationBytes::new(0, vec![1])),
1955 ),
1956 (
1957 "unsupported format",
1958 Some(CommittedGenerationBytes::new(0, unsupported)),
1959 None,
1960 ),
1961 (
1962 "invalid CBOR",
1963 Some(CommittedGenerationBytes::new(
1964 0,
1965 LedgerPayloadEnvelope::current(vec![255])
1966 .try_encode()
1967 .unwrap(),
1968 )),
1969 None,
1970 ),
1971 (
1972 "generation mismatch",
1973 Some(CommittedGenerationBytes::new(1, genesis)),
1974 None,
1975 ),
1976 (
1977 "invalid committed integrity",
1978 Some(CommittedGenerationBytes::new(
1979 3,
1980 enveloped_payload(&ledger()),
1981 )),
1982 None,
1983 ),
1984 ];
1985
1986 for (label, slot0, slot1) in cases {
1987 let empty = slot0.is_none() && slot1.is_none();
1988 let store = LedgerCommitStore {
1989 physical: DualCommitStore { slot0, slot1 },
1990 };
1991 let before = store.clone();
1992 let (recovered, diagnostic) = store.recover_with_diagnostic();
1993 assert_eq!(recovered, store.recover(), "{label}");
1994 assert_eq!(diagnostic, store.physical().diagnostic(), "{label}");
1995 assert_eq!(store, before, "{label}");
1996
1997 let mut initializing = store.clone();
1998 let initialized = initializing.recover_or_initialize(&committed_ledger(0));
1999 if empty {
2000 let initialized = initialized.expect("empty store accepts genesis");
2001 assert_eq!(initialized.current_generation(), 0);
2002 assert_eq!(initialized.physical_generation(), 0);
2003 assert_eq!(initializing.recover().unwrap(), initialized);
2004 } else {
2005 assert_eq!(initialized, recovered, "{label}");
2006 assert_eq!(initializing, before, "{label}");
2007 }
2008
2009 if let Err(LedgerCommitError::Recovery(error)) = recovered {
2010 assert_eq!(diagnostic.recovery, Err(error), "{label}");
2011 } else {
2012 assert!(diagnostic.recovery.is_ok(), "{label}");
2013 }
2014 }
2015 }
2016
2017 #[test]
2018 fn ledger_commit_store_recovers_current_format_genesis_and_first_real_commit() {
2019 let mut store = LedgerCommitStore::default();
2020 let genesis = committed_ledger(0);
2021
2022 let recovered = store
2023 .recover_or_initialize(&genesis)
2024 .expect("current-format genesis ledger");
2025 assert_eq!(recovered.current_generation(), 0);
2026 assert_eq!(recovered.physical_generation(), 0);
2027 assert_eq!(recovered.ledger().allocation_history().generations(), []);
2028 assert_eq!(recovered, store.recover().unwrap());
2029
2030 let first = recovered
2031 .ledger()
2032 .stage_validated_generation(
2033 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2034 None,
2035 )
2036 .expect("first real generation");
2037 let recovered = store.commit(&first).expect("first commit");
2038
2039 assert_eq!(recovered, store.recover().unwrap());
2040 assert_eq!(recovered.current_generation(), 1);
2041 assert_eq!(recovered.physical_generation(), 1);
2042 assert_eq!(
2043 recovered.ledger().allocation_history().generations()[0].generation,
2044 1
2045 );
2046 assert_eq!(
2047 record(recovered.ledger(), "app.users.v1").first_generation,
2048 1
2049 );
2050 }
2051
2052 #[test]
2053 fn ledger_commit_store_rejects_corrupt_latest_slot_without_rollback() {
2054 let mut store = LedgerCommitStore::default();
2055 let genesis = committed_ledger(0);
2056 store.commit(&genesis).expect("genesis commit");
2057 let first = genesis
2058 .stage_validated_generation(
2059 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2060 None,
2061 )
2062 .expect("first generation");
2063 let first = store.commit(&first).expect("first commit");
2064 let second = first
2065 .ledger()
2066 .stage_validated_generation(
2067 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
2068 None,
2069 )
2070 .expect("second generation");
2071
2072 store.commit(&second).expect("second commit");
2073 store.physical.slot0.as_mut().expect("latest slot").checksum ^= 1;
2074
2075 let err = store
2076 .recover()
2077 .expect_err("corrupt latest must not roll back");
2078
2079 assert_eq!(
2080 err,
2081 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2082 slot0_invalid: true,
2083 slot1_invalid: false,
2084 })
2085 );
2086 }
2087
2088 #[test]
2089 fn ledger_commit_store_recovers_identical_duplicate_slots() {
2090 let ledger = committed_ledger(0)
2091 .stage_validated_generation(
2092 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
2093 None,
2094 )
2095 .expect("first generation");
2096 let payload = enveloped_payload(&ledger);
2097 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
2098 let store = LedgerCommitStore {
2099 physical: DualCommitStore {
2100 slot0: Some(committed.clone()),
2101 slot1: Some(committed),
2102 },
2103 };
2104
2105 let recovered = store.recover().expect("recovered");
2106
2107 assert_eq!(recovered.ledger(), &ledger);
2108 }
2109
2110 #[test]
2111 fn ledger_commit_store_rejects_corrupt_inactive_ledger() {
2112 let mut store = LedgerCommitStore::default();
2113 let first = committed_ledger(1);
2114 let second = committed_ledger(2);
2115
2116 store.commit(&first).expect("first commit");
2117 store
2118 .write_corrupt_inactive_ledger(&second)
2119 .expect("corrupt write");
2120 let err = store.recover().expect_err("corrupt slot");
2121
2122 assert_eq!(
2123 err,
2124 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2125 slot0_invalid: false,
2126 slot1_invalid: true,
2127 })
2128 );
2129 }
2130
2131 #[test]
2132 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
2133 let store = LedgerCommitStore {
2134 physical: DualCommitStore {
2135 slot0: Some(CommittedGenerationBytes::new(
2136 7,
2137 enveloped_payload(&committed_ledger(6)),
2138 )),
2139 slot1: None,
2140 },
2141 };
2142 let err = store.recover().expect_err("mismatch");
2143
2144 assert_eq!(
2145 err,
2146 LedgerCommitError::PhysicalLogicalGenerationMismatch {
2147 physical_generation: 7,
2148 logical_generation: 6
2149 }
2150 );
2151 }
2152
2153 #[test]
2154 fn ledger_commit_store_rejects_non_next_logical_generation() {
2155 let mut store = LedgerCommitStore::default();
2156 store.commit(&committed_ledger(1)).expect("first commit");
2157
2158 let err = store
2159 .commit(&committed_ledger(3))
2160 .expect_err("skipped generation");
2161
2162 assert_eq!(
2163 err,
2164 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
2165 expected: 2,
2166 actual: 3
2167 })
2168 );
2169 }
2170
2171 #[test]
2172 fn ledger_commit_store_initializes_empty_store_explicitly() {
2173 let mut store = LedgerCommitStore::default();
2174 let genesis = committed_ledger(3);
2175
2176 let recovered = store
2177 .recover_or_initialize(&genesis)
2178 .expect("initialized ledger");
2179
2180 assert_eq!(recovered.current_generation(), 3);
2181 assert_eq!(recovered.physical_generation(), 3);
2182 assert!(!store.physical().is_uninitialized());
2183 }
2184
2185 #[test]
2186 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
2187 let mut store = LedgerCommitStore::default();
2188 store
2189 .write_corrupt_inactive_ledger(&ledger())
2190 .expect("corrupt write");
2191
2192 let err = store
2193 .recover_or_initialize(&ledger())
2194 .expect_err("corrupt state");
2195
2196 assert!(matches!(
2197 err,
2198 LedgerCommitError::Recovery(CommitRecoveryError::InvalidCommitSlots {
2199 slot0_invalid: true,
2200 slot1_invalid: false,
2201 })
2202 ));
2203 }
2204 #[test]
2205 fn history_boundary_round_trips_and_rejects_next_generation_without_mutation() {
2206 let mut source = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 - 1);
2207 source
2208 .allocation_history
2209 .records
2210 .push(active_record("app.users.v1", 100));
2211 let retirement =
2212 AllocationRetirement::new("app.users.v1", MemoryManagerSlot::new(100).unwrap())
2213 .unwrap();
2214 let retired = source
2215 .stage_retirement_generation(&retirement, None)
2216 .unwrap();
2217 retired.validate_committed_integrity().unwrap();
2218 assert_eq!(
2219 retired.allocation_history.generations().len(),
2220 crate::constants::MAX_LEDGER_GENERATIONS
2221 );
2222 assert_eq!(
2223 retired.allocation_history.records()[0].schema_history,
2224 source.allocation_history.records()[0].schema_history
2225 );
2226 assert_eq!(
2227 source.allocation_history.records()[0].state,
2228 AllocationState::Active
2229 );
2230 assert!(matches!(
2231 retired.stage_retirement_generation(&retirement, None),
2232 Err(AllocationRetirementError::Integrity(
2233 LedgerIntegrityError::LimitExceeded {
2234 resource: "generation history",
2235 ..
2236 }
2237 ))
2238 ));
2239
2240 let ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2241 let mut store = LedgerCommitStore::default();
2242 let recovered = store.commit(&ledger).expect("boundary is admissible");
2243 let record = crate::StableCellLedgerRecord::new(store.clone());
2244 let bytes = crate::test_cbor::to_vec(&record).unwrap();
2245 assert_eq!(record.encoded_size(), bytes.len());
2246 let recovered_record = crate::decode_stable_cell_ledger_record(&bytes).unwrap();
2247 assert_eq!(
2248 recovered_record.store().recover().unwrap().ledger(),
2249 &ledger
2250 );
2251 let before = store.clone();
2252 let err = crate::AllocationBootstrap::new(&mut store)
2253 .validate_and_commit(
2254 DeclarationSnapshot::new(Vec::new()).unwrap(),
2255 &crate::GenericRangePolicy,
2256 None,
2257 )
2258 .unwrap_err();
2259 assert!(matches!(
2260 err,
2261 crate::BootstrapError::Staging(AllocationStageError::Integrity(
2262 LedgerIntegrityError::LimitExceeded { .. }
2263 ))
2264 ));
2265 assert_eq!(store, before);
2266 assert_eq!(
2267 recovered.current_generation(),
2268 crate::constants::MAX_LEDGER_GENERATIONS as u64
2269 );
2270 let oversize = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64 + 1);
2271 assert!(matches!(
2272 store.commit(&oversize),
2273 Err(LedgerCommitError::Integrity(
2274 LedgerIntegrityError::LimitExceeded { .. }
2275 ))
2276 ));
2277 assert_eq!(store, before);
2278 }
2279
2280 #[test]
2281 fn record_count_boundary_and_malicious_history_reject_on_recovery() {
2282 let mut ledger = committed_ledger(1);
2283 ledger.allocation_history.records = (0..255_u8)
2284 .map(|id| active_record(&format!("app.slot{id}.v1"), id))
2285 .collect();
2286 let mut store = LedgerCommitStore::default();
2287 store.commit(&ledger).unwrap();
2288 assert_eq!(
2289 store
2290 .recover()
2291 .unwrap()
2292 .ledger()
2293 .allocation_history()
2294 .records()
2295 .len(),
2296 255
2297 );
2298 ledger
2299 .allocation_history
2300 .records
2301 .push(active_record("app.extra.v1", 254));
2302 assert!(matches!(
2303 store.commit(&ledger),
2304 Err(LedgerCommitError::Integrity(
2305 LedgerIntegrityError::LimitExceeded { .. }
2306 ))
2307 ));
2308 let payload = enveloped_payload(&ledger);
2309 let mut invalid = LedgerCommitStore {
2310 physical: DualCommitStore {
2311 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2312 slot1: None,
2313 },
2314 };
2315 assert!(invalid.recover().is_err());
2316 assert!(invalid.recover_or_initialize(&committed_ledger(0)).is_err());
2317 for bytes in [
2318 vec![0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
2319 vec![0x81; 34],
2320 vec![0xa1],
2321 ] {
2322 let payload = LedgerPayloadEnvelope::current(bytes).try_encode().unwrap();
2323 let invalid = LedgerCommitStore {
2324 physical: DualCommitStore {
2325 slot0: Some(CommittedGenerationBytes::new(1, payload)),
2326 slot1: None,
2327 },
2328 };
2329 assert!(matches!(
2330 invalid.recover(),
2331 Err(LedgerCommitError::Codec(_))
2332 ));
2333 }
2334 }
2335
2336 #[test]
2337 fn ledger_collection_limits_preserve_refusal_order_and_committed_slots() {
2338 let limit = crate::constants::MAX_LEDGER_GENERATIONS;
2339 let mut oversized = committed_ledger(1);
2340 let mut record = active_record("app.users.v1", 100);
2341 record.schema_history =
2342 vec![SchemaMetadataRecord::new(1, crate::SchemaMetadata::default()); limit + 1];
2343 oversized.allocation_history.records =
2344 vec![active_record("app.users.v1", 100); crate::constants::MAX_ALLOCATIONS + 1];
2345 oversized.allocation_history.records[0] = record;
2346 let generation = oversized.allocation_history.generations[0].clone();
2347 oversized
2348 .allocation_history
2349 .generations
2350 .resize(limit + 1, generation);
2351
2352 let mut store = LedgerCommitStore::default();
2353 store.commit(&committed_ledger(0)).unwrap();
2354 store.commit(&committed_ledger(1)).unwrap();
2355 let before = store.clone();
2356 for (resource, maximum) in [
2359 ("allocation records", crate::constants::MAX_ALLOCATIONS),
2360 ("generation history", limit),
2361 ("schema history", limit),
2362 ] {
2363 let expected = LedgerIntegrityError::LimitExceeded {
2364 resource,
2365 limit: maximum,
2366 };
2367 assert_eq!(oversized.validate_integrity(), Err(expected.clone()));
2368 assert_eq!(
2369 oversized.validate_committed_integrity(),
2370 Err(expected.clone())
2371 );
2372 assert_eq!(
2373 store.commit(&oversized),
2374 Err(LedgerCommitError::Integrity(expected.clone()))
2375 );
2376 assert_eq!(
2377 oversized.stage_validated_generation(&validated(1, Vec::new()), None),
2378 Err(AllocationStageError::Integrity(expected))
2379 );
2380 assert_eq!(store, before, "{resource} refusal preserves both slots");
2381 match resource {
2382 "allocation records" => oversized.allocation_history.records.truncate(1),
2383 "generation history" => oversized.allocation_history.generations.truncate(1),
2384 _ => (),
2385 }
2386 }
2387 assert_eq!(store.recover(), before.recover());
2388 }
2389
2390 #[test]
2391 fn schema_collection_limit_accepts_exact_history_and_rejects_the_next_record() {
2392 let limit = crate::constants::MAX_LEDGER_GENERATIONS;
2393 let mut ledger = committed_ledger(limit as u64);
2394 let mut record = active_record("app.users.v1", 100);
2395 record.last_seen_generation = limit as u64;
2396 record.schema_history = (1..=limit as u64)
2397 .map(|generation| {
2398 SchemaMetadataRecord::new(generation, crate::SchemaMetadata::default())
2399 })
2400 .collect();
2401 ledger.allocation_history.records.push(record);
2402 assert_eq!(ledger.validate_committed_integrity(), Ok(()));
2403
2404 ledger
2406 .allocation_history
2407 .records
2408 .push(active_record("app.orders.v1", 101));
2409 assert_eq!(
2410 ledger.validate_committed_integrity(),
2411 Err(LedgerIntegrityError::LimitExceeded {
2412 resource: "schema history",
2413 limit,
2414 })
2415 );
2416 }
2417
2418 #[test]
2419 fn encoded_byte_limit_rejects_before_commit_mutation() {
2420 let mut store = LedgerCommitStore::default();
2421 store.commit(&committed_ledger(0)).unwrap();
2422 let before = store.clone();
2423 let mut ledger = committed_ledger(crate::constants::MAX_LEDGER_GENERATIONS as u64);
2424 for generation in &mut ledger.allocation_history.generations {
2425 generation.runtime_fingerprint =
2426 Some("x".repeat(crate::constants::DIAGNOSTIC_STRING_MAX_BYTES));
2427 }
2428 assert!(matches!(
2429 store.commit(&ledger),
2430 Err(LedgerCommitError::Integrity(
2431 LedgerIntegrityError::LimitExceeded {
2432 resource: "ledger bytes",
2433 ..
2434 }
2435 ))
2436 ));
2437 assert_eq!(store, before);
2438 let mut envelope = LedgerPayloadEnvelope::current(Vec::new())
2439 .try_encode()
2440 .unwrap();
2441 envelope[16..24]
2442 .copy_from_slice(&((crate::constants::MAX_LEDGER_BYTES + 1) as u64).to_le_bytes());
2443 assert!(matches!(
2444 LedgerPayloadEnvelope::decode(&envelope),
2445 Err(LedgerPayloadEnvelopeError::PayloadTooLarge { .. })
2446 ));
2447 }
2448}