liminal-protocol 0.14.0

Shared participant-lifecycle protocol types for liminal
Documentation
//! DUR-027 R2: a closed conversation retires from the observer aggregate by a
//! barriered transition that refuses an unknown or armed conversation by name.

use alloc::vec;

use super::{
    ObserverProgressRetireDecision, ObserverProgressRetireError, ObserverProgressTrackDecision,
    ObserverRecoveryAggregate,
};

/// Three tracked conversations (1, 2 and 3 at progress 4, 5 and 6) and one arm,
/// installed on conversation 3 at its progress.
fn three_with_an_arm() -> Result<ObserverRecoveryAggregate, &'static str> {
    ObserverRecoveryAggregate::restore(&[(1, 4), (2, 5), (3, 6)], &[(3, 6)])
        .map_err(|_| "the fixture's restore was refused")
}

#[test]
fn retire_removes_exactly_its_row_and_leaves_every_other_row_and_arm() -> Result<(), &'static str> {
    let ObserverProgressRetireDecision::Commit(transaction) = three_with_an_arm()?.decide_retire(2)
    else {
        return Err("the retirement of a tracked, unarmed conversation was refused");
    };
    assert_eq!(transaction.conversation_id(), 2);
    let aggregate = transaction.commit();
    assert_eq!(aggregate.progress_rows(), vec![(1, 4), (3, 6)]);
    assert_eq!(aggregate.armed_rows(), vec![(3, 6)]);
    assert_eq!(aggregate.observer_progress(2), None);
    Ok(())
}

#[test]
fn an_untracked_conversation_refuses_unknown_with_the_aggregate_unchanged()
-> Result<(), &'static str> {
    let before = three_with_an_arm()?;
    let decision = three_with_an_arm()?.decide_retire(9);
    assert_eq!(
        decision,
        ObserverProgressRetireDecision::Refuse {
            aggregate: before,
            error: ObserverProgressRetireError::ConversationUnknown { conversation_id: 9 },
        }
    );
    Ok(())
}

#[test]
fn an_armed_conversation_refuses_armed_with_the_aggregate_unchanged() -> Result<(), &'static str> {
    let before = three_with_an_arm()?;
    let decision = three_with_an_arm()?.decide_retire(3);
    assert_eq!(
        decision,
        ObserverProgressRetireDecision::Refuse {
            aggregate: before,
            error: ObserverProgressRetireError::Armed {
                conversation_id: 3,
                epoch: 6,
            },
        }
    );
    Ok(())
}

#[test]
fn an_aborted_retirement_keeps_the_row() -> Result<(), &'static str> {
    let before = three_with_an_arm()?;
    let ObserverProgressRetireDecision::Commit(transaction) = three_with_an_arm()?.decide_retire(1)
    else {
        return Err("the retirement of a tracked, unarmed conversation was refused");
    };
    assert_eq!(transaction.abort(), before);
    Ok(())
}

/// The aggregate keeps no retired set: the same id tracks again after its
/// retirement. The server's tombstone, not the aggregate, refuses a retired id.
#[test]
fn the_aggregate_does_not_remember_a_retired_id() -> Result<(), &'static str> {
    let ObserverProgressRetireDecision::Commit(transaction) = three_with_an_arm()?.decide_retire(1)
    else {
        return Err("the retirement of a tracked, unarmed conversation was refused");
    };
    let retired = transaction.commit();
    let ObserverProgressTrackDecision::Commit(track) = retired.decide_track(1, 0) else {
        return Err("a retired id was refused a fresh track");
    };
    assert_eq!(track.commit().observer_progress(1), Some(0));
    Ok(())
}