use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{
ActivityError, ActivityId, PackageVersion, Payload, RunId, ScheduleConfig, ScheduleId,
SearchAttributeValue, TimerId, WorkflowError, WorkflowId,
};
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
pub struct EventEnvelope {
pub seq: u64,
pub recorded_at: DateTime<Utc>,
pub workflow_id: WorkflowId,
}
pub const DEFAULT_TASK_QUEUE: &str = "default";
fn default_task_queue() -> String {
String::from(DEFAULT_TASK_QUEUE)
}
const LEGACY_ACTIVITY_ATTEMPT: u32 = 0;
fn legacy_activity_attempt() -> u32 {
LEGACY_ACTIVITY_ATTEMPT
}
pub const START_TIME_TASK_QUEUE_ATTRIBUTE: &str = "aion.task_queue";
#[must_use]
pub fn start_time_task_queue(events: &[Event]) -> Option<String> {
let attributes = crate::search_attributes_from_events(events);
match attributes.get(START_TIME_TASK_QUEUE_ATTRIBUTE) {
Some(crate::SearchAttributeValue::String(queue)) => Some(queue.clone()),
_ => None,
}
}
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq)]
#[serde(tag = "type", content = "data")]
pub enum Event {
WorkflowStarted {
envelope: EventEnvelope,
workflow_type: String,
input: Payload,
run_id: RunId,
parent_run_id: Option<RunId>,
package_version: PackageVersion,
},
WorkflowCompleted {
envelope: EventEnvelope,
result: Payload,
},
WorkflowFailed {
envelope: EventEnvelope,
error: WorkflowError,
},
WorkflowCancelled {
envelope: EventEnvelope,
reason: String,
},
WorkflowTimedOut {
envelope: EventEnvelope,
timeout: String,
},
WorkflowContinuedAsNew {
envelope: EventEnvelope,
input: Payload,
workflow_type: Option<String>,
parent_run_id: RunId,
},
WorkflowReopened {
envelope: EventEnvelope,
run_id: RunId,
reopened: Vec<ActivityId>,
},
WorkflowPaused {
envelope: EventEnvelope,
run_id: RunId,
reason: Option<String>,
operator: Option<String>,
},
WorkflowResumed {
envelope: EventEnvelope,
run_id: RunId,
operator: Option<String>,
},
SearchAttributesUpdated {
envelope: EventEnvelope,
workflow_id: WorkflowId,
attributes: HashMap<String, SearchAttributeValue>,
},
ActivityScheduled {
envelope: EventEnvelope,
activity_id: ActivityId,
activity_type: String,
input: Payload,
#[serde(default = "default_task_queue")]
task_queue: String,
#[serde(default)]
node: Option<String>,
},
ActivityStarted {
envelope: EventEnvelope,
activity_id: ActivityId,
#[serde(default = "legacy_activity_attempt")]
attempt: u32,
},
ActivityCompleted {
envelope: EventEnvelope,
activity_id: ActivityId,
result: Payload,
#[serde(default = "legacy_activity_attempt")]
attempt: u32,
},
ActivityFailed {
envelope: EventEnvelope,
activity_id: ActivityId,
error: ActivityError,
attempt: u32,
},
ActivityCancelled {
envelope: EventEnvelope,
activity_id: ActivityId,
#[serde(default = "legacy_activity_attempt")]
attempt: u32,
},
TimerStarted {
envelope: EventEnvelope,
timer_id: TimerId,
fire_at: DateTime<Utc>,
},
TimerFired {
envelope: EventEnvelope,
timer_id: TimerId,
},
TimerCancelled {
envelope: EventEnvelope,
timer_id: TimerId,
#[serde(default)]
cause: TimerCancelCause,
},
WithTimeoutCompleted {
envelope: EventEnvelope,
timer_id: TimerId,
outcome: WithTimeoutOutcome,
result: Option<Payload>,
},
SignalReceived {
envelope: EventEnvelope,
name: String,
payload: Payload,
},
SignalSent {
envelope: EventEnvelope,
target_workflow_id: WorkflowId,
name: String,
payload: Payload,
},
ChildWorkflowStarted {
envelope: EventEnvelope,
child_workflow_id: WorkflowId,
workflow_type: String,
input: Payload,
package_version: PackageVersion,
},
ChildWorkflowCompleted {
envelope: EventEnvelope,
child_workflow_id: WorkflowId,
result: Payload,
},
ChildWorkflowFailed {
envelope: EventEnvelope,
child_workflow_id: WorkflowId,
error: WorkflowError,
},
ChildWorkflowCancelled {
envelope: EventEnvelope,
child_workflow_id: WorkflowId,
},
ScheduleCreated {
envelope: EventEnvelope,
schedule_id: ScheduleId,
config: ScheduleConfig,
},
ScheduleUpdated {
envelope: EventEnvelope,
schedule_id: ScheduleId,
config: ScheduleConfig,
},
SchedulePaused {
envelope: EventEnvelope,
schedule_id: ScheduleId,
},
ScheduleResumed {
envelope: EventEnvelope,
schedule_id: ScheduleId,
},
ScheduleDeleted {
envelope: EventEnvelope,
schedule_id: ScheduleId,
},
ScheduleTriggered {
envelope: EventEnvelope,
schedule_id: ScheduleId,
workflow_id: WorkflowId,
run_id: RunId,
},
}
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
pub enum WithTimeoutOutcome {
OperationCompleted,
TimedOut,
}
#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TimerCancelCause {
#[default]
WorkflowIntent,
CancelTeardown,
}
impl Event {
#[must_use]
pub const fn envelope(&self) -> &EventEnvelope {
match self {
Self::WorkflowStarted { envelope, .. }
| Self::WorkflowCompleted { envelope, .. }
| Self::WorkflowFailed { envelope, .. }
| Self::WorkflowCancelled { envelope, .. }
| Self::WorkflowTimedOut { envelope, .. }
| Self::WorkflowContinuedAsNew { envelope, .. }
| Self::WorkflowReopened { envelope, .. }
| Self::WorkflowPaused { envelope, .. }
| Self::WorkflowResumed { envelope, .. }
| Self::SearchAttributesUpdated { envelope, .. }
| Self::ActivityScheduled { envelope, .. }
| Self::ActivityStarted { envelope, .. }
| Self::ActivityCompleted { envelope, .. }
| Self::ActivityFailed { envelope, .. }
| Self::ActivityCancelled { envelope, .. }
| Self::TimerStarted { envelope, .. }
| Self::TimerFired { envelope, .. }
| Self::TimerCancelled { envelope, .. }
| Self::WithTimeoutCompleted { envelope, .. }
| Self::SignalReceived { envelope, .. }
| Self::SignalSent { envelope, .. }
| Self::ChildWorkflowStarted { envelope, .. }
| Self::ChildWorkflowCompleted { envelope, .. }
| Self::ChildWorkflowFailed { envelope, .. }
| Self::ChildWorkflowCancelled { envelope, .. }
| Self::ScheduleCreated { envelope, .. }
| Self::ScheduleUpdated { envelope, .. }
| Self::SchedulePaused { envelope, .. }
| Self::ScheduleResumed { envelope, .. }
| Self::ScheduleDeleted { envelope, .. }
| Self::ScheduleTriggered { envelope, .. } => envelope,
}
}
#[must_use]
pub const fn seq(&self) -> u64 {
self.envelope().seq
}
#[must_use]
pub const fn recorded_at(&self) -> &DateTime<Utc> {
&self.envelope().recorded_at
}
#[must_use]
pub const fn workflow_id(&self) -> &WorkflowId {
&self.envelope().workflow_id
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde_json::json;
use super::{
DEFAULT_TASK_QUEUE, Event, EventEnvelope, LEGACY_ACTIVITY_ATTEMPT, TimerCancelCause,
};
use crate::{
ActivityError, ActivityErrorKind, ActivityId, CatchUpPolicy, OverlapPolicy, PackageVersion,
Payload, RunId, ScheduleConfig, ScheduleId, SearchAttributeValue, TimerId, TriggerSpec,
WorkflowError, WorkflowId,
};
fn package_version() -> PackageVersion {
PackageVersion::new("a".repeat(64))
}
fn recorded_at() -> DateTime<Utc> {
DateTime::from_timestamp(1_700_000_000, 123_000_000).unwrap_or_default()
}
fn envelope(seq: u64) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: recorded_at(),
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
}
}
fn payload(label: &str) -> Result<Payload, crate::PayloadError> {
Payload::from_json(&json!({ "label": label }))
}
fn schedule_config(label: &str) -> Result<ScheduleConfig, crate::PayloadError> {
Ok(ScheduleConfig {
trigger: TriggerSpec::Cron {
expression: String::from("0 0 * * *"),
},
overlap_policy: OverlapPolicy::Skip,
catch_up_policy: CatchUpPolicy::One,
workflow_type: String::from("checkout"),
input: payload(label)?,
search_attributes: HashMap::from([(
String::from("aion.namespace"),
crate::SearchAttributeValue::String(String::from("tenant-a")),
)]),
})
}
fn workflow_error(message: &str) -> WorkflowError {
WorkflowError {
message: String::from(message),
details: None,
}
}
fn activity_error(kind: ActivityErrorKind, message: &str) -> ActivityError {
ActivityError {
kind,
message: String::from(message),
details: None,
}
}
fn round_trip(event: &Event) -> Result<(), serde_json::Error> {
let json = serde_json::to_string(event)?;
let decoded = serde_json::from_str::<Event>(&json)?;
assert_eq!(*event, decoded);
Ok(())
}
#[test]
fn activity_scheduled_records_and_reads_back_its_task_queue()
-> Result<(), Box<dyn std::error::Error>> {
let event = Event::ActivityScheduled {
envelope: envelope(6),
activity_id: ActivityId::from_sequence_position(6),
activity_type: String::from("charge-card"),
input: payload("activity-input")?,
task_queue: String::from("claude"),
node: None,
};
let json = serde_json::to_string(&event)?;
let decoded = serde_json::from_str::<Event>(&json)?;
match decoded {
Event::ActivityScheduled { task_queue, .. } => {
assert_eq!(
task_queue, "claude",
"the recorded task queue must survive the round-trip"
);
}
other => return Err(format!("expected ActivityScheduled, got {other:?}").into()),
}
Ok(())
}
#[test]
fn activity_scheduled_decodes_old_history_without_task_queue_as_default()
-> Result<(), Box<dyn std::error::Error>> {
let current = Event::ActivityScheduled {
envelope: envelope(6),
activity_id: ActivityId::from_sequence_position(6),
activity_type: String::from("charge-card"),
input: payload("activity-input")?,
task_queue: String::from("ignored-when-stripped"),
node: Some(String::from("ignored-when-stripped")),
};
let mut value = serde_json::to_value(¤t)?;
let data = value
.get_mut("data")
.and_then(serde_json::Value::as_object_mut)
.ok_or("ActivityScheduled must serialize to a tagged object with a `data` map")?;
assert!(
data.remove("task_queue").is_some(),
"the current wire form must contain task_queue before we strip it"
);
let old_wire = serde_json::to_string(&value)?;
for _ in 0..4 {
let decoded = serde_json::from_str::<Event>(&old_wire)?;
match &decoded {
Event::ActivityScheduled { task_queue, .. } => {
assert_eq!(
task_queue, DEFAULT_TASK_QUEUE,
"a missing task_queue must default to the named default queue"
);
assert_eq!(task_queue, "default");
}
other => return Err(format!("expected ActivityScheduled, got {other:?}").into()),
}
}
Ok(())
}
#[test]
fn activity_scheduled_records_and_reads_back_its_node() -> Result<(), Box<dyn std::error::Error>>
{
let event = Event::ActivityScheduled {
envelope: envelope(6),
activity_id: ActivityId::from_sequence_position(6),
activity_type: String::from("charge-card"),
input: payload("activity-input")?,
task_queue: String::from("claude"),
node: Some(String::from("box-7")),
};
let json = serde_json::to_string(&event)?;
let decoded = serde_json::from_str::<Event>(&json)?;
match decoded {
Event::ActivityScheduled { node, .. } => {
assert_eq!(
node.as_deref(),
Some("box-7"),
"the recorded node affinity must survive the round-trip"
);
}
other => return Err(format!("expected ActivityScheduled, got {other:?}").into()),
}
Ok(())
}
#[test]
fn activity_scheduled_decodes_old_history_without_node_as_none()
-> Result<(), Box<dyn std::error::Error>> {
let current = Event::ActivityScheduled {
envelope: envelope(6),
activity_id: ActivityId::from_sequence_position(6),
activity_type: String::from("charge-card"),
input: payload("activity-input")?,
task_queue: String::from("default"),
node: Some(String::from("ignored-when-stripped")),
};
let mut value = serde_json::to_value(¤t)?;
let data = value
.get_mut("data")
.and_then(serde_json::Value::as_object_mut)
.ok_or("ActivityScheduled must serialize to a tagged object with a `data` map")?;
assert!(
data.remove("node").is_some(),
"the current wire form must contain node before we strip it"
);
let old_wire = serde_json::to_string(&value)?;
for _ in 0..4 {
let decoded = serde_json::from_str::<Event>(&old_wire)?;
match &decoded {
Event::ActivityScheduled { node, .. } => {
assert_eq!(
*node, None,
"a missing node must default to None (no affinity)"
);
}
other => return Err(format!("expected ActivityScheduled, got {other:?}").into()),
}
}
Ok(())
}
#[test]
fn activity_lifecycle_records_and_reads_back_its_attempt()
-> Result<(), Box<dyn std::error::Error>> {
let started = Event::ActivityStarted {
envelope: envelope(7),
activity_id: ActivityId::from_sequence_position(6),
attempt: 3,
};
let completed = Event::ActivityCompleted {
envelope: envelope(8),
activity_id: ActivityId::from_sequence_position(6),
result: payload("activity-result")?,
attempt: 3,
};
let cancelled = Event::ActivityCancelled {
envelope: envelope(9),
activity_id: ActivityId::from_sequence_position(6),
attempt: 3,
};
for event in [&started, &completed, &cancelled] {
round_trip(event)?;
}
match serde_json::from_str::<Event>(&serde_json::to_string(&started)?)? {
Event::ActivityStarted { attempt, .. } => assert_eq!(attempt, 3),
other => return Err(format!("expected ActivityStarted, got {other:?}").into()),
}
match serde_json::from_str::<Event>(&serde_json::to_string(&completed)?)? {
Event::ActivityCompleted { attempt, .. } => assert_eq!(attempt, 3),
other => return Err(format!("expected ActivityCompleted, got {other:?}").into()),
}
match serde_json::from_str::<Event>(&serde_json::to_string(&cancelled)?)? {
Event::ActivityCancelled { attempt, .. } => assert_eq!(attempt, 3),
other => return Err(format!("expected ActivityCancelled, got {other:?}").into()),
}
Ok(())
}
#[test]
fn activity_lifecycle_decodes_old_history_without_attempt_as_legacy_sentinel()
-> Result<(), Box<dyn std::error::Error>> {
let started = Event::ActivityStarted {
envelope: envelope(7),
activity_id: ActivityId::from_sequence_position(6),
attempt: 5,
};
let completed = Event::ActivityCompleted {
envelope: envelope(8),
activity_id: ActivityId::from_sequence_position(6),
result: payload("activity-result")?,
attempt: 5,
};
let cancelled = Event::ActivityCancelled {
envelope: envelope(9),
activity_id: ActivityId::from_sequence_position(6),
attempt: 5,
};
for current in [&started, &completed, &cancelled] {
let mut value = serde_json::to_value(current)?;
let data = value
.get_mut("data")
.and_then(serde_json::Value::as_object_mut)
.ok_or("activity lifecycle event must serialize to a tagged object with `data`")?;
assert!(
data.remove("attempt").is_some(),
"the current wire form must contain attempt before we strip it"
);
let old_wire = serde_json::to_string(&value)?;
for _ in 0..4 {
let decoded = serde_json::from_str::<Event>(&old_wire)?;
let attempt = match &decoded {
Event::ActivityStarted { attempt, .. }
| Event::ActivityCompleted { attempt, .. }
| Event::ActivityCancelled { attempt, .. } => *attempt,
other => {
return Err(
format!("expected an activity lifecycle event, got {other:?}").into(),
);
}
};
assert_eq!(
attempt, LEGACY_ACTIVITY_ATTEMPT,
"a missing attempt must default to the legacy sentinel (0)"
);
assert_eq!(attempt, 0);
}
}
Ok(())
}
#[test]
fn timer_cancelled_decodes_old_history_without_cause_as_workflow_intent()
-> Result<(), Box<dyn std::error::Error>> {
let cancelled = Event::TimerCancelled {
envelope: envelope(7),
timer_id: TimerId::named("deadline")?,
cause: TimerCancelCause::CancelTeardown,
};
let mut value = serde_json::to_value(&cancelled)?;
let data = value
.get_mut("data")
.and_then(serde_json::Value::as_object_mut)
.ok_or("TimerCancelled must serialize to a tagged object with `data`")?;
assert!(
data.remove("cause").is_some(),
"the current wire form must contain cause before we strip it"
);
let old_wire = serde_json::to_string(&value)?;
for _ in 0..4 {
let decoded = serde_json::from_str::<Event>(&old_wire)?;
match &decoded {
Event::TimerCancelled { cause, .. } => assert_eq!(
*cause,
TimerCancelCause::WorkflowIntent,
"a missing cause must default to WorkflowIntent (never resurrected)"
),
other => {
return Err(format!("expected TimerCancelled, got {other:?}").into());
}
}
}
Ok(())
}
#[test]
fn pause_resume_events_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
let events = vec![
Event::WorkflowPaused {
envelope: envelope(2),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
reason: Some(String::from("operator hold")),
operator: Some(String::from("tom")),
},
Event::WorkflowPaused {
envelope: envelope(3),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
reason: None,
operator: None,
},
Event::WorkflowResumed {
envelope: envelope(4),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
operator: Some(String::from("tom")),
},
];
for event in &events {
round_trip(event)?;
}
Ok(())
}
#[test]
fn old_history_without_pause_resume_decodes_unchanged() -> Result<(), Box<dyn std::error::Error>>
{
let started = Event::WorkflowStarted {
envelope: envelope(1),
workflow_type: String::from("checkout"),
input: payload("input")?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
package_version: package_version(),
};
let completed = Event::WorkflowCompleted {
envelope: envelope(2),
result: payload("result")?,
};
let history = vec![started, completed];
let json = serde_json::to_string(&history)?;
let decoded = serde_json::from_str::<Vec<Event>>(&json)?;
assert_eq!(history, decoded);
Ok(())
}
#[test]
fn start_time_task_queue_projects_from_recorded_attribute()
-> Result<(), Box<dyn std::error::Error>> {
use super::{START_TIME_TASK_QUEUE_ATTRIBUTE, start_time_task_queue};
use crate::SearchAttributeValue;
let events = vec![
Event::WorkflowStarted {
envelope: envelope(1),
workflow_type: String::from("checkout"),
input: payload("input")?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
package_version: package_version(),
},
Event::SearchAttributesUpdated {
envelope: envelope(2),
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
attributes: HashMap::from([(
START_TIME_TASK_QUEUE_ATTRIBUTE.to_owned(),
SearchAttributeValue::String(String::from("gpu")),
)]),
},
];
assert_eq!(start_time_task_queue(&events).as_deref(), Some("gpu"));
Ok(())
}
#[test]
fn start_time_task_queue_is_none_without_the_attribute()
-> Result<(), Box<dyn std::error::Error>> {
use super::start_time_task_queue;
let events = vec![Event::WorkflowStarted {
envelope: envelope(1),
workflow_type: String::from("checkout"),
input: payload("input")?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
package_version: package_version(),
}];
assert_eq!(start_time_task_queue(&events), None);
Ok(())
}
#[test]
fn event_accessors_return_envelope_fields() -> Result<(), Box<dyn std::error::Error>> {
let workflow_id = WorkflowId::new_v4();
let recorded_at = recorded_at();
let envelope = EventEnvelope {
seq: 17,
recorded_at,
workflow_id: workflow_id.clone(),
};
let event = Event::WorkflowStarted {
envelope,
workflow_type: String::from("checkout"),
input: payload("input")?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
package_version: package_version(),
};
assert_eq!(event.seq(), 17);
assert_eq!(event.recorded_at(), &recorded_at);
assert_eq!(event.workflow_id(), &workflow_id);
Ok(())
}
#[test]
fn events_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
let fire_at = DateTime::from_timestamp(1_700_000_100, 0).unwrap_or_default();
let events = vec![
Event::WorkflowStarted {
envelope: envelope(1),
workflow_type: String::from("checkout"),
input: payload("workflow-input")?,
run_id: RunId::new(uuid::Uuid::from_u128(1)),
parent_run_id: None,
package_version: package_version(),
},
Event::WorkflowCompleted {
envelope: envelope(2),
result: payload("workflow-result")?,
},
Event::WorkflowFailed {
envelope: envelope(3),
error: workflow_error("workflow failed"),
},
Event::WorkflowCancelled {
envelope: envelope(4),
reason: String::from("caller requested cancellation"),
},
Event::WorkflowTimedOut {
envelope: envelope(5),
timeout: String::from("execution"),
},
Event::ActivityScheduled {
envelope: envelope(6),
activity_id: ActivityId::from_sequence_position(6),
activity_type: String::from("charge-card"),
input: payload("activity-input")?,
task_queue: String::from("claude"),
node: Some(String::from("box-7")),
},
Event::ActivityStarted {
envelope: envelope(7),
activity_id: ActivityId::from_sequence_position(6),
attempt: 1,
},
Event::ActivityCompleted {
envelope: envelope(8),
activity_id: ActivityId::from_sequence_position(6),
result: payload("activity-result")?,
attempt: 1,
},
Event::ActivityFailed {
envelope: envelope(9),
activity_id: ActivityId::from_sequence_position(6),
error: activity_error(ActivityErrorKind::Retryable, "temporary outage"),
attempt: 1,
},
Event::ActivityCancelled {
envelope: envelope(10),
activity_id: ActivityId::from_sequence_position(6),
attempt: 1,
},
Event::TimerStarted {
envelope: envelope(11),
timer_id: TimerId::anonymous(11),
fire_at,
},
Event::TimerFired {
envelope: envelope(12),
timer_id: TimerId::anonymous(11),
},
Event::TimerCancelled {
envelope: envelope(13),
timer_id: TimerId::named("reminder")?,
cause: TimerCancelCause::WorkflowIntent,
},
Event::SignalReceived {
envelope: envelope(14),
name: String::from("approve"),
payload: payload("signal")?,
},
Event::SignalSent {
envelope: envelope(15),
target_workflow_id: WorkflowId::new(uuid::Uuid::from_u128(5)),
name: String::from("approve"),
payload: payload("signal-sent")?,
},
];
for event in events {
round_trip(&event)?;
}
Ok(())
}
#[test]
fn child_events_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
let child_workflow_id = WorkflowId::new(uuid::Uuid::from_u128(1));
let events = vec![
Event::ChildWorkflowStarted {
envelope: envelope(16),
child_workflow_id: child_workflow_id.clone(),
workflow_type: String::from("fulfillment"),
input: payload("child-input")?,
package_version: package_version(),
},
Event::ChildWorkflowCompleted {
envelope: envelope(16),
child_workflow_id: child_workflow_id.clone(),
result: payload("child-result")?,
},
Event::ChildWorkflowFailed {
envelope: envelope(17),
child_workflow_id: child_workflow_id.clone(),
error: workflow_error("child failed"),
},
Event::ChildWorkflowCancelled {
envelope: envelope(18),
child_workflow_id,
},
];
for event in events {
round_trip(&event)?;
}
Ok(())
}
#[test]
fn extended_events_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
let schedule_id = ScheduleId::new(uuid::Uuid::from_u128(2));
let triggered_workflow_id = WorkflowId::new(uuid::Uuid::from_u128(3));
let triggered_run_id = RunId::new(uuid::Uuid::from_u128(4));
let events = vec![
Event::WorkflowContinuedAsNew {
envelope: envelope(19),
input: payload("continued-input")?,
workflow_type: Some(String::from("checkout-v2")),
parent_run_id: RunId::new(uuid::Uuid::from_u128(2)),
},
Event::SearchAttributesUpdated {
envelope: envelope(20),
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
attributes: HashMap::from([(
String::from("customer_id"),
SearchAttributeValue::String(String::from("cust-123")),
)]),
},
Event::ScheduleCreated {
envelope: envelope(20),
schedule_id: schedule_id.clone(),
config: schedule_config("schedule-created")?,
},
Event::ScheduleUpdated {
envelope: envelope(21),
schedule_id: schedule_id.clone(),
config: schedule_config("schedule-updated")?,
},
Event::SchedulePaused {
envelope: envelope(22),
schedule_id: schedule_id.clone(),
},
Event::ScheduleResumed {
envelope: envelope(23),
schedule_id: schedule_id.clone(),
},
Event::ScheduleDeleted {
envelope: envelope(24),
schedule_id: schedule_id.clone(),
},
Event::ScheduleTriggered {
envelope: envelope(25),
schedule_id,
workflow_id: triggered_workflow_id,
run_id: triggered_run_id,
},
];
for event in events {
round_trip(&event)?;
}
Ok(())
}
}