1use std::future::Future;
4use std::sync::Arc;
5
6use aion_core::{ActivityError, ActivityId, Payload, RunId, WorkflowId};
7use aion_store::EventStore;
8use chrono::{DateTime, Utc};
9use tokio::runtime::Handle;
10use tokio::sync::Mutex;
11
12use crate::EngineError;
13use crate::durability::{
14 Command, DurabilityError, FanOutCompletionResult, FanOutItem, FanOutOutcome, HistoryCursor,
15 Recorder, ResolveOutcome, Resolver,
16};
17use crate::registry::{Registry, WorkflowHandle};
18
19#[derive(thiserror::Error, Debug)]
21pub enum NifContextError {
22 #[error("unknown workflow process pid {pid}")]
24 UnknownProcess {
25 pid: u64,
27 },
28 #[error("workflow recorder lock is poisoned")]
30 RecorderPoisoned,
31 #[error("durability error: {0}")]
33 Durability(#[from] DurabilityError),
34 #[error("term encoding error: {reason}")]
36 TermEncoding {
37 reason: String,
39 },
40}
41
42impl NifContextError {
43 pub(crate) fn error_reason(&self) -> String {
49 match self {
50 Self::UnknownProcess { pid } => format!("unknown_process:{pid}"),
51 Self::RecorderPoisoned => "recorder_poisoned".to_owned(),
52 Self::Durability(error) => format!("durability:{error}"),
53 Self::TermEncoding { reason } => format!("term_encoding:{reason}"),
54 }
55 }
56}
57
58pub struct NifContext {
60 handle: WorkflowHandle,
61 recorder: Arc<Mutex<Recorder>>,
62 tokio_handle: Handle,
63 resolver: Resolver,
64 last_recorded_at: Option<DateTime<Utc>>,
65}
66
67impl NifContext {
68 pub fn new(
78 pid: u64,
79 registry: &Registry,
80 tokio_handle: Handle,
81 birth_wait: crate::runtime::SignalDeliveryConfig,
82 ) -> Result<Self, NifContextError> {
83 Self::new_with_history_store(pid, registry, tokio_handle, None, birth_wait)
84 }
85
86 pub fn new_with_history_store(
97 pid: u64,
98 registry: &Registry,
99 tokio_handle: Handle,
100 store: Option<Arc<dyn EventStore>>,
101 birth_wait: crate::runtime::SignalDeliveryConfig,
102 ) -> Result<Self, NifContextError> {
103 let handle = resolve_handle_with_birth_wait(registry, pid, birth_wait)?;
104 let recorder = handle.recorder();
105 let workflow_id = handle.workflow_id().clone();
106 let history = match store {
107 Some(store) => tokio_handle
108 .block_on(store.read_history(&workflow_id))
109 .map_err(DurabilityError::from)?,
110 None => tokio_handle.block_on(async {
111 let recorder = recorder.lock().await;
112 recorder.read_history().await
113 })?,
114 };
115 let history = crate::durability::current_run_segment(history, handle.run_id())?;
118 let last_recorded_at = history.last().map(|event| *event.recorded_at());
119 let cursor = HistoryCursor::new(history)?;
120 let resolver = Resolver::new(workflow_id, cursor);
121
122 Ok(Self {
123 handle,
124 recorder,
125 tokio_handle,
126 resolver,
127 last_recorded_at,
128 })
129 }
130
131 #[must_use]
133 pub fn workflow_id(&self) -> &WorkflowId {
134 self.handle.workflow_id()
135 }
136
137 #[must_use]
139 pub fn run_id(&self) -> &RunId {
140 self.handle.run_id()
141 }
142
143 #[must_use]
150 pub fn next_activity_ordinal(&self) -> u64 {
151 self.handle.allocate_activity_ordinals(1)
152 }
153
154 #[must_use]
156 pub fn allocate_activity_ordinals(&self, count: u64) -> u64 {
157 self.handle.allocate_activity_ordinals(count)
158 }
159
160 #[must_use]
165 pub fn next_timer_ordinal(&self) -> u64 {
166 self.handle.allocate_timer_ordinals(1)
167 }
168
169 #[must_use]
178 pub fn next_child_ordinal(&self) -> u64 {
179 self.handle.allocate_child_ordinals(1)
180 }
181
182 #[must_use]
184 pub fn signal_receives_consumed(&self, name: &str) -> u64 {
185 self.handle.signal_receives_consumed(name)
186 }
187
188 pub fn mark_signal_receive_consumed(&self, name: &str) {
190 self.handle.mark_signal_receive_consumed(name);
191 }
192
193 #[must_use]
195 pub fn signal_sends_completed(&self, name: &str) -> u64 {
196 self.handle.signal_sends_completed(name)
197 }
198
199 pub fn mark_signal_send_completed(&self, name: &str) {
201 self.handle.mark_signal_send_completed(name);
202 }
203
204 #[must_use]
206 pub fn workflow_handle(&self) -> WorkflowHandle {
207 self.handle.clone()
208 }
209
210 #[must_use]
212 pub const fn pid(&self) -> u64 {
213 self.handle.pid()
214 }
215
216 #[must_use]
218 pub const fn last_recorded_at(&self) -> Option<DateTime<Utc>> {
219 self.last_recorded_at
220 }
221
222 #[must_use]
224 pub fn next_deterministic_sequence(&self) -> u64 {
225 self.handle.next_deterministic_nif_sequence()
226 }
227
228 #[must_use]
230 pub fn recorder(&self) -> Arc<Mutex<Recorder>> {
231 Arc::clone(&self.recorder)
232 }
233
234 pub fn block_on_recorder<T, F>(&self, f: F) -> Result<T, NifContextError>
240 where
241 F: for<'a> FnOnce(
242 &'a mut Recorder,
243 ) -> std::pin::Pin<
244 Box<dyn Future<Output = Result<T, DurabilityError>> + Send + 'a>,
245 >,
246 {
247 self.tokio_handle
248 .block_on(async {
249 let mut recorder = self.recorder.lock().await;
250 f(&mut recorder).await
251 })
252 .map_err(Into::into)
253 }
254
255 pub fn record_activity_scheduled_started(
261 &self,
262 recorded_at: chrono::DateTime<chrono::Utc>,
263 activity_id: ActivityId,
264 activity_type: String,
265 input: Payload,
266 task_queue: String,
267 node: Option<String>,
268 ) -> Result<(), NifContextError> {
269 self.tokio_handle
270 .block_on(async {
271 let mut recorder = self.recorder.lock().await;
272 recorder
273 .record_activity_scheduled(
274 recorded_at,
275 activity_id.clone(),
276 activity_type,
277 input,
278 task_queue,
281 node,
284 )
285 .await?;
286 recorder
287 .record_activity_started(recorded_at, activity_id)
288 .await
289 })
290 .map_err(Into::into)
291 }
292
293 pub fn record_activity_completed(
299 &self,
300 recorded_at: chrono::DateTime<chrono::Utc>,
301 activity_id: ActivityId,
302 result: Payload,
303 ) -> Result<(), NifContextError> {
304 self.tokio_handle
305 .block_on(async {
306 let mut recorder = self.recorder.lock().await;
307 recorder
308 .record_activity_completed(recorded_at, activity_id, result)
309 .await
310 })
311 .map_err(Into::into)
312 }
313
314 pub fn record_activity_failed(
320 &self,
321 recorded_at: chrono::DateTime<chrono::Utc>,
322 activity_id: ActivityId,
323 error: ActivityError,
324 attempt: u32,
325 ) -> Result<(), NifContextError> {
326 self.tokio_handle
327 .block_on(async {
328 let mut recorder = self.recorder.lock().await;
329 recorder
330 .record_activity_failed(recorded_at, activity_id, error, attempt)
331 .await
332 })
333 .map_err(Into::into)
334 }
335
336 pub fn record_activity_cancelled(
342 &self,
343 recorded_at: chrono::DateTime<chrono::Utc>,
344 activity_id: ActivityId,
345 ) -> Result<(), NifContextError> {
346 self.tokio_handle
347 .block_on(async {
348 let mut recorder = self.recorder.lock().await;
349 recorder
350 .record_activity_cancelled(recorded_at, activity_id)
351 .await
352 })
353 .map_err(Into::into)
354 }
355
356 pub fn record_activity_cancelled_and_settle_outbox(
362 &self,
363 recorded_at: chrono::DateTime<chrono::Utc>,
364 ordinal: u64,
365 ) -> Result<(), NifContextError> {
366 self.tokio_handle
367 .block_on(async {
368 let mut recorder = self.recorder.lock().await;
369 recorder
370 .record_activity_cancelled_and_settle_outbox(recorded_at, ordinal)
371 .await
372 })
373 .map_err(Into::into)
374 }
375
376 pub fn record_fan_out_dispatch(
382 &self,
383 recorded_at: chrono::DateTime<chrono::Utc>,
384 items: &[FanOutItem],
385 ) -> Result<(), NifContextError> {
386 self.tokio_handle
387 .block_on(async {
388 let mut recorder = self.recorder.lock().await;
389 recorder.record_fan_out_dispatch(recorded_at, items).await
390 })
391 .map_err(Into::into)
392 }
393
394 pub fn rearm_outbox_pending(
401 &self,
402 recorded_at: chrono::DateTime<chrono::Utc>,
403 items: &[FanOutItem],
404 ) -> Result<(), NifContextError> {
405 self.tokio_handle
406 .block_on(async {
407 let recorder = self.recorder.lock().await;
408 recorder.rearm_outbox_pending(recorded_at, items).await
409 })
410 .map_err(Into::into)
411 }
412
413 pub fn record_fan_out_completion(
419 &self,
420 recorded_at: chrono::DateTime<chrono::Utc>,
421 ordinal: u64,
422 outcome: FanOutOutcome,
423 ) -> Result<FanOutCompletionResult, NifContextError> {
424 self.tokio_handle
425 .block_on(async {
426 let mut recorder = self.recorder.lock().await;
427 recorder
428 .record_fan_out_completion(recorded_at, ordinal, None, outcome)
429 .await
430 })
431 .map_err(Into::into)
432 }
433
434 #[must_use]
436 pub fn history(&self) -> &[aion_core::Event] {
437 self.resolver.history()
438 }
439
440 pub fn resolve_command(&mut self, command: Command) -> Result<ResolveOutcome, NifContextError> {
447 if let Some(key) = command.key() {
455 self.resolver.fast_forward_to(key);
456 } else if let Command::AwaitChild { child_workflow_id } = &command {
457 self.resolver
458 .fast_forward_to_child_terminal(child_workflow_id);
459 }
460 self.resolver.resolve(command).map_err(Into::into)
461 }
462}
463
464fn registry_error_to_context(error: &EngineError) -> NifContextError {
465 match error {
466 EngineError::RegistryPoisoned => NifContextError::RecorderPoisoned,
467 _ => NifContextError::TermEncoding {
468 reason: format!("registry lookup failed: {error}"),
469 },
470 }
471}
472
473fn resolve_handle_with_birth_wait(
494 registry: &Registry,
495 pid: u64,
496 birth_wait: crate::runtime::SignalDeliveryConfig,
497) -> Result<WorkflowHandle, NifContextError> {
498 let lookup = |registry: &Registry| -> Result<Option<WorkflowHandle>, NifContextError> {
499 Ok(registry
500 .list()
501 .map_err(|error| registry_error_to_context(&error))?
502 .into_iter()
503 .find(|handle| handle.pid() == pid))
504 };
505 if let Some(handle) = lookup(registry)? {
506 return Ok(handle);
507 }
508 let budget = birth_wait
509 .ready_timeout
510 .saturating_mul(birth_wait.max_enqueue_attempts.max(1));
511 let deadline = std::time::Instant::now() + budget;
512 let mut backoff = birth_wait.initial_backoff;
513 while std::time::Instant::now() < deadline {
514 std::thread::sleep(backoff);
515 let doubled = backoff.saturating_mul(2);
516 backoff = if doubled > birth_wait.max_backoff {
517 birth_wait.max_backoff
518 } else {
519 doubled
520 };
521 if let Some(handle) = lookup(registry)? {
522 return Ok(handle);
523 }
524 }
525 Err(NifContextError::UnknownProcess { pid })
526}
527
528#[cfg(test)]
529mod tests {
530 use std::sync::Arc;
531
532 use aion_core::{ActivityId, Event, EventEnvelope, Payload, WorkflowStatus};
533 use aion_package::ContentHash;
534 use aion_store::{EventStore, InMemoryStore, WriteToken};
535 use chrono::{TimeZone, Utc};
536 use serde_json::json;
537
538 use super::{NifContext, NifContextError};
539 use crate::durability::{Command, CorrelationKey, Recorder, Resolution, ResolveOutcome};
540 use crate::registry::{
541 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
542 };
543
544 type TestResult = Result<(), Box<dyn std::error::Error>>;
545
546 fn hash() -> ContentHash {
547 ContentHash::from_bytes([7; 32])
548 }
549
550 fn birth_wait() -> crate::runtime::SignalDeliveryConfig {
552 crate::runtime::SignalDeliveryConfig::new(
553 std::time::Duration::from_millis(200),
554 1,
555 std::time::Duration::from_millis(2),
556 std::time::Duration::from_millis(8),
557 )
558 }
559
560 fn payload(label: &str) -> Result<Payload, Box<dyn std::error::Error>> {
561 Ok(Payload::from_json(&json!({ "label": label }))?)
562 }
563
564 fn envelope(
565 workflow_id: &aion_core::WorkflowId,
566 seq: u64,
567 ) -> Result<EventEnvelope, Box<dyn std::error::Error>> {
568 let recorded_at = Utc
569 .timestamp_opt(i64::try_from(seq)?, 0)
570 .single()
571 .ok_or_else(|| "invalid timestamp".to_owned())?;
572 Ok(EventEnvelope {
573 seq,
574 recorded_at,
575 workflow_id: workflow_id.clone(),
576 })
577 }
578
579 fn started_event(
580 workflow_id: &aion_core::WorkflowId,
581 run_id: &aion_core::RunId,
582 ) -> Result<Event, Box<dyn std::error::Error>> {
583 Ok(Event::WorkflowStarted {
584 envelope: envelope(workflow_id, 1)?,
585 workflow_type: "checkout".to_owned(),
586 input: payload("input")?,
587 run_id: run_id.clone(),
588 parent_run_id: None,
589 package_version: aion_core::PackageVersion::new("a".repeat(64)),
590 })
591 }
592
593 fn handle(
594 pid: u64,
595 store: Arc<dyn EventStore>,
596 workflow_id: aion_core::WorkflowId,
597 run_id: aion_core::RunId,
598 ) -> WorkflowHandle {
599 let recorder = Recorder::resume_at(workflow_id.clone(), store, 1);
600 WorkflowHandle::new(WorkflowHandleParts {
601 workflow_id,
602 run_id,
603 pid,
604 workflow_type: "checkout".to_owned(),
605 namespace: String::from("default"),
606 loaded_version: hash(),
607 cached_status: WorkflowStatus::Running,
608 residency: HandleResidency::Resident,
609 recorder,
610 completion: CompletionNotifier::new(),
611 })
612 }
613
614 type TestContext = (Registry, Arc<dyn EventStore>, WorkflowHandle);
615
616 fn context_with_history(
617 runtime: &tokio::runtime::Runtime,
618 pid: u64,
619 workflow_id: aion_core::WorkflowId,
620 history: &[Event],
621 ) -> Result<TestContext, Box<dyn std::error::Error>> {
622 let registry = Registry::default();
623 let run_id = aion_core::RunId::new_v4();
624 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
625 let mut full_history = vec![started_event(&workflow_id, &run_id)?];
626 full_history.extend_from_slice(history);
627 runtime.block_on(store.append(WriteToken::recorder(), &workflow_id, &full_history, 0))?;
628 let recorder = Recorder::resume_at(
629 workflow_id.clone(),
630 Arc::clone(&store),
631 full_history.len() as u64,
632 );
633 let handle = WorkflowHandle::new(WorkflowHandleParts {
634 workflow_id: workflow_id.clone(),
635 run_id: run_id.clone(),
636 pid,
637 workflow_type: "checkout".to_owned(),
638 namespace: String::from("default"),
639 loaded_version: hash(),
640 cached_status: WorkflowStatus::Running,
641 residency: HandleResidency::Resident,
642 recorder,
643 completion: CompletionNotifier::new(),
644 });
645 registry.insert((workflow_id, run_id), handle.clone())?;
646 Ok((registry, store, handle))
647 }
648
649 #[test]
650 fn resolves_registered_pid_to_context() -> TestResult {
651 let runtime = tokio::runtime::Runtime::new()?;
652 let registry = Registry::default();
653 let workflow_id = aion_core::WorkflowId::new_v4();
654 let run_id = aion_core::RunId::new_v4();
655 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
656 runtime.block_on(store.append(
657 WriteToken::recorder(),
658 &workflow_id,
659 &[started_event(&workflow_id, &run_id)?],
660 0,
661 ))?;
662 let handle = handle(44, Arc::clone(&store), workflow_id.clone(), run_id.clone());
663 registry.insert((workflow_id.clone(), run_id), handle)?;
664
665 let context = NifContext::new(44, ®istry, runtime.handle().clone(), birth_wait())?;
666
667 assert_eq!(context.workflow_id(), &workflow_id);
668 assert_eq!(context.pid(), 44);
669 Ok(())
670 }
671
672 #[test]
673 fn unknown_pid_returns_unknown_process() -> TestResult {
674 let runtime = tokio::runtime::Runtime::new()?;
675 let registry = Registry::default();
676
677 let error = NifContext::new(77, ®istry, runtime.handle().clone(), birth_wait())
678 .err()
679 .ok_or("expected unknown process error")?;
680
681 assert!(matches!(error, NifContextError::UnknownProcess { pid: 77 }));
682 Ok(())
683 }
684
685 #[test]
693 fn birth_window_registration_resolves_instead_of_failing() -> TestResult {
694 let runtime = tokio::runtime::Runtime::new()?;
695 let registry = Arc::new(Registry::default());
696 let workflow_id = aion_core::WorkflowId::new_v4();
697 let run_id = aion_core::RunId::new_v4();
698 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
699 runtime.block_on(store.append(
700 WriteToken::recorder(),
701 &workflow_id,
702 &[started_event(&workflow_id, &run_id)?],
703 0,
704 ))?;
705 let handle = handle(91, Arc::clone(&store), workflow_id.clone(), run_id.clone());
706
707 let late_registry = Arc::clone(®istry);
710 let inserter = std::thread::spawn(move || {
711 std::thread::sleep(std::time::Duration::from_millis(30));
712 late_registry.insert((workflow_id.clone(), run_id), handle)
713 });
714
715 let context = NifContext::new(91, ®istry, runtime.handle().clone(), birth_wait())?;
716
717 assert_eq!(context.pid(), 91);
718 inserter
719 .join()
720 .map_err(|_| "registry insert thread panicked")??;
721 Ok(())
722 }
723
724 #[test]
725 fn block_on_recorder_reads_current_head_without_deadlock() -> TestResult {
726 let runtime = tokio::runtime::Runtime::new()?;
727 let registry = Registry::default();
728 let workflow_id = aion_core::WorkflowId::new_v4();
729 let run_id = aion_core::RunId::new_v4();
730 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
731 runtime.block_on(store.append(
732 WriteToken::recorder(),
733 &workflow_id,
734 &[started_event(&workflow_id, &run_id)?],
735 0,
736 ))?;
737 let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(&store), 5);
738 let handle = WorkflowHandle::new(WorkflowHandleParts {
739 workflow_id: workflow_id.clone(),
740 run_id: run_id.clone(),
741 pid: 55,
742 workflow_type: "checkout".to_owned(),
743 namespace: String::from("default"),
744 loaded_version: hash(),
745 cached_status: WorkflowStatus::Running,
746 residency: HandleResidency::Resident,
747 recorder,
748 completion: CompletionNotifier::new(),
749 });
750 registry.insert((workflow_id, run_id), handle)?;
751 let context = NifContext::new(55, ®istry, runtime.handle().clone(), birth_wait())?;
752
753 let head = context
754 .block_on_recorder(|recorder| Box::pin(async move { Ok(recorder.current_head()) }))?;
755
756 assert_eq!(head, 5);
757 Ok(())
758 }
759
760 #[test]
761 fn resolve_command_returns_recorded_activity_resolution() -> TestResult {
762 let runtime = tokio::runtime::Runtime::new()?;
763 let workflow_id = aion_core::WorkflowId::new_v4();
764 let result = payload("activity-result")?;
765 let history = vec![
766 Event::ActivityScheduled {
767 envelope: envelope(&workflow_id, 2)?,
768 activity_id: ActivityId::from_sequence_position(0),
769 activity_type: "activity".to_owned(),
770 input: payload("activity-input")?,
771 task_queue: String::from("default"),
772 node: None,
773 },
774 Event::ActivityCompleted {
775 envelope: envelope(&workflow_id, 3)?,
776 activity_id: ActivityId::from_sequence_position(0),
777 result: result.clone(),
778 },
779 ];
780 let (registry, store, handle) = context_with_history(&runtime, 66, workflow_id, &history)?;
781 let mut context = NifContext::new_with_history_store(
782 66,
783 ®istry,
784 runtime.handle().clone(),
785 Some(store),
786 birth_wait(),
787 )?;
788
789 assert_eq!(context.workflow_id(), handle.workflow_id());
790 assert_eq!(
791 context.resolve_command(Command::RunActivity {
792 key: CorrelationKey::Activity(0),
793 activity_type: "activity".to_owned(),
794 input: payload("activity-input")?,
795 })?,
796 ResolveOutcome::Recorded(Resolution::ActivityCompleted(result))
797 );
798 Ok(())
799 }
800
801 fn child_history(
802 workflow_id: &aion_core::WorkflowId,
803 child_workflow_id: &aion_core::WorkflowId,
804 include_terminal: bool,
805 ) -> Result<Vec<Event>, Box<dyn std::error::Error>> {
806 let timer_id = aion_core::TimerId::anonymous(0);
807 let mut history = vec![
808 Event::ActivityScheduled {
809 envelope: envelope(workflow_id, 2)?,
810 activity_id: ActivityId::from_sequence_position(0),
811 activity_type: "activity".to_owned(),
812 input: payload("activity-input")?,
813 task_queue: String::from("default"),
814 node: None,
815 },
816 Event::ActivityCompleted {
817 envelope: envelope(workflow_id, 3)?,
818 activity_id: ActivityId::from_sequence_position(0),
819 result: payload("activity-result")?,
820 },
821 Event::TimerStarted {
822 envelope: envelope(workflow_id, 4)?,
823 timer_id: timer_id.clone(),
824 fire_at: Utc
825 .timestamp_opt(99, 0)
826 .single()
827 .ok_or_else(|| "invalid timestamp".to_owned())?,
828 },
829 Event::TimerFired {
830 envelope: envelope(workflow_id, 5)?,
831 timer_id,
832 },
833 Event::ChildWorkflowStarted {
834 envelope: envelope(workflow_id, 6)?,
835 child_workflow_id: child_workflow_id.clone(),
836 workflow_type: "child".to_owned(),
837 input: payload("child-input")?,
838 package_version: aion_core::PackageVersion::new("a".repeat(64)),
839 },
840 ];
841 if include_terminal {
842 history.push(Event::ChildWorkflowCompleted {
843 envelope: envelope(workflow_id, 7)?,
844 child_workflow_id: child_workflow_id.clone(),
845 result: payload("child-result")?,
846 });
847 }
848 Ok(history)
849 }
850
851 #[test]
852 fn await_child_skips_consumed_commands_to_recorded_terminal() -> TestResult {
853 let runtime = tokio::runtime::Runtime::new()?;
854 let workflow_id = aion_core::WorkflowId::new_v4();
855 let child_workflow_id = aion_core::WorkflowId::new_v4();
856 let history = child_history(&workflow_id, &child_workflow_id, true)?;
861 let (registry, store, _handle) = context_with_history(&runtime, 88, workflow_id, &history)?;
862 let mut context = NifContext::new_with_history_store(
863 88,
864 ®istry,
865 runtime.handle().clone(),
866 Some(store),
867 birth_wait(),
868 )?;
869
870 assert_eq!(
871 context.resolve_command(Command::AwaitChild {
872 child_workflow_id: child_workflow_id.clone(),
873 })?,
874 ResolveOutcome::Recorded(Resolution::ChildCompleted(payload("child-result")?))
875 );
876 Ok(())
877 }
878
879 #[test]
880 fn await_child_without_recorded_terminal_resumes_live() -> TestResult {
881 let runtime = tokio::runtime::Runtime::new()?;
882 let workflow_id = aion_core::WorkflowId::new_v4();
883 let child_workflow_id = aion_core::WorkflowId::new_v4();
884 let history = child_history(&workflow_id, &child_workflow_id, false)?;
888 let (registry, store, _handle) = context_with_history(&runtime, 89, workflow_id, &history)?;
889 let mut context = NifContext::new_with_history_store(
890 89,
891 ®istry,
892 runtime.handle().clone(),
893 Some(store),
894 birth_wait(),
895 )?;
896
897 assert_eq!(
898 context.resolve_command(Command::AwaitChild { child_workflow_id })?,
899 ResolveOutcome::ResumeLive
900 );
901 Ok(())
902 }
903}