Skip to main content

aion_core/
status.rs

1//! Workflow status projection from authoritative event history.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Event, RunId};
6
7/// Projected lifecycle status for a workflow execution.
8///
9/// Status must be obtained only by projecting from event history with
10/// [`status_from_events`], never assigned directly or stored as an independent
11/// mutable field. Event history remains authoritative for every workflow state.
12#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Copy, Debug, PartialEq, Eq)]
13pub enum WorkflowStatus {
14    /// The workflow has not recorded a terminal lifecycle event.
15    Running,
16    /// The workflow recorded a [`Event::WorkflowCompleted`] terminal event.
17    Completed,
18    /// The workflow recorded a [`Event::WorkflowFailed`] terminal event.
19    Failed,
20    /// The workflow recorded a [`Event::WorkflowCancelled`] terminal event.
21    Cancelled,
22    /// The workflow recorded a [`Event::WorkflowTimedOut`] terminal event.
23    TimedOut,
24    /// The workflow recorded a [`Event::WorkflowContinuedAsNew`] terminal event.
25    ContinuedAsNew,
26    /// The workflow recorded a [`Event::WorkflowPaused`] marker with no later
27    /// [`Event::WorkflowResumed`]. NON-terminal: the run has recorded no terminal
28    /// event and can still complete/fail/cancel or be resumed.
29    Paused,
30}
31
32impl WorkflowStatus {
33    /// Returns whether this status represents a terminal workflow execution state.
34    #[must_use]
35    pub const fn is_terminal(self) -> bool {
36        match self {
37            // Paused is non-terminal: complete/fail/cancel stay reachable, and a
38            // paused run is excluded from the active set without being terminal.
39            Self::Running | Self::Paused => false,
40            Self::Completed
41            | Self::Failed
42            | Self::Cancelled
43            | Self::TimedOut
44            | Self::ContinuedAsNew => true,
45        }
46    }
47}
48
49/// Projects workflow status from an event history.
50///
51/// The last terminal workflow lifecycle event determines the projected status.
52/// Histories without a terminal workflow event are considered running.
53/// When a history contains multiple runs for continue-as-new, a later
54/// [`Event::WorkflowStarted`] begins the current run and supersedes earlier
55/// terminal events from the previous run.
56#[must_use]
57pub fn status_from_events(events: &[Event]) -> WorkflowStatus {
58    events
59        .iter()
60        .rev()
61        .find_map(|event| match event {
62            // A run start and a reopen both put the run in Running. A reopen
63            // supersedes the run's prior terminal event under this same
64            // last-lifecycle-event-wins scan.
65            // A run start, a reopen, and a resume all put the run in Running. A
66            // resume supersedes the run's prior WorkflowPaused under this same
67            // last-lifecycle-event-wins scan.
68            Event::WorkflowStarted { .. }
69            | Event::WorkflowReopened { .. }
70            | Event::WorkflowResumed { .. } => Some(WorkflowStatus::Running),
71            Event::WorkflowCompleted { .. } => Some(WorkflowStatus::Completed),
72            Event::WorkflowFailed { .. } => Some(WorkflowStatus::Failed),
73            Event::WorkflowCancelled { .. } => Some(WorkflowStatus::Cancelled),
74            Event::WorkflowTimedOut { .. } => Some(WorkflowStatus::TimedOut),
75            Event::WorkflowContinuedAsNew { .. } => Some(WorkflowStatus::ContinuedAsNew),
76            // Paused is the one non-terminal lifecycle event that projects a
77            // distinct status; a later WorkflowResumed supersedes it above.
78            Event::WorkflowPaused { .. } => Some(WorkflowStatus::Paused),
79            Event::SearchAttributesUpdated { .. }
80            | Event::ActivityScheduled { .. }
81            | Event::ActivityStarted { .. }
82            | Event::ActivityAdoptionOffered { .. }
83            | Event::ActivityCompleted { .. }
84            | Event::ActivityFailed { .. }
85            // The advisory warning says a SIDE CHANNEL failed; it says
86            // nothing about the run's outcome, so it projects no status.
87            | Event::ActivityAdvisoryExhausted { .. }
88            // A fallback hop is an open-trail routing fact, not a run outcome.
89            | Event::ActivityFallbackRouted { .. }
90            | Event::ActivityCancelled { .. }
91            | Event::TimerStarted { .. }
92            | Event::TimerFired { .. }
93            | Event::TimerCancelled { .. }
94            | Event::WithTimeoutCompleted { .. }
95            | Event::SignalReceived { .. }
96            | Event::SignalSent { .. }
97            | Event::ChildWorkflowStarted { .. }
98            | Event::ChildWorkflowCompleted { .. }
99            | Event::ChildWorkflowFailed { .. }
100            | Event::ChildWorkflowCancelled { .. }
101            | Event::ScheduleCreated { .. }
102            | Event::ScheduleUpdated { .. }
103            | Event::SchedulePaused { .. }
104            | Event::ScheduleResumed { .. }
105            | Event::ScheduleDeleted { .. }
106            | Event::ScheduleTriggered { .. }
107            // Workloop bookkeeping is deliberately status-invisible: a cadence
108            // fire, an iteration close, a hatch, and an unconfirmed-invariant
109            // alarm say nothing about the run's outcome. LoopRetired's terminal
110            // is the WorkflowCompleted recorded in the same append.
111            | Event::CadenceFired { .. }
112            | Event::IterationClosed { .. }
113            | Event::LoopRetired { .. }
114            | Event::WorkflowHatched { .. }
115            | Event::InvariantUnconfirmed { .. } => None,
116        })
117        .unwrap_or(WorkflowStatus::Running)
118}
119
120/// Returns the terminal lifecycle event of the run's current lease, or `None`
121/// when the run is not currently terminal — either it never recorded a terminal
122/// event, or a later [`Event::WorkflowReopened`] reopened it.
123///
124/// This is the single reset-aware terminal predicate every site derives from:
125/// close-time is its `recorded_at`, the terminal outcome is a match on the
126/// returned event, and "is the run terminal now" is `is_some()`. Scanning back
127/// from the end it stops at the first reset point (a run start or a reopen), so
128/// terminality is scoped to "since the last reopen point" — a run holds exactly
129/// one terminal event per lease.
130#[must_use]
131pub fn current_lease_terminal(events: &[Event]) -> Option<&Event> {
132    events
133        .iter()
134        .rev()
135        .find_map(|event| match event {
136            Event::WorkflowCompleted { .. }
137            | Event::WorkflowFailed { .. }
138            | Event::WorkflowCancelled { .. }
139            | Event::WorkflowTimedOut { .. }
140            | Event::WorkflowContinuedAsNew { .. } => Some(Some(event)),
141            // Reset points: the current lease has no terminal before them.
142            Event::WorkflowStarted { .. } | Event::WorkflowReopened { .. } => Some(None),
143            // Pause/resume are neither a terminal nor a run-start reset — they
144            // fall through like SearchAttributesUpdated, so a paused (or resumed)
145            // run keeps whatever current-lease terminal state it otherwise has
146            // (None while live), leaving complete/fail/cancel reachable.
147            Event::WorkflowPaused { .. }
148            | Event::WorkflowResumed { .. }
149            | Event::SearchAttributesUpdated { .. }
150            | Event::ActivityScheduled { .. }
151            | Event::ActivityStarted { .. }
152            | Event::ActivityAdoptionOffered { .. }
153            | Event::ActivityCompleted { .. }
154            | Event::ActivityFailed { .. }
155            // The advisory warning says a SIDE CHANNEL failed; it says
156            // nothing about the run's outcome, so it projects no status.
157            | Event::ActivityAdvisoryExhausted { .. }
158            // A fallback hop is an open-trail routing fact, not a run outcome.
159            | Event::ActivityFallbackRouted { .. }
160            | Event::ActivityCancelled { .. }
161            | Event::TimerStarted { .. }
162            | Event::TimerFired { .. }
163            | Event::TimerCancelled { .. }
164            | Event::WithTimeoutCompleted { .. }
165            | Event::SignalReceived { .. }
166            | Event::SignalSent { .. }
167            | Event::ChildWorkflowStarted { .. }
168            | Event::ChildWorkflowCompleted { .. }
169            | Event::ChildWorkflowFailed { .. }
170            | Event::ChildWorkflowCancelled { .. }
171            | Event::ScheduleCreated { .. }
172            | Event::ScheduleUpdated { .. }
173            | Event::SchedulePaused { .. }
174            | Event::ScheduleResumed { .. }
175            | Event::ScheduleDeleted { .. }
176            | Event::ScheduleTriggered { .. }
177            // Workloop bookkeeping is neither a terminal nor a reset point —
178            // it falls through exactly like SearchAttributesUpdated. The
179            // terminal accompanying LoopRetired is its own WorkflowCompleted.
180            | Event::CadenceFired { .. }
181            | Event::IterationClosed { .. }
182            | Event::LoopRetired { .. }
183            | Event::WorkflowHatched { .. }
184            | Event::InvariantUnconfirmed { .. } => None,
185        })
186        .flatten()
187}
188
189/// Returns the slice of `events` belonging to the run identified by `run_id`:
190/// from that run's [`Event::WorkflowStarted`] up to (but excluding) the next
191/// run's start, or an empty slice when the run is absent.
192///
193/// Scoping [`current_lease_terminal`] to a `run_segment` answers "is this
194/// particular run currently terminal" without a separate bespoke scan per call
195/// site.
196#[must_use]
197pub fn run_segment<'a>(events: &'a [Event], run_id: &RunId) -> &'a [Event] {
198    let Some(start) = events.iter().position(
199        |event| matches!(event, Event::WorkflowStarted { run_id: id, .. } if id == run_id),
200    ) else {
201        return &[];
202    };
203    let end = events[start + 1..]
204        .iter()
205        .position(|event| matches!(event, Event::WorkflowStarted { .. }))
206        .map_or(events.len(), |offset| start + 1 + offset);
207    &events[start..end]
208}
209
210#[cfg(test)]
211mod tests {
212    use std::collections::HashMap;
213
214    use chrono::{DateTime, Utc};
215    use serde_json::json;
216
217    use super::{WorkflowStatus, current_lease_terminal, run_segment, status_from_events};
218    use crate::{
219        ActivityId, Event, EventEnvelope, Payload, RunId, ScheduleId, SearchAttributeValue,
220        WorkflowError, WorkflowId,
221    };
222
223    fn recorded_at(offset: i64) -> DateTime<Utc> {
224        DateTime::from_timestamp(1_700_000_000 + offset, 0).unwrap_or_default()
225    }
226
227    fn envelope(seq: u64) -> EventEnvelope {
228        EventEnvelope {
229            seq,
230            recorded_at: recorded_at(i64::try_from(seq).unwrap_or(0)),
231            workflow_id: WorkflowId::new(uuid::Uuid::nil()),
232        }
233    }
234
235    fn payload(label: &str) -> Result<Payload, crate::PayloadError> {
236        Payload::from_json(&json!({ "label": label }))
237    }
238
239    fn workflow_started(seq: u64) -> Result<Event, crate::PayloadError> {
240        Ok(Event::WorkflowStarted {
241            envelope: envelope(seq),
242            workflow_type: String::from("checkout"),
243            input: payload("input")?,
244            run_id: RunId::new(uuid::Uuid::from_u128(1)),
245            parent_run_id: None,
246            parent_workflow_id: None,
247            package_version: crate::PackageVersion::new("a".repeat(64)),
248        })
249    }
250
251    fn workflow_error(message: &str) -> WorkflowError {
252        WorkflowError {
253            message: String::from(message),
254            details: None,
255        }
256    }
257
258    #[test]
259    fn empty_history_projects_to_running() {
260        assert_eq!(status_from_events(&[]), WorkflowStatus::Running);
261    }
262
263    #[test]
264    fn replacement_start_projects_continue_as_new_chain_running() -> Result<(), crate::PayloadError>
265    {
266        let parent_run_id = RunId::new(uuid::Uuid::from_u128(7));
267        let events = vec![
268            workflow_started(1)?,
269            Event::WorkflowContinuedAsNew {
270                envelope: envelope(2),
271                input: payload("replacement")?,
272                workflow_type: None,
273                parent_run_id: parent_run_id.clone(),
274            },
275            Event::WorkflowStarted {
276                envelope: envelope(3),
277                workflow_type: String::from("checkout"),
278                input: payload("replacement")?,
279                run_id: RunId::new(uuid::Uuid::from_u128(1)),
280                parent_run_id: Some(parent_run_id),
281                parent_workflow_id: None,
282                package_version: crate::PackageVersion::new("a".repeat(64)),
283            },
284        ];
285
286        assert_eq!(status_from_events(&events), WorkflowStatus::Running);
287        Ok(())
288    }
289
290    #[test]
291    fn completed_terminal_event_projects_to_completed() -> Result<(), Box<dyn std::error::Error>> {
292        let events = vec![
293            workflow_started(1)?,
294            Event::WorkflowCompleted {
295                envelope: envelope(2),
296                result: payload("result")?,
297            },
298        ];
299
300        assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
301        Ok(())
302    }
303
304    #[test]
305    fn failed_terminal_event_projects_to_failed() -> Result<(), Box<dyn std::error::Error>> {
306        let events = vec![
307            workflow_started(1)?,
308            Event::WorkflowFailed {
309                envelope: envelope(2),
310                error: workflow_error("failed"),
311            },
312        ];
313
314        assert_eq!(status_from_events(&events), WorkflowStatus::Failed);
315        Ok(())
316    }
317
318    #[test]
319    fn cancelled_terminal_event_projects_to_cancelled() -> Result<(), Box<dyn std::error::Error>> {
320        let events = vec![
321            workflow_started(1)?,
322            Event::WorkflowCancelled {
323                envelope: envelope(2),
324                reason: String::from("caller requested cancellation"),
325            },
326        ];
327
328        assert_eq!(status_from_events(&events), WorkflowStatus::Cancelled);
329        Ok(())
330    }
331
332    #[test]
333    fn timed_out_terminal_event_projects_to_timed_out() -> Result<(), Box<dyn std::error::Error>> {
334        let events = vec![
335            workflow_started(1)?,
336            Event::WorkflowTimedOut {
337                envelope: envelope(2),
338                timeout: String::from("execution"),
339            },
340        ];
341
342        assert_eq!(status_from_events(&events), WorkflowStatus::TimedOut);
343        Ok(())
344    }
345
346    #[test]
347    fn continued_as_new_projects_status() -> Result<(), Box<dyn std::error::Error>> {
348        let events = vec![
349            workflow_started(1)?,
350            Event::WorkflowContinuedAsNew {
351                envelope: envelope(2),
352                input: payload("continued-input")?,
353                workflow_type: Some(String::from("checkout-v2")),
354                parent_run_id: RunId::new(uuid::Uuid::from_u128(2)),
355            },
356        ];
357
358        assert_eq!(status_from_events(&events), WorkflowStatus::ContinuedAsNew);
359        Ok(())
360    }
361
362    #[test]
363    fn workflow_status_terminality_classifies_running_and_terminal_statuses() {
364        assert!(!WorkflowStatus::Running.is_terminal());
365        assert!(WorkflowStatus::Completed.is_terminal());
366        assert!(WorkflowStatus::Failed.is_terminal());
367        assert!(WorkflowStatus::Cancelled.is_terminal());
368        assert!(WorkflowStatus::TimedOut.is_terminal());
369        assert!(WorkflowStatus::ContinuedAsNew.is_terminal());
370    }
371
372    #[test]
373    fn started_then_continued_as_new_projects_status() -> Result<(), Box<dyn std::error::Error>> {
374        let events = vec![
375            workflow_started(1)?,
376            Event::WorkflowContinuedAsNew {
377                envelope: envelope(2),
378                input: payload("continued-input")?,
379                workflow_type: None,
380                parent_run_id: RunId::new(uuid::Uuid::from_u128(3)),
381            },
382        ];
383
384        assert_eq!(status_from_events(&events), WorkflowStatus::ContinuedAsNew);
385        Ok(())
386    }
387
388    #[test]
389    fn non_terminal_history_projects_to_running() -> Result<(), Box<dyn std::error::Error>> {
390        let events = vec![
391            workflow_started(1)?,
392            Event::SearchAttributesUpdated {
393                envelope: envelope(2),
394                workflow_id: WorkflowId::new(uuid::Uuid::nil()),
395                attributes: HashMap::from([(
396                    String::from("customer_id"),
397                    SearchAttributeValue::String(String::from("customer-123")),
398                )]),
399            },
400            Event::ActivityScheduled {
401                envelope: envelope(3),
402                activity_id: ActivityId::from_sequence_position(3),
403                activity_type: String::from("charge-card"),
404                input: payload("activity-input")?,
405                task_queue: String::from("default"),
406                node: None,
407            },
408        ];
409
410        assert_eq!(status_from_events(&events), WorkflowStatus::Running);
411        Ok(())
412    }
413
414    #[test]
415    fn schedule_events_do_not_change_workflow_status() -> Result<(), Box<dyn std::error::Error>> {
416        let events = vec![
417            workflow_started(1)?,
418            Event::SchedulePaused {
419                envelope: envelope(2),
420                schedule_id: ScheduleId::new(uuid::Uuid::from_u128(2)),
421            },
422        ];
423
424        assert_eq!(status_from_events(&events), WorkflowStatus::Running);
425        Ok(())
426    }
427
428    #[test]
429    fn projection_is_deterministic() -> Result<(), Box<dyn std::error::Error>> {
430        let events = vec![
431            workflow_started(1)?,
432            Event::WorkflowCompleted {
433                envelope: envelope(2),
434                result: payload("result")?,
435            },
436        ];
437
438        let first = status_from_events(&events);
439        let second = status_from_events(&events);
440
441        assert_eq!(first, second);
442        Ok(())
443    }
444
445    #[test]
446    fn last_terminal_lifecycle_event_determines_status() -> Result<(), Box<dyn std::error::Error>> {
447        let events = vec![
448            workflow_started(1)?,
449            Event::WorkflowCompleted {
450                envelope: envelope(2),
451                result: payload("result")?,
452            },
453            Event::WorkflowTimedOut {
454                envelope: envelope(3),
455                timeout: String::from("execution"),
456            },
457        ];
458
459        assert_eq!(status_from_events(&events), WorkflowStatus::TimedOut);
460        Ok(())
461    }
462
463    fn run_id() -> RunId {
464        RunId::new(uuid::Uuid::from_u128(1))
465    }
466
467    fn workflow_reopened(seq: u64, reopened: Vec<ActivityId>) -> Event {
468        Event::WorkflowReopened {
469            envelope: envelope(seq),
470            run_id: run_id(),
471            reopened,
472        }
473    }
474
475    #[test]
476    fn reopen_after_failure_projects_running() -> Result<(), Box<dyn std::error::Error>> {
477        let events = vec![
478            workflow_started(1)?,
479            Event::WorkflowFailed {
480                envelope: envelope(2),
481                error: workflow_error("transient"),
482            },
483            workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
484        ];
485
486        assert_eq!(status_from_events(&events), WorkflowStatus::Running);
487        Ok(())
488    }
489
490    #[test]
491    fn reopen_then_new_terminal_projects_that_terminal() -> Result<(), Box<dyn std::error::Error>> {
492        let events = vec![
493            workflow_started(1)?,
494            Event::WorkflowFailed {
495                envelope: envelope(2),
496                error: workflow_error("transient"),
497            },
498            workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
499            Event::WorkflowCompleted {
500                envelope: envelope(4),
501                result: payload("result")?,
502            },
503        ];
504
505        assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
506        Ok(())
507    }
508
509    #[test]
510    fn current_lease_terminal_is_none_for_running_and_reopened()
511    -> Result<(), Box<dyn std::error::Error>> {
512        let running = vec![workflow_started(1)?];
513        assert!(current_lease_terminal(&running).is_none());
514
515        let failed = vec![
516            workflow_started(1)?,
517            Event::WorkflowFailed {
518                envelope: envelope(2),
519                error: workflow_error("boom"),
520            },
521        ];
522        assert!(matches!(
523            current_lease_terminal(&failed),
524            Some(Event::WorkflowFailed { .. })
525        ));
526
527        let mut reopened = failed.clone();
528        reopened.push(workflow_reopened(
529            3,
530            vec![ActivityId::from_sequence_position(2)],
531        ));
532        assert!(
533            current_lease_terminal(&reopened).is_none(),
534            "a reopened run has no current-lease terminal"
535        );
536        Ok(())
537    }
538
539    #[test]
540    fn current_lease_terminal_returns_terminal_after_reopen_and_retermination()
541    -> Result<(), Box<dyn std::error::Error>> {
542        let events = vec![
543            workflow_started(1)?,
544            Event::WorkflowFailed {
545                envelope: envelope(2),
546                error: workflow_error("boom"),
547            },
548            workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
549            Event::WorkflowCompleted {
550                envelope: envelope(4),
551                result: payload("result")?,
552            },
553        ];
554
555        assert!(matches!(
556            current_lease_terminal(&events),
557            Some(Event::WorkflowCompleted { .. })
558        ));
559        Ok(())
560    }
561
562    fn workflow_paused(seq: u64) -> Event {
563        Event::WorkflowPaused {
564            envelope: envelope(seq),
565            run_id: run_id(),
566            reason: None,
567            operator: None,
568        }
569    }
570
571    fn workflow_resumed(seq: u64) -> Event {
572        Event::WorkflowResumed {
573            envelope: envelope(seq),
574            run_id: run_id(),
575            operator: None,
576        }
577    }
578
579    #[test]
580    fn pause_projects_paused_and_is_non_terminal() -> Result<(), Box<dyn std::error::Error>> {
581        let events = vec![workflow_started(1)?, workflow_paused(2)];
582        assert_eq!(status_from_events(&events), WorkflowStatus::Paused);
583        assert!(!WorkflowStatus::Paused.is_terminal());
584        // Paused is non-terminal, so the run has no current-lease terminal and can
585        // still complete/fail/cancel.
586        assert!(current_lease_terminal(&events).is_none());
587        Ok(())
588    }
589
590    #[test]
591    fn resume_after_pause_projects_running() -> Result<(), Box<dyn std::error::Error>> {
592        let events = vec![
593            workflow_started(1)?,
594            workflow_paused(2),
595            workflow_resumed(3),
596        ];
597        assert_eq!(status_from_events(&events), WorkflowStatus::Running);
598        assert!(current_lease_terminal(&events).is_none());
599        Ok(())
600    }
601
602    #[test]
603    fn complete_after_resume_projects_completed() -> Result<(), Box<dyn std::error::Error>> {
604        let events = vec![
605            workflow_started(1)?,
606            workflow_paused(2),
607            workflow_resumed(3),
608            Event::WorkflowCompleted {
609                envelope: envelope(4),
610                result: payload("result")?,
611            },
612        ];
613        assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
614        assert!(matches!(
615            current_lease_terminal(&events),
616            Some(Event::WorkflowCompleted { .. })
617        ));
618        Ok(())
619    }
620
621    #[test]
622    fn complete_while_paused_still_projects_completed() -> Result<(), Box<dyn std::error::Error>> {
623        // The drain case: a paused run reaches a terminal via drained work.
624        // Last-lifecycle-event-wins projects the terminal, not Paused.
625        let events = vec![
626            workflow_started(1)?,
627            workflow_paused(2),
628            Event::WorkflowCompleted {
629                envelope: envelope(3),
630                result: payload("result")?,
631            },
632        ];
633        assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
634        Ok(())
635    }
636
637    #[test]
638    fn run_segment_scopes_to_the_named_run() -> Result<(), Box<dyn std::error::Error>> {
639        let first_run = RunId::new(uuid::Uuid::from_u128(10));
640        let second_run = RunId::new(uuid::Uuid::from_u128(20));
641        let events = vec![
642            Event::WorkflowStarted {
643                envelope: envelope(1),
644                workflow_type: String::from("checkout"),
645                input: payload("input")?,
646                run_id: first_run.clone(),
647                parent_run_id: None,
648                parent_workflow_id: None,
649                package_version: crate::PackageVersion::new("a".repeat(64)),
650            },
651            Event::WorkflowContinuedAsNew {
652                envelope: envelope(2),
653                input: payload("again")?,
654                workflow_type: None,
655                parent_run_id: first_run.clone(),
656            },
657            Event::WorkflowStarted {
658                envelope: envelope(3),
659                workflow_type: String::from("checkout"),
660                input: payload("input")?,
661                run_id: second_run.clone(),
662                parent_run_id: Some(first_run.clone()),
663                parent_workflow_id: None,
664                package_version: crate::PackageVersion::new("a".repeat(64)),
665            },
666            Event::WorkflowCompleted {
667                envelope: envelope(4),
668                result: payload("result")?,
669            },
670        ];
671
672        let first = run_segment(&events, &first_run);
673        assert_eq!(first.len(), 2, "first run spans its start through its CAN");
674        assert!(matches!(
675            current_lease_terminal(first),
676            Some(Event::WorkflowContinuedAsNew { .. })
677        ));
678
679        let second = run_segment(&events, &second_run);
680        assert_eq!(
681            second.len(),
682            2,
683            "second run spans its start through completion"
684        );
685        assert!(matches!(
686            current_lease_terminal(second),
687            Some(Event::WorkflowCompleted { .. })
688        ));
689
690        assert!(
691            run_segment(&events, &RunId::new(uuid::Uuid::from_u128(99))).is_empty(),
692            "absent run yields an empty segment"
693        );
694        Ok(())
695    }
696}