use chrono::{DateTime, Utc};
use super::{current_step, open_steps};
use crate::{
ActivityError, ActivityErrorKind, ActivityId, Event, EventEnvelope, Payload, RunId, StepState,
WorkflowId,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn workflow_id() -> WorkflowId {
WorkflowId::new(uuid::Uuid::from_u128(1))
}
fn recorded_at(offset: i64) -> DateTime<Utc> {
DateTime::from_timestamp(1_700_000_000 + offset, 0).unwrap_or_default()
}
fn envelope(seq: u64) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: recorded_at(i64::try_from(seq).unwrap_or(0)),
workflow_id: workflow_id(),
}
}
fn payload() -> Result<Payload, crate::PayloadError> {
Payload::from_json(&serde_json::json!({ "input": true }))
}
fn started(seq: u64) -> Result<Event, crate::PayloadError> {
Ok(Event::WorkflowStarted {
envelope: envelope(seq),
workflow_type: "fixture".to_owned(),
input: payload()?,
run_id: RunId::new(uuid::Uuid::from_u128(10)),
parent_run_id: None,
package_version: crate::PackageVersion::new("a".repeat(64)),
})
}
fn scheduled(seq: u64, ordinal: u64, activity_type: &str) -> Result<Event, crate::PayloadError> {
Ok(Event::ActivityScheduled {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
activity_type: activity_type.to_owned(),
input: payload()?,
task_queue: "agents".to_owned(),
node: Some("node-a".to_owned()),
})
}
fn dispatched(seq: u64, ordinal: u64, attempt: u32) -> Event {
Event::ActivityStarted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
attempt,
}
}
fn completed(seq: u64, ordinal: u64, attempt: u32) -> Result<Event, crate::PayloadError> {
Ok(Event::ActivityCompleted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
result: payload()?,
attempt,
})
}
fn failed(seq: u64, ordinal: u64, attempt: u32) -> Event {
Event::ActivityFailed {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
error: ActivityError {
kind: ActivityErrorKind::Retryable,
message: "boom".to_owned(),
details: None,
},
attempt,
}
}
fn cancelled(seq: u64, ordinal: u64, attempt: u32) -> Event {
Event::ActivityCancelled {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
attempt,
}
}
#[test]
fn a_run_with_no_activity_has_no_current_step() -> TestResult {
let history = vec![started(1)?];
assert_eq!(current_step(&history), None);
assert!(open_steps(&history).is_empty());
Ok(())
}
#[test]
fn an_empty_history_folds_to_nothing() {
assert_eq!(current_step(&[]), None);
assert!(open_steps(&[]).is_empty());
}
#[test]
fn a_scheduled_step_is_current_and_carries_its_stamped_address() -> TestResult {
let history = vec![started(1)?, scheduled(2, 4, "review")?];
let step = current_step(&history).ok_or("scheduled step must be current")?;
assert_eq!(step.activity_id, ActivityId::from_sequence_position(4));
assert_eq!(step.activity_type, "review");
assert_eq!(step.task_queue, "agents");
assert_eq!(step.node.as_deref(), Some("node-a"));
assert_eq!(step.scheduled_at, recorded_at(2));
assert_eq!(step.state, StepState::Scheduled);
Ok(())
}
#[test]
fn dispatch_advances_the_same_ordinal() -> TestResult {
let history = vec![started(1)?, scheduled(2, 4, "review")?, dispatched(3, 4, 1)];
assert_eq!(open_steps(&history).len(), 1, "one ordinal, not two");
let step = current_step(&history).ok_or("dispatched step must be current")?;
assert_eq!(
step.state,
StepState::Dispatched {
attempt: 1,
dispatched_at: recorded_at(3),
}
);
Ok(())
}
#[test]
fn each_terminal_retires_only_its_own_ordinal() -> TestResult {
let terminals: Vec<Event> = vec![completed(5, 4, 1)?, failed(5, 4, 1), cancelled(5, 4, 1)];
for terminal in terminals {
let history = vec![
started(1)?,
scheduled(2, 4, "review")?,
dispatched(3, 4, 1),
scheduled(4, 6, "survivor")?,
terminal.clone(),
];
let open = open_steps(&history);
assert_eq!(
open.len(),
1,
"the terminal {terminal:?} must retire exactly its own ordinal"
);
assert_eq!(open[0].activity_type, "survivor");
let step = current_step(&history).ok_or("the survivor is current")?;
assert_eq!(step.activity_type, "survivor");
}
Ok(())
}
#[test]
fn a_retried_ordinal_reports_the_new_attempt_once() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "review")?,
dispatched(3, 4, 1),
failed(4, 4, 1),
scheduled(5, 4, "review")?,
dispatched(6, 4, 2),
];
let open = open_steps(&history);
assert_eq!(open.len(), 1);
assert_eq!(
open[0].state,
StepState::Dispatched {
attempt: 2,
dispatched_at: recorded_at(6),
}
);
Ok(())
}
#[test]
fn advisory_exhaustion_is_not_a_terminal() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "notify")?,
dispatched(3, 4, 1),
Event::ActivityAdvisoryExhausted {
envelope: envelope(4),
activity_id: ActivityId::from_sequence_position(4),
activity_type: "notify".to_owned(),
reason: "boom".to_owned(),
attempt: 1,
},
];
let open = open_steps(&history);
assert_eq!(open.len(), 1, "the advisory marker retires nothing");
assert_eq!(
open[0].state,
StepState::Dispatched {
attempt: 1,
dispatched_at: recorded_at(3),
}
);
let mut with_failure = history;
with_failure.push(failed(5, 4, 1));
assert!(open_steps(&with_failure).is_empty());
Ok(())
}
#[test]
fn a_reopen_reopens_only_the_activities_it_names() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "review")?,
dispatched(3, 4, 1),
failed(4, 4, 1),
scheduled(5, 6, "publish")?,
dispatched(6, 6, 1),
completed(7, 6, 1)?,
Event::WorkflowReopened {
envelope: envelope(8),
run_id: RunId::new(uuid::Uuid::from_u128(10)),
reopened: vec![ActivityId::from_sequence_position(4)],
},
];
let open = open_steps(&history);
assert_eq!(open.len(), 1, "only the named ordinal comes back");
assert_eq!(open[0].activity_type, "review");
assert_eq!(
open[0].state,
StepState::Reopened {
reopened_at: recorded_at(8),
},
"a reopened ordinal has not been dispatched in this lease"
);
assert_eq!(open[0].task_queue, "agents");
assert_eq!(open[0].scheduled_at, recorded_at(2));
Ok(())
}
#[test]
fn a_reopen_of_an_unscheduled_ordinal_reports_nothing() -> TestResult {
let history = vec![
started(1)?,
Event::WorkflowReopened {
envelope: envelope(2),
run_id: RunId::new(uuid::Uuid::from_u128(10)),
reopened: vec![ActivityId::from_sequence_position(4)],
},
];
assert!(open_steps(&history).is_empty());
assert_eq!(current_step(&history), None);
Ok(())
}
#[test]
fn re_dispatch_after_a_reopen_advances_the_ordinal() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "review")?,
dispatched(3, 4, 1),
failed(4, 4, 1),
Event::WorkflowReopened {
envelope: envelope(5),
run_id: RunId::new(uuid::Uuid::from_u128(10)),
reopened: vec![ActivityId::from_sequence_position(4)],
},
scheduled(6, 4, "review")?,
dispatched(7, 4, 2),
];
let open = open_steps(&history);
assert_eq!(open.len(), 1);
assert_eq!(
open[0].state,
StepState::Dispatched {
attempt: 2,
dispatched_at: recorded_at(7),
}
);
Ok(())
}
#[test]
fn a_new_run_segment_drops_the_prior_segments_open_step() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "old")?,
dispatched(3, 4, 1),
started(4)?,
scheduled(5, 1, "new")?,
];
let open = open_steps(&history);
assert_eq!(open.len(), 1, "only the active segment is folded");
assert_eq!(open[0].activity_type, "new");
Ok(())
}
#[test]
fn a_dispatch_without_a_scheduling_opens_nothing() -> TestResult {
let history = vec![started(1)?, dispatched(2, 4, 1)];
assert!(open_steps(&history).is_empty());
assert_eq!(current_step(&history), None);
Ok(())
}
#[test]
fn a_fan_out_keeps_every_sibling_and_current_is_the_most_recently_advanced() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "left")?,
scheduled(3, 5, "middle")?,
scheduled(4, 6, "right")?,
dispatched(5, 5, 1),
];
let open = open_steps(&history);
assert_eq!(open.len(), 3, "no sibling is lost");
assert_eq!(
open.iter()
.map(|step| step.activity_type.as_str())
.collect::<Vec<_>>(),
vec!["left", "middle", "right"],
"siblings stay in first-opened order"
);
let step = current_step(&history).ok_or("a fan-out still has a current step")?;
assert_eq!(step.activity_type, "middle");
Ok(())
}
#[test]
fn unrelated_events_neither_open_nor_retire_a_step() -> TestResult {
let history = vec![
started(1)?,
scheduled(2, 4, "review")?,
dispatched(3, 4, 1),
completed(4, 4, 1)?,
Event::TimerStarted {
envelope: envelope(5),
timer_id: crate::TimerId::anonymous(1),
fire_at: recorded_at(60),
},
];
assert_eq!(current_step(&history), None);
assert!(open_steps(&history).is_empty());
Ok(())
}