aion-rs 0.25.1

Transport-agnostic Aion workflow engine with durability, replay, timers, and supervision.
Documentation
use super::*;

#[tokio::test(flavor = "multi_thread")]
async fn pinned_base_is_reused_across_reentries_and_counter_advances_once() -> TestResult {
    let harness = CollectHarness::over_events(&pending_batch(&["alpha", "beta"])).await?;
    let two = specs(&["alpha", "beta"]);

    // First arrival: allocates the base once and pins it.
    assert_eq!(
        harness.step(CollectKind::All, &two),
        Ok(CollectStep::Suspend)
    );
    assert!(matches!(
        harness.pinned(),
        Some(PendingAwait::Collect {
            base_ordinal: 0,
            count: 2,
            kind: CollectKind::All,
        })
    ));
    assert_eq!(harness.handle.activity_ordinals_allocated(), 2);

    // Wake re-entry: the pinned base is reused, the counter must not
    // advance a second time.
    assert_eq!(
        harness.step(CollectKind::All, &two),
        Ok(CollectStep::Suspend)
    );
    assert!(matches!(
        harness.pinned(),
        Some(PendingAwait::Collect {
            base_ordinal: 0,
            ..
        })
    ));
    assert_eq!(harness.handle.activity_ordinals_allocated(), 2);
    harness.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn fail_fast_returns_lowest_ordinal_failure_and_cancels_unresolved() -> TestResult {
    // Recorded failures at ordinals 1 and 2; ordinal 0 still pending.
    let mut events = pending_batch(&["a", "b", "c"]);
    events.push(failed(1, "boom-b"));
    events.push(failed(2, "boom-c"));
    let harness = CollectHarness::over_events(&events).await?;

    let step = harness.step(CollectKind::All, &specs(&["a", "b", "c"]));

    assert_eq!(step, Ok(CollectStep::FailFast("boom-b".to_owned())));
    assert_eq!(harness.cancelled_ordinals().await?, vec![0]);
    assert_eq!(harness.pinned(), None);
    harness.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn cancellation_set_covers_exactly_the_unresolved_ordinals() -> TestResult {
    // 0 completed, 2 failed, 1 and 3 pending: the cancel set is {1, 3}.
    let mut events = pending_batch(&["a", "b", "c", "d"]);
    events.push(completed(0, r#""done-a""#));
    events.push(failed(2, "boom-c"));
    let harness = CollectHarness::over_events(&events).await?;

    let step = harness.step(CollectKind::All, &specs(&["a", "b", "c", "d"]));

    assert_eq!(step, Ok(CollectStep::FailFast("boom-c".to_owned())));
    assert_eq!(harness.cancelled_ordinals().await?, vec![1, 3]);
    assert_eq!(harness.pinned(), None);
    harness.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn race_winner_is_first_recorded_terminal_not_lowest_ordinal() -> TestResult {
    // Ordinal 1 settled first (its terminal is recorded); ordinal 0 is
    // still pending and must be cancelled, not preferred.
    let mut events = pending_batch(&["a", "b"]);
    events.push(completed(1, r#""win-b""#));
    let harness = CollectHarness::over_events(&events).await?;

    let step = harness.step(CollectKind::Race, &specs(&["a", "b"]));

    assert_eq!(step, Ok(CollectStep::RaceWon(Ok(r#""win-b""#.to_owned()))));
    assert_eq!(harness.cancelled_ordinals().await?, vec![0]);
    assert_eq!(harness.pinned(), None);

    // First-settle includes failure: a recorded failure wins the race.
    let mut events = pending_batch(&["a", "b"]);
    events.push(failed(1, "boom-b"));
    let failing = CollectHarness::over_events(&events).await?;
    assert_eq!(
        failing.step(CollectKind::Race, &specs(&["a", "b"])),
        Ok(CollectStep::RaceWon(Err("boom-b".to_owned())))
    );
    assert_eq!(failing.cancelled_ordinals().await?, vec![0]);
    harness.shutdown()?;
    failing.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn race_batch_tie_breaks_to_lowest_ordinal_and_drains_loser_entries() -> TestResult {
    let harness = CollectHarness::over_events(&pending_batch(&["a", "b"])).await?;
    // Both completions sit in the runtime maps on one wake.
    harness.deps.runtime.deliver_activity_completion_message(
        harness.pid,
        "activity:0",
        r#""r0""#.to_owned(),
    )?;
    harness.deps.runtime.deliver_activity_completion_message(
        harness.pid,
        "activity:1",
        r#""r1""#.to_owned(),
    )?;

    let step = harness.step(CollectKind::Race, &specs(&["a", "b"]));

    assert_eq!(step, Ok(CollectStep::RaceWon(Ok(r#""r0""#.to_owned()))));
    assert_eq!(harness.cancelled_ordinals().await?, vec![1]);
    // The loser's retained entry was dropped at settle (D5 hygiene).
    assert_eq!(harness.deps.runtime.retained_activity_completions(), 0);
    let history = harness.store.read_history(&harness.workflow_id).await?;
    let winner_terminals = history
        .iter()
        .filter(|event| {
            matches!(
                event,
                Event::ActivityCompleted { .. } | Event::ActivityFailed { .. }
            )
        })
        .count();
    assert_eq!(
        winner_terminals, 1,
        "exactly one non-cancelled terminal may exist: {history:#?}"
    );
    harness.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn empty_list_resolves_immediately_without_pinning() -> TestResult {
    let harness = CollectHarness::over_events(&[]).await?;

    assert_eq!(
        harness.step(CollectKind::All, &[]),
        Ok(CollectStep::AllCompleted(Vec::new()))
    );
    assert_eq!(harness.pinned(), None);
    assert_eq!(harness.handle.activity_ordinals_allocated(), 0);

    let race = harness.step(CollectKind::Race, &[]);
    assert_eq!(race, Err("expected at least one activity".to_owned()));
    assert_eq!(harness.pinned(), None);
    harness.shutdown()
}

#[tokio::test(flavor = "multi_thread")]
async fn expired_scope_cancels_unresolved_and_replay_derives_the_same_abort() -> TestResult {
    let mut events = pending_batch(&["a", "b"]);
    events.push(completed(0, r#""done-a""#));
    let harness = CollectHarness::over_events(&events).await?;
    harness
        .state
        .timeout_scopes
        .insert(9, TimeoutScope::replayed_for_test(harness.pid, true));
    harness
        .state
        .timeout_scope_stacks
        .insert(harness.pid, vec![9]);

    let step = harness.step(CollectKind::All, &specs(&["a", "b"]));

    assert_eq!(
        step,
        Ok(CollectStep::ScopeExpired(
            "timeout:deadline expired".to_owned()
        ))
    );
    assert_eq!(harness.cancelled_ordinals().await?, vec![1]);
    assert_eq!(harness.pinned(), None);
    let store = Arc::clone(&harness.store);
    let workflow_id = harness.workflow_id.clone();
    let run_id = harness.handle.run_id().clone();
    let history_len = store.read_history(&workflow_id).await?.len();
    harness.shutdown()?;

    // Fresh engine epoch over the same store (the restart analogue):
    // the recorded cancelled-without-failure set derives the same abort
    // and appends nothing.
    let replay = CollectHarness::over_store(store, workflow_id, run_id).await?;
    assert_eq!(
        replay.step(CollectKind::All, &specs(&["a", "b"])),
        Ok(CollectStep::ScopeExpired(
            "timeout:deadline expired".to_owned()
        ))
    );
    assert_eq!(
        replay.store.read_history(&replay.workflow_id).await?.len(),
        history_len,
        "replay must append nothing"
    );
    assert_eq!(replay.pinned(), None);
    replay.shutdown()
}

/// aion#1: a settled race advances workflow-visible now over EVERY member's
/// recorded terminal — the WINNER included.
///
/// A one-member race has no loser cancellation to cover the winner, so a
/// settlement that observed only the losers would leave a replayed run's
/// position behind its live original's: live advanced when it recorded the
/// completion, replay would advance to nothing. The same gap opens on a
/// multi-member race whose losers already carried earlier terminals.
#[tokio::test(flavor = "multi_thread")]
async fn race_settlement_advances_now_over_the_recorded_winner() -> TestResult {
    let mut events = pending_batch(&["a"]);
    events.push(completed(0, r#""win-a""#));
    let harness = CollectHarness::over_events(&events).await?;

    // A fresh engine epoch: this handle has consumed nothing yet, so the
    // position cell is unset and `now` would answer the run's start.
    assert_eq!(
        harness.handle.workflow_now_millis(),
        None,
        "a fresh handle starts with no consumed position"
    );

    let step = harness.step(CollectKind::Race, &specs(&["a"]));
    assert_eq!(step, Ok(CollectStep::RaceWon(Ok(r#""win-a""#.to_owned()))));

    let history = harness.store.read_history(&harness.workflow_id).await?;
    let winner_recorded_at = history
        .iter()
        .find_map(|event| match event {
            Event::ActivityCompleted { .. } => Some(*event.recorded_at()),
            _ => None,
        })
        .ok_or("the winner's ActivityCompleted must be in history")?;

    assert_eq!(
        harness.handle.workflow_now_millis(),
        Some(winner_recorded_at.timestamp_millis()),
        "the settled race must advance workflow-visible now to the winner's recorded terminal"
    );
    harness.shutdown()
}