use alloc::{
format,
string::{String, ToString},
};
use crate::{
algebra::ResourceVector,
wire::{BindingEpoch, ConnectionIncarnation, Generation},
};
use super::{BindingTerminalAdmission, BindingTerminalCauseClass, LiveFrontierOwner};
use crate::lifecycle::{
ActiveBinding, BindingTerminalDisposition, DetachedBindingTransition, DiedBindingTransition,
FrontierBinding, PendingFinalization, RetainedRecordCharge,
};
const CONVERSATION: u64 = 1;
const PARTICIPANT: u64 = 0;
const HIGH_WATERMARK: u64 = 3;
fn pending_terminal_owner(
class: BindingTerminalCauseClass,
) -> Result<(LiveFrontierOwner, PendingFinalization, BindingEpoch), String> {
let binding_epoch = BindingEpoch::new(
ConnectionIncarnation::new(1, 2),
Generation::new(1).ok_or_else(|| "test generation must be nonzero".to_string())?,
);
let active = ActiveBinding {
participant_id: PARTICIPANT,
conversation_id: CONVERSATION,
binding_epoch,
};
let owner = super::binding_fate_tests::frontier_owner_with_limit(
CONVERSATION,
PARTICIPANT,
binding_epoch,
0,
HIGH_WATERMARK,
0,
)?;
let prepared = owner
.prepare_binding_terminal(active, class, 0, HIGH_WATERMARK + 1, HIGH_WATERMARK)
.map_err(|refused| format!("terminal prepare refused: {:?}", refused.error()))?;
let key = prepared.candidate_key();
let BindingTerminalAdmission::Pending(pending) =
prepared.admit(key.bind_v3_charge(ResourceVector::new(1, 73)))
else {
return Err("the zero-cap terminal did not pend".to_string());
};
let (owner, position) = pending.into_parts();
let disposition = BindingTerminalDisposition::Pending(position);
let finalization = match class {
BindingTerminalCauseClass::Died => match active.connection_lost(disposition) {
DiedBindingTransition::Pending(died) => PendingFinalization::Died(died),
DiedBindingTransition::Committed(_) => {
return Err("a pending position committed the Died terminal".to_string());
}
},
BindingTerminalCauseClass::Detached => match active.clean_deregister(disposition) {
DetachedBindingTransition::Pending(detached) => PendingFinalization::Detached(detached),
DetachedBindingTransition::Committed(_) => {
return Err("a pending position committed the Detached terminal".to_string());
}
},
};
Ok((owner, finalization, binding_epoch))
}
fn drain(
owner: LiveFrontierOwner,
pending: PendingFinalization,
) -> Result<LiveFrontierOwner, String> {
let charge = RetainedRecordCharge::new(
HIGH_WATERMARK + 1,
pending.admission_order(),
ResourceVector::new(1, 73),
);
let drained = owner
.drain_pending_terminal(pending, charge)
.map_err(|refused| format!("terminal drain refused: {:?}", refused.error()))?;
Ok(drained.into_parts().0)
}
#[test]
fn a_drained_died_leaves_the_frontier_and_frees_its_identity_slot() -> Result<(), String> {
let (owner, pending, _) = pending_terminal_owner(BindingTerminalCauseClass::Died)?;
let before = owner.frontiers();
assert_eq!(before.active_identities().len(), 1);
assert_eq!(before.identity_slot_limit(), 1, "the fixture has one slot");
assert_eq!(before.sequence().ledger().claims().live_members(), 1);
assert_eq!(before.order().ledger().claims().membership_exits(), 1);
let drained = drain(owner, pending)?;
let frontiers = drained.frontiers();
assert!(
frontiers
.active_identities()
.participants()
.iter()
.all(|participant| participant.participant_index() != PARTICIPANT),
"the drained Died identity stayed in the frontier's active identities"
);
assert_eq!(
frontiers.active_identities().len(),
0,
"the drained Died identity still holds its identity slot"
);
assert!(
frontiers.active_identities().len() < frontiers.identity_slot_limit(),
"the identity slot the drained Died held is not free"
);
assert_eq!(
frontiers.sequence().ledger().claims().live_members(),
0,
"the drained Died identity kept its membership-exit sequence claim"
);
assert_eq!(
frontiers.order().ledger().claims().membership_exits(),
0,
"the drained Died identity kept its membership-exit order claim"
);
assert_eq!(
frontiers.sequence().ledger().high_watermark(),
HIGH_WATERMARK + 1,
"the drain did not sequence the terminal row"
);
assert!(
frontiers.sequence().immutable_candidates().is_empty(),
"the drain left its candidate in the lane"
);
Ok(())
}
#[test]
fn a_drained_detached_keeps_its_identity_at_its_dead_epoch() -> Result<(), String> {
let (owner, pending, binding_epoch) =
pending_terminal_owner(BindingTerminalCauseClass::Detached)?;
let drained = drain(owner, pending)?;
let frontiers = drained.frontiers();
let [participant] = frontiers.active_identities().participants() else {
return Err("the drained Detached identity left the frontier".to_string());
};
assert_eq!(participant.participant_index(), PARTICIPANT);
assert_eq!(
participant.binding(),
FrontierBinding::Detached(binding_epoch),
"the drained Detached identity is not detached at its dead epoch"
);
assert_eq!(frontiers.sequence().ledger().claims().live_members(), 1);
assert_eq!(frontiers.order().ledger().claims().membership_exits(), 1);
Ok(())
}