use std::error::Error;
use liminal_protocol::lifecycle::{
ObserverProgressAdvanceDecision, ObserverProgressAdvanceError, ObserverProgressRetireDecision,
ObserverProgressTrackDecision, ObserverProgressTrackError, ObserverRecoveryAggregate,
};
use super::observer::{ObserverRow, encode_for_tests};
const HISTORIES: [u64; 2] = [100, 1_000];
const LIVE_CONVERSATION: u64 = 1_000_000;
const COMPLETED_PROGRESS: u64 = 2;
const LIVE_PROGRESS: u64 = 3;
fn track(
aggregate: ObserverRecoveryAggregate,
conversation_id: u64,
) -> Result<ObserverRecoveryAggregate, Box<dyn Error>> {
match aggregate.decide_track(conversation_id, 0) {
ObserverProgressTrackDecision::Commit(transaction) => Ok(transaction.commit()),
ObserverProgressTrackDecision::Refuse { error, .. } => {
Err(format!("track of {conversation_id} refused: {error:?}").into())
}
}
}
fn advance(
aggregate: ObserverRecoveryAggregate,
conversation_id: u64,
progress: u64,
) -> Result<ObserverRecoveryAggregate, Box<dyn Error>> {
match aggregate.decide_progress_advance(conversation_id, progress) {
ObserverProgressAdvanceDecision::Commit(transaction) => Ok(transaction.commit().0),
ObserverProgressAdvanceDecision::Refuse { error, .. } => {
Err(format!("advance of {conversation_id} to {progress} refused: {error:?}").into())
}
}
}
fn completed_then_live(
conversations: u64,
) -> Result<(ObserverRecoveryAggregate, u64), Box<dyn Error>> {
let mut aggregate = ObserverRecoveryAggregate::new();
let mut committed = 0_u64;
for conversation_id in 1..=conversations {
aggregate = track(aggregate, conversation_id)?;
committed += 1;
for progress in 1..=COMPLETED_PROGRESS {
aggregate = advance(aggregate, conversation_id, progress)?;
committed += 1;
}
aggregate = retire(aggregate, conversation_id)?;
committed += 1;
}
aggregate = track(aggregate, LIVE_CONVERSATION)?;
committed += 1;
for progress in 1..=LIVE_PROGRESS {
aggregate = advance(aggregate, LIVE_CONVERSATION, progress)?;
committed += 1;
}
Ok((aggregate, committed))
}
fn retire(
aggregate: ObserverRecoveryAggregate,
conversation_id: u64,
) -> Result<ObserverRecoveryAggregate, Box<dyn Error>> {
match aggregate.decide_retire(conversation_id) {
ObserverProgressRetireDecision::Commit(transaction) => Ok(transaction.commit()),
ObserverProgressRetireDecision::Refuse { error, .. } => {
Err(format!("retire of {conversation_id} refused: {error:?}").into())
}
}
}
fn checkpoint_of(aggregate: &ObserverRecoveryAggregate) -> Result<(usize, usize), Box<dyn Error>> {
let encoded = encode_for_tests(&ObserverRow::snapshot_of(aggregate))?;
Ok((aggregate.progress_rows().len(), encoded.len()))
}
#[test]
fn completed_conversations_leave_the_live_answers_unchanged() -> Result<(), Box<dyn Error>> {
let [small, large] = HISTORIES;
let (smaller, small_commits) = completed_then_live(small)?;
let (larger, large_commits) = completed_then_live(large)?;
let per_conversation = COMPLETED_PROGRESS + 2;
assert_eq!(
large_commits - small_commits,
(large - small) * per_conversation,
"positive control failed: the larger history did not commit its extra transitions"
);
for aggregate in [&smaller, &larger] {
assert_eq!(
aggregate.observer_progress(LIVE_CONVERSATION),
Some(LIVE_PROGRESS),
"the live conversation's progress differs with the history behind it"
);
}
let ObserverProgressTrackDecision::Refuse { aggregate, error } =
larger.decide_track(LIVE_CONVERSATION, 0)
else {
return Err("a second track of the live conversation was not refused".into());
};
assert_eq!(
error,
ObserverProgressTrackError::AlreadyTracked {
conversation_id: LIVE_CONVERSATION
}
);
let ObserverProgressAdvanceDecision::Refuse { error, .. } =
aggregate.decide_progress_advance(LIVE_CONVERSATION, LIVE_PROGRESS)
else {
return Err("a non-advancing advance of the live conversation was not refused".into());
};
assert_eq!(
error,
ObserverProgressAdvanceError::NotAdvancing {
conversation_id: LIVE_CONVERSATION,
current_observer_progress: LIVE_PROGRESS,
presented_progress: LIVE_PROGRESS,
}
);
Ok(())
}
#[test]
fn completed_conversations_leave_the_observer_checkpoint() -> Result<(), Box<dyn Error>> {
let [small, large] = HISTORIES;
let (smaller, _) = completed_then_live(small)?;
let (larger, _) = completed_then_live(large)?;
let (small_rows, small_bytes) = checkpoint_of(&smaller)?;
let (large_rows, large_bytes) = checkpoint_of(&larger)?;
let mut grown = Vec::new();
if large_rows != small_rows {
grown.push(format!(
"progress rows {large_rows} at {large} vs {small_rows} at {small}"
));
}
if large_bytes != small_bytes {
grown.push(format!(
"checkpoint encoded bytes {large_bytes} at {large} vs {small_bytes} at {small}"
));
}
assert!(
grown.is_empty(),
"R3b review finding 21: with the same live tail the observer checkpoint grew with \
completed conversations, expected equal: {}",
grown.join("; ")
);
Ok(())
}