use std::sync::OnceLock;
use liminal_protocol::wire::{BindingEpoch, ConnectionIncarnation, Generation};
use super::restore_checkpoint_history::{
CheckpointHistoryNode, CheckpointHistoryRoot, HistoryCodecError, ROOT_ENCODED_BYTES,
};
use super::restore_checkpoint_history_key::{
CheckpointHistoryKey, HistoryEntryKey, HistoryLineageKey, HistoryOccurrenceKey,
};
struct Fixture {
keys: Vec<CheckpointHistoryKey>,
entry: CheckpointHistoryNode,
root: CheckpointHistoryRoot,
encoded_entry: Vec<u8>,
}
fn fixture_entry_keys(epoch: BindingEpoch) -> Vec<CheckpointHistoryKey> {
let entries = [
HistoryEntryKey::FateOccurrence {
participant_id: 2,
binding_epoch: epoch,
},
HistoryEntryKey::RetiredIdentity { participant_id: 2 },
HistoryEntryKey::EnrollmentReverse { token: [4; 16] },
HistoryEntryKey::CommittedAdmission {
token: [4; 16],
participant_id: 2,
fingerprint: [5; 32],
},
HistoryEntryKey::AttachProvenance {
participant_id: 2,
token: [4; 16],
},
HistoryEntryKey::AckSource { source_sequence: 6 },
HistoryEntryKey::RecipientObligation {
participant_id: 2,
delivery_sequence: 7,
},
HistoryEntryKey::AckFrontier { participant_id: 2 },
HistoryEntryKey::MarkerAckFrontier { participant_id: 2 },
HistoryEntryKey::OutboxRetiredIdentity { participant_id: 2 },
HistoryEntryKey::CursorFact {
participant_index: 2,
boundary: 8,
},
HistoryEntryKey::RetiredAdmission {
token: [4; 16],
participant_id: 2,
fingerprint: [5; 32],
},
];
entries
.into_iter()
.map(|entry| CheckpointHistoryKey::new(9, entry))
.collect()
}
static FIXTURE: OnceLock<Result<Fixture, HistoryCodecError>> = OnceLock::new();
fn fixture() -> Result<&'static Fixture, HistoryCodecError> {
FIXTURE
.get_or_init(|| {
let generation = Generation::new(3).ok_or(HistoryCodecError::Generation)?;
let epoch = BindingEpoch::new(ConnectionIncarnation::new(1, 2), generation);
let mut keys = fixture_entry_keys(epoch);
for occurrence in [
HistoryOccurrenceKey::AttachedTerminal {
participant_id: 2,
terminal_delivery_sequence: 7,
},
HistoryOccurrenceKey::DetachedTerminal {
participant_id: 2,
terminal_delivery_sequence: 7,
},
HistoryOccurrenceKey::DiedTerminal {
participant_id: 2,
terminal_delivery_sequence: 7,
},
HistoryOccurrenceKey::ParticipantAck {
participant_id: 2,
through_sequence: 7,
source_sequence: 6,
},
HistoryOccurrenceKey::MarkerAck {
participant_id: 2,
marker_delivery_sequence: 7,
resulting_cursor: 8,
base_log_head: 6,
extension_sequence: 3,
},
HistoryOccurrenceKey::Leave {
participant_id: 2,
left_delivery_sequence: 7,
},
HistoryOccurrenceKey::RecoveredFate {
participant_id: 2,
binding_epoch: epoch,
resulting_floor: 8,
},
] {
keys.push(CheckpointHistoryKey::new(
9,
HistoryEntryKey::ObserverOccurrence(occurrence),
));
}
for lineage in [
HistoryLineageKey::ParticipantCursor,
HistoryLineageKey::ParticipantTerminal,
HistoryLineageKey::BindingFateFloor,
] {
keys.push(CheckpointHistoryKey::new(
9,
HistoryEntryKey::ObserverLineage {
participant_id: 2,
lineage,
},
));
}
let entry = CheckpointHistoryNode::entry(keys[3], vec![1, 2, 3, 4]);
let encoded_entry = entry.encode_canonical()?;
let root = CheckpointHistoryRoot::for_node(&entry)?;
Ok(Fixture {
keys,
entry,
root,
encoded_entry,
})
})
.as_ref()
.map_err(|error| *error)
}
#[test]
fn every_history_key_keeps_its_full_typed_identity() -> Result<(), HistoryCodecError> {
let image = fixture()?;
assert_eq!(image.keys.len(), 22);
let mut distinct = std::collections::BTreeSet::new();
for key in &image.keys {
let bytes = key.encode_canonical();
assert_eq!(CheckpointHistoryKey::decode_canonical(&bytes, 9)?, *key);
assert!(distinct.insert(bytes));
}
assert_eq!(distinct.len(), 22);
Ok(())
}
#[test]
fn history_key_refuses_foreign_unknown_trailing_and_zero_generation()
-> Result<(), HistoryCodecError> {
let image = fixture()?;
let bytes = image.keys[0].encode_canonical();
assert!(matches!(
CheckpointHistoryKey::decode_canonical(&bytes, 10),
Err(HistoryCodecError::Conversation {
expected: 10,
actual: 9
})
));
let mut unknown = bytes.clone();
unknown[14] = 255;
assert!(matches!(
CheckpointHistoryKey::decode_canonical(&unknown, 9),
Err(HistoryCodecError::Tag { .. })
));
let mut trailing = bytes.clone();
trailing.push(0);
assert!(matches!(
CheckpointHistoryKey::decode_canonical(&trailing, 9),
Err(HistoryCodecError::Trailing)
));
let mut zero_generation = bytes;
let start = zero_generation.len() - 8;
zero_generation[start..].fill(0);
assert!(matches!(
CheckpointHistoryKey::decode_canonical(&zero_generation, 9),
Err(HistoryCodecError::Generation)
));
Ok(())
}
#[test]
fn history_root_schema_has_exact_algorithm_width_and_conversation() -> Result<(), HistoryCodecError>
{
let image = fixture()?;
let bytes = image.root.encode_canonical();
assert_eq!(bytes.len(), ROOT_ENCODED_BYTES);
assert_eq!(ROOT_ENCODED_BYTES, 15 + blake3::OUT_LEN);
assert_eq!(
CheckpointHistoryRoot::decode_canonical(&bytes, 9)?,
image.root
);
assert!(matches!(
CheckpointHistoryRoot::decode_canonical(&bytes[..bytes.len() - 1], 9),
Err(HistoryCodecError::Length)
));
let mut foreign_algorithm = bytes;
foreign_algorithm[6] = 2;
assert!(matches!(
CheckpointHistoryRoot::decode_canonical(&foreign_algorithm, 9),
Err(HistoryCodecError::Algorithm(2))
));
assert!(matches!(
CheckpointHistoryRoot::decode_canonical(&bytes, 10),
Err(HistoryCodecError::Conversation {
expected: 10,
actual: 9
})
));
Ok(())
}
#[test]
fn authenticated_history_node_rejects_content_and_key_association_damage()
-> Result<(), HistoryCodecError> {
let image = fixture()?;
assert_eq!(
CheckpointHistoryNode::decode_canonical(&image.encoded_entry, image.root)?,
image.entry
);
let mut damaged = image.encoded_entry.clone();
let last = damaged.len() - 1;
damaged[last] ^= 1;
assert!(matches!(
CheckpointHistoryNode::decode_canonical(&damaged, image.root),
Err(HistoryCodecError::Integrity)
));
assert_eq!(
&image.root.routed_node_key()[..27],
b"checkpoint/history/node/v1/"
);
Ok(())
}
#[test]
fn branch_children_and_depth_are_validated_before_publication() -> Result<(), HistoryCodecError> {
let image = fixture()?;
let empty = CheckpointHistoryRoot::empty(9);
let branch = CheckpointHistoryNode::branch(9, 0, image.root, empty)?;
let root = CheckpointHistoryRoot::for_node(&branch)?;
assert_eq!(
CheckpointHistoryNode::decode_canonical(&branch.encode_canonical()?, root)?,
branch
);
assert!(matches!(
CheckpointHistoryNode::branch(9, 256, image.root, empty),
Err(HistoryCodecError::Depth(256))
));
assert!(matches!(
CheckpointHistoryNode::branch(9, 0, image.root, CheckpointHistoryRoot::empty(10)),
Err(HistoryCodecError::Conversation {
expected: 9,
actual: 10
})
));
Ok(())
}