1use aion_proto::{
4 ProtoCancelRequest, ProtoCancelResponse, ProtoPauseRequest, ProtoPauseResponse,
5 ProtoQueryRequest, ProtoQueryResponse, ProtoReopenRequest, ProtoReopenResponse,
6 ProtoResumeRequest, ProtoResumeResponse, ProtoSignalRequest, ProtoSignalResponse,
7 ProtoStartWorkflowRequest, ProtoStartWorkflowResponse, WireError, proto_query_response,
8};
9use tracing::{Instrument, info_span};
10
11use super::error::{
12 cancel_terminal_error, log_server_error, map_start_error, map_workflow_operation_error,
13 signal_terminal_error,
14};
15use super::payload::{required_payload, required_workflow_id};
16use super::runs::{resolve_run_id, terminal_status};
17use crate::{
18 CallerIdentity, NamespaceGuard, NamespaceMinter, NamespaceOperation, ServerError,
19 WorkflowTarget,
20};
21
22pub async fn start(
33 guard: &NamespaceGuard,
34 caller: &CallerIdentity,
35 request: ProtoStartWorkflowRequest,
36) -> Result<ProtoStartWorkflowResponse, WireError> {
37 start_with_placement(guard, caller, request, None, None).await
38}
39
40pub async fn start_with_placement(
66 guard: &NamespaceGuard,
67 caller: &CallerIdentity,
68 request: ProtoStartWorkflowRequest,
69 placement: Option<aion_core::WorkflowId>,
70 minter: Option<&NamespaceMinter>,
71) -> Result<ProtoStartWorkflowResponse, WireError> {
72 let scoped = guard
73 .scope(caller, &NamespaceOperation::start(&request))
74 .await
75 .map_err(|error| error.to_wire_error())?;
76 let namespace = scoped.namespace().to_owned();
77 if let Some(minter) = minter {
84 minter
85 .mint_or_gate(
86 std::slice::from_ref(&namespace),
87 aion_store::NamespaceOrigin::StartMint,
88 )
89 .await
90 .map_err(|error| error.to_wire_error())?;
91 }
92 let input = required_payload(request.input.clone())?;
93 let task_queue = request
96 .task_queue
97 .as_deref()
98 .map(str::trim)
99 .filter(|queue| !queue.is_empty());
100 let span = info_span!(
101 "engine_operation",
102 operation = "start",
103 namespace = %namespace,
104 workflow_id = tracing::field::Empty,
105 workflow_type = %request.workflow_type,
106 );
107 let search_attributes = start_search_attributes(&namespace, task_queue);
108 let handle = async {
109 scoped
110 .engine()
111 .map_err(|error| log_server_error("start", Some(&namespace), None, &error))?
112 .start_workflow_with_id(
113 &request.workflow_type,
114 input,
115 search_attributes,
116 namespace.clone(),
117 placement,
118 None,
122 )
123 .await
124 .map_err(|error| map_start_error(error, &request.workflow_type))
125 }
126 .instrument(span.clone())
127 .await?;
128 span.record("workflow_id", tracing::field::display(handle.workflow_id()));
129
130 Ok(ProtoStartWorkflowResponse {
131 workflow_id: Some(handle.workflow_id().clone().into()),
132 run_id: Some(handle.run_id().clone().into()),
133 })
134}
135
136fn start_search_attributes(
144 namespace: &str,
145 task_queue: Option<&str>,
146) -> std::collections::HashMap<String, aion_core::SearchAttributeValue> {
147 let mut attributes = std::collections::HashMap::from([(
148 crate::namespace::NAMESPACE_ATTRIBUTE.to_owned(),
149 aion_core::SearchAttributeValue::String(namespace.to_owned()),
150 )]);
151 if let Some(task_queue) = task_queue {
152 attributes.insert(
153 crate::namespace::TASK_QUEUE_ATTRIBUTE.to_owned(),
154 aion_core::SearchAttributeValue::String(task_queue.to_owned()),
155 );
156 }
157 attributes
158}
159
160pub async fn signal(
167 guard: &NamespaceGuard,
168 caller: &CallerIdentity,
169 request: ProtoSignalRequest,
170) -> Result<ProtoSignalResponse, WireError> {
171 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
172 let target = WorkflowTarget::workflow(&workflow_id);
173 let scoped = guard
174 .scope(caller, &NamespaceOperation::signal(&request, target))
175 .await
176 .map_err(|error| error.to_wire_error())?;
177 let namespace = scoped.namespace().to_owned();
178 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
179 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
180 let payload = required_payload(request.payload.clone())?;
181 if let Some(status) = terminal_status(engine.as_ref(), &workflow_id).await? {
182 return Err(signal_terminal_error(&workflow_id, status));
183 }
184
185 let signal_name = request.signal_name.clone();
186 let span = info_span!(
187 "engine_operation",
188 operation = "signal",
189 namespace = %namespace,
190 workflow_id = %workflow_id,
191 signal_name = %signal_name,
192 );
193
194 async {
195 engine
196 .signal(&workflow_id, &run_id, signal_name, payload)
197 .await
198 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
199 }
200 .instrument(span)
201 .await?;
202
203 Ok(ProtoSignalResponse {})
204}
205
206pub async fn query(
213 guard: &NamespaceGuard,
214 caller: &CallerIdentity,
215 request: ProtoQueryRequest,
216) -> Result<ProtoQueryResponse, WireError> {
217 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
218 let target = WorkflowTarget::workflow(&workflow_id);
219 let scoped = guard
220 .scope(caller, &NamespaceOperation::query(&request, target))
221 .await
222 .map_err(|error| error.to_wire_error())?;
223 let namespace = scoped.namespace().to_owned();
224 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
225 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
226 let query_name = request.query_name.clone();
227 let span = info_span!(
228 "engine_operation",
229 operation = "query",
230 namespace = %namespace,
231 workflow_id = %workflow_id,
232 query_name = %query_name,
233 );
234
235 let outcome = async { engine.query(&workflow_id, &run_id, query_name).await }
236 .instrument(span)
237 .await;
238
239 match outcome {
240 Ok(result) => Ok(ProtoQueryResponse {
241 outcome: Some(proto_query_response::Outcome::Result(result.into())),
242 }),
243 Err(error @ aion::EngineError::Query(_)) => Ok(ProtoQueryResponse {
249 outcome: Some(proto_query_response::Outcome::Error(
250 ServerError::from(error).to_wire_error().into(),
251 )),
252 }),
253 Err(error) => Err(map_workflow_operation_error(error, &workflow_id)),
254 }
255}
256
257pub async fn cancel(
264 guard: &NamespaceGuard,
265 caller: &CallerIdentity,
266 request: ProtoCancelRequest,
267) -> Result<ProtoCancelResponse, WireError> {
268 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
269 let target = WorkflowTarget::workflow(&workflow_id);
270 let scoped = guard
271 .scope(caller, &NamespaceOperation::cancel(&request, target))
272 .await
273 .map_err(|error| error.to_wire_error())?;
274 let namespace = scoped.namespace().to_owned();
275 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
276 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
277 if let Some(status) = terminal_status(engine.as_ref(), &workflow_id).await? {
278 return Err(cancel_terminal_error(&workflow_id, status));
279 }
280
281 let span = info_span!(
282 "engine_operation",
283 operation = "cancel",
284 namespace = %namespace,
285 workflow_id = %workflow_id,
286 );
287
288 async {
289 engine
290 .cancel(&workflow_id, &run_id, request.reason)
291 .await
292 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
293 }
294 .instrument(span)
295 .await?;
296
297 Ok(ProtoCancelResponse {})
298}
299
300pub async fn reopen(
314 guard: &NamespaceGuard,
315 caller: &CallerIdentity,
316 request: ProtoReopenRequest,
317) -> Result<ProtoReopenResponse, WireError> {
318 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
319 let target = WorkflowTarget::workflow(&workflow_id);
320 let scoped = guard
321 .scope(caller, &NamespaceOperation::reopen(&request, target))
322 .await
323 .map_err(|error| error.to_wire_error())?;
324 let namespace = scoped.namespace().to_owned();
325 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
326 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
327
328 let span = info_span!(
329 "engine_operation",
330 operation = "reopen",
331 namespace = %namespace,
332 workflow_id = %workflow_id,
333 );
334
335 let handle = async {
336 engine
337 .reopen_workflow(&workflow_id, &run_id)
338 .await
339 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
340 }
341 .instrument(span)
342 .await?;
343
344 Ok(ProtoReopenResponse {
345 run_id: Some(handle.run_id().clone().into()),
346 status: aion_proto::ProtoWorkflowStatus::from(handle.cached_status()) as i32,
347 })
348}
349
350pub async fn pause(
363 guard: &NamespaceGuard,
364 caller: &CallerIdentity,
365 request: ProtoPauseRequest,
366) -> Result<ProtoPauseResponse, WireError> {
367 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
368 let target = WorkflowTarget::workflow(&workflow_id);
369 let scoped = guard
370 .scope(
371 caller,
372 &NamespaceOperation::pause_workflow(&request, target),
373 )
374 .await
375 .map_err(|error| error.to_wire_error())?;
376 let namespace = scoped.namespace().to_owned();
377 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
378 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
379 let reason = if request.reason.is_empty() {
380 None
381 } else {
382 Some(request.reason.clone())
383 };
384
385 let span = info_span!(
386 "engine_operation",
387 operation = "pause",
388 namespace = %namespace,
389 workflow_id = %workflow_id,
390 );
391
392 let handle = async {
393 engine
394 .pause_workflow(&workflow_id, &run_id, reason, None)
395 .await
396 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
397 }
398 .instrument(span)
399 .await?;
400
401 Ok(ProtoPauseResponse {
402 run_id: Some(handle.run_id().clone().into()),
403 status: aion_proto::ProtoWorkflowStatus::Paused as i32,
406 })
407}
408
409pub async fn resume(
421 guard: &NamespaceGuard,
422 caller: &CallerIdentity,
423 request: ProtoResumeRequest,
424) -> Result<ProtoResumeResponse, WireError> {
425 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
426 let target = WorkflowTarget::workflow(&workflow_id);
427 let scoped = guard
428 .scope(
429 caller,
430 &NamespaceOperation::resume_workflow(&request, target),
431 )
432 .await
433 .map_err(|error| error.to_wire_error())?;
434 let namespace = scoped.namespace().to_owned();
435 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
436 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
437
438 let span = info_span!(
439 "engine_operation",
440 operation = "resume",
441 namespace = %namespace,
442 workflow_id = %workflow_id,
443 );
444
445 let handle = async {
446 engine
447 .resume_paused_workflow(&workflow_id, &run_id, None)
448 .await
449 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
450 }
451 .instrument(span)
452 .await?;
453
454 Ok(ProtoResumeResponse {
455 run_id: Some(handle.run_id().clone().into()),
456 status: aion_proto::ProtoWorkflowStatus::Running as i32,
457 })
458}
459
460#[cfg(test)]
461mod tests {
462 use aion_proto::{WireError, WireErrorCode};
463
464 use super::super::test_support::{
465 NAMESPACE, append_completed, append_failed, append_started, append_timed_out,
466 assert_workflow_not_found, cancel_request, context, denied_guard, proto_payload,
467 query_request, reopen_request, run_id, signal_request, workflow_id,
468 };
469 use super::*;
470
471 #[tokio::test]
472 async fn start_handler_scopes_then_invokes_engine_start()
473 -> Result<(), Box<dyn std::error::Error>> {
474 let context = context().await?;
475 let request = ProtoStartWorkflowRequest {
476 namespace: NAMESPACE.to_owned(),
477 workflow_type: "missing-workflow".to_owned(),
478 input: Some(proto_payload()?),
479 routing_key: None,
480 task_queue: None,
481 };
482
483 let error = start(&context.guard, &context.caller, request).await;
484
485 let error = error
486 .err()
487 .ok_or_else(|| WireError::backend("expected error"))?;
488 assert_eq!(error.code, WireErrorCode::NotFound);
489 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
490 assert_eq!(
491 error.message,
492 "workflow type missing-workflow is not registered"
493 );
494 Ok(())
495 }
496
497 #[test]
498 fn start_records_namespace_only_when_no_task_queue_selected() {
499 use crate::namespace::{NAMESPACE_ATTRIBUTE, TASK_QUEUE_ATTRIBUTE};
500
501 let attributes = start_search_attributes("tenant-a", None);
502 assert_eq!(
503 attributes.get(NAMESPACE_ATTRIBUTE),
504 Some(&aion_core::SearchAttributeValue::String(
505 "tenant-a".to_owned()
506 ))
507 );
508 assert!(!attributes.contains_key(TASK_QUEUE_ATTRIBUTE));
511 }
512
513 #[test]
514 fn start_records_selected_task_queue_durably_like_namespace() {
515 use crate::namespace::{NAMESPACE_ATTRIBUTE, TASK_QUEUE_ATTRIBUTE};
516
517 let attributes = start_search_attributes("tenant-a", Some("gpu"));
518 assert_eq!(
519 attributes.get(NAMESPACE_ATTRIBUTE),
520 Some(&aion_core::SearchAttributeValue::String(
521 "tenant-a".to_owned()
522 ))
523 );
524 assert_eq!(
528 attributes.get(TASK_QUEUE_ATTRIBUTE),
529 Some(&aion_core::SearchAttributeValue::String("gpu".to_owned()))
530 );
531 }
532
533 #[tokio::test]
534 async fn signal_handler_scopes_then_invokes_engine_signal()
535 -> Result<(), Box<dyn std::error::Error>> {
536 let context = context().await?;
537 context.ownership.record(workflow_id(), NAMESPACE)?;
538
539 let error = signal(&context.guard, &context.caller, signal_request()?).await;
540
541 let error = error
542 .err()
543 .ok_or_else(|| WireError::backend("expected error"))?;
544 assert_eq!(error.code, WireErrorCode::NotFound);
545 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
546 assert_eq!(
547 error.message,
548 format!("workflow {} not found", workflow_id())
549 );
550 Ok(())
551 }
552
553 #[tokio::test]
554 async fn query_handler_scopes_then_invokes_engine_query()
555 -> Result<(), Box<dyn std::error::Error>> {
556 let context = context().await?;
557 context.ownership.record(workflow_id(), NAMESPACE)?;
558
559 let error = query(&context.guard, &context.caller, query_request()).await;
560
561 let error = error
562 .err()
563 .ok_or_else(|| WireError::backend("expected error"))?;
564 assert_eq!(error.code, WireErrorCode::NotFound);
565 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
566 assert_eq!(
567 error.message,
568 format!("workflow {} not found", workflow_id())
569 );
570 Ok(())
571 }
572
573 #[tokio::test]
574 async fn query_handler_returns_not_running_outcome_for_terminal_workflow()
575 -> Result<(), Box<dyn std::error::Error>> {
576 let context = context().await?;
577 context.ownership.record(workflow_id(), NAMESPACE)?;
578 append_completed(context.store.as_ref()).await?;
579 let mut request = query_request();
582 request.run_id = None;
583
584 let response = query(&context.guard, &context.caller, request).await?;
585
586 let Some(proto_query_response::Outcome::Error(error)) = response.outcome else {
589 return Err("expected a QueryResponse.error outcome".into());
590 };
591 let error = WireError::try_from(error)?;
592 assert_eq!(error.code, WireErrorCode::NotRunning);
593 assert_eq!(error.error_type.as_deref(), Some("QueryNotRunning"));
594 Ok(())
595 }
596
597 #[tokio::test]
598 async fn query_handler_keeps_non_resident_non_terminal_workflow_as_transport_not_found()
599 -> Result<(), Box<dyn std::error::Error>> {
600 let context = context().await?;
604 context.ownership.record(workflow_id(), NAMESPACE)?;
605 append_started(context.store.as_ref()).await?;
606 let mut request = query_request();
607 request.run_id = None;
608
609 let error = query(&context.guard, &context.caller, request).await;
610
611 let error = error
612 .err()
613 .ok_or_else(|| WireError::backend("expected error"))?;
614 assert_eq!(error.code, WireErrorCode::NotFound);
615 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
616 Ok(())
617 }
618
619 #[tokio::test]
620 async fn cancel_handler_scopes_then_invokes_engine_cancel()
621 -> Result<(), Box<dyn std::error::Error>> {
622 let context = context().await?;
623 context.ownership.record(workflow_id(), NAMESPACE)?;
624
625 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
626
627 let error = error
628 .err()
629 .ok_or_else(|| WireError::backend("expected error"))?;
630 assert_eq!(error.code, WireErrorCode::NotFound);
631 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
632 assert_eq!(
633 error.message,
634 format!("workflow {} not found", workflow_id())
635 );
636 Ok(())
637 }
638
639 #[tokio::test]
640 async fn reopen_handler_maps_missing_workflow_to_not_found()
641 -> Result<(), Box<dyn std::error::Error>> {
642 let context = context().await?;
643 context.ownership.record(workflow_id(), NAMESPACE)?;
644
645 let error = reopen(&context.guard, &context.caller, reopen_request()).await;
646
647 let error = error
648 .err()
649 .ok_or_else(|| WireError::backend("expected error"))?;
650 assert_eq!(error.code, WireErrorCode::NotFound);
651 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
652 Ok(())
653 }
654
655 #[tokio::test]
656 async fn reopen_handler_rejects_completed_workflow_as_invalid_state()
657 -> Result<(), Box<dyn std::error::Error>> {
658 let context = context().await?;
659 context.ownership.record(workflow_id(), NAMESPACE)?;
660 append_completed(context.store.as_ref()).await?;
661 let mut request = reopen_request();
662 request.run_id = None;
663
664 let error = reopen(&context.guard, &context.caller, request).await;
665
666 let error = error
667 .err()
668 .ok_or_else(|| WireError::backend("expected error"))?;
669 assert_eq!(error.code, WireErrorCode::InvalidState);
670 assert_eq!(error.error_type.as_deref(), Some("InvalidState"));
671 Ok(())
672 }
673
674 #[tokio::test]
675 async fn reopen_handler_rejects_timed_out_workflow_as_invalid_state()
676 -> Result<(), Box<dyn std::error::Error>> {
677 let context = context().await?;
678 context.ownership.record(workflow_id(), NAMESPACE)?;
679 append_timed_out(context.store.as_ref()).await?;
680 let mut request = reopen_request();
681 request.run_id = None;
682
683 let error = reopen(&context.guard, &context.caller, request).await;
684
685 let error = error
686 .err()
687 .ok_or_else(|| WireError::backend("expected error"))?;
688 assert_eq!(error.code, WireErrorCode::InvalidState);
690 assert_eq!(error.error_type.as_deref(), Some("InvalidState"));
691 Ok(())
692 }
693
694 #[tokio::test]
695 async fn reopen_handler_maps_omitted_run_missing_workflow_to_not_found()
696 -> Result<(), Box<dyn std::error::Error>> {
697 let context = context().await?;
698 context.ownership.record(workflow_id(), NAMESPACE)?;
699 let mut request = reopen_request();
700 request.run_id = None;
701
702 let error = reopen(&context.guard, &context.caller, request).await;
703
704 assert_workflow_not_found(error)?;
705 Ok(())
706 }
707
708 #[tokio::test]
711 async fn denied_reopen_is_namespace_denied_before_engine_check()
712 -> Result<(), Box<dyn std::error::Error>> {
713 let (guard, caller) = denied_guard();
714 let request = ProtoReopenRequest {
715 namespace: NAMESPACE.to_owned(),
716 workflow_id: Some(workflow_id().into()),
717 run_id: Some(run_id().into()),
718 };
719
720 let error = reopen(&guard, &caller, request).await;
721
722 assert_eq!(
723 error.err().map(|error| error.code),
724 Some(WireErrorCode::NamespaceDenied)
725 );
726 Ok(())
727 }
728
729 #[tokio::test]
730 async fn signal_handler_rejects_completed_workflow() -> Result<(), Box<dyn std::error::Error>> {
731 let context = context().await?;
732 context.ownership.record(workflow_id(), NAMESPACE)?;
733 append_completed(context.store.as_ref()).await?;
734
735 let error = signal(&context.guard, &context.caller, signal_request()?).await;
736
737 let error = error
738 .err()
739 .ok_or_else(|| WireError::backend("expected error"))?;
740 assert_eq!(error.code, WireErrorCode::NotRunning);
741 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
742 assert_eq!(
743 error.message,
744 format!(
745 "workflow {} has already reached terminal state Completed",
746 workflow_id()
747 )
748 );
749 Ok(())
750 }
751
752 #[tokio::test]
753 async fn signal_handler_rejects_failed_workflow() -> Result<(), Box<dyn std::error::Error>> {
754 let context = context().await?;
755 context.ownership.record(workflow_id(), NAMESPACE)?;
756 append_failed(context.store.as_ref()).await?;
757
758 let error = signal(&context.guard, &context.caller, signal_request()?).await;
759
760 let error = error
761 .err()
762 .ok_or_else(|| WireError::backend("expected error"))?;
763 assert_eq!(error.code, WireErrorCode::NotRunning);
764 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
765 assert_eq!(
766 error.message,
767 format!(
768 "workflow {} has already reached terminal state Failed",
769 workflow_id()
770 )
771 );
772 Ok(())
773 }
774
775 #[tokio::test]
776 async fn cancel_handler_rejects_completed_workflow() -> Result<(), Box<dyn std::error::Error>> {
777 let context = context().await?;
778 context.ownership.record(workflow_id(), NAMESPACE)?;
779 append_completed(context.store.as_ref()).await?;
780
781 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
782
783 let error = error
784 .err()
785 .ok_or_else(|| WireError::backend("expected error"))?;
786 assert_eq!(error.code, WireErrorCode::NotRunning);
787 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
788 assert_eq!(
789 error.message,
790 format!(
791 "workflow {} has already completed with status Completed",
792 workflow_id()
793 )
794 );
795 assert!(!error.message.contains("process 0 is not live"));
796 Ok(())
797 }
798
799 #[tokio::test]
800 async fn cancel_handler_rejects_failed_workflow() -> Result<(), Box<dyn std::error::Error>> {
801 let context = context().await?;
802 context.ownership.record(workflow_id(), NAMESPACE)?;
803 append_failed(context.store.as_ref()).await?;
804
805 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
806
807 let error = error
808 .err()
809 .ok_or_else(|| WireError::backend("expected error"))?;
810 assert_eq!(error.code, WireErrorCode::NotRunning);
811 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
812 assert_eq!(
813 error.message,
814 format!(
815 "workflow {} has already completed with status Failed",
816 workflow_id()
817 )
818 );
819 assert!(!error.message.contains("process 0 is not live"));
820 Ok(())
821 }
822
823 #[tokio::test]
824 async fn signal_handler_maps_omitted_run_missing_workflow_to_not_found()
825 -> Result<(), Box<dyn std::error::Error>> {
826 let context = context().await?;
827 context.ownership.record(workflow_id(), NAMESPACE)?;
828 let mut request = signal_request()?;
829 request.run_id = None;
830
831 let error = signal(&context.guard, &context.caller, request).await;
832
833 assert_workflow_not_found(error)?;
834 Ok(())
835 }
836
837 #[tokio::test]
838 async fn query_handler_maps_omitted_run_missing_workflow_to_not_found()
839 -> Result<(), Box<dyn std::error::Error>> {
840 let context = context().await?;
841 context.ownership.record(workflow_id(), NAMESPACE)?;
842 let mut request = query_request();
843 request.run_id = None;
844
845 let error = query(&context.guard, &context.caller, request).await;
846
847 assert_workflow_not_found(error)?;
848 Ok(())
849 }
850
851 #[tokio::test]
852 async fn cancel_handler_maps_omitted_run_missing_workflow_to_not_found()
853 -> Result<(), Box<dyn std::error::Error>> {
854 let context = context().await?;
855 context.ownership.record(workflow_id(), NAMESPACE)?;
856 let mut request = cancel_request();
857 request.run_id = None;
858
859 let error = cancel(&context.guard, &context.caller, request).await;
860
861 assert_workflow_not_found(error)?;
862 Ok(())
863 }
864
865 #[tokio::test]
866 async fn denied_start_does_not_decode_missing_payload_before_namespace_check()
867 -> Result<(), Box<dyn std::error::Error>> {
868 let (guard, caller) = denied_guard();
869 let request = ProtoStartWorkflowRequest {
870 namespace: NAMESPACE.to_owned(),
871 workflow_type: "fixture".to_owned(),
872 input: None,
873 routing_key: None,
874 task_queue: None,
875 };
876
877 let error = start(&guard, &caller, request).await;
878
879 assert_eq!(
880 error.err().map(|error| error.code),
881 Some(WireErrorCode::NamespaceDenied)
882 );
883 Ok(())
884 }
885
886 #[tokio::test]
887 async fn denied_signal_does_not_decode_missing_payload_before_namespace_check()
888 -> Result<(), Box<dyn std::error::Error>> {
889 let (guard, caller) = denied_guard();
890 let request = ProtoSignalRequest {
891 namespace: NAMESPACE.to_owned(),
892 workflow_id: Some(workflow_id().into()),
893 run_id: Some(run_id().into()),
894 signal_name: "poke".to_owned(),
895 payload: None,
896 };
897
898 let error = signal(&guard, &caller, request).await;
899
900 assert_eq!(
901 error.err().map(|error| error.code),
902 Some(WireErrorCode::NamespaceDenied)
903 );
904 Ok(())
905 }
906
907 use std::sync::Arc;
910
911 use aion_store::{NamespaceOrigin, NamespaceStore};
912
913 use crate::config::AutoCreate;
914
915 fn namespace_store() -> Arc<dyn NamespaceStore> {
916 Arc::new(aion_store::InMemoryStore::default())
917 }
918
919 fn minter(store: &Arc<dyn NamespaceStore>, policy: AutoCreate) -> NamespaceMinter {
920 NamespaceMinter::new(Arc::clone(store), policy)
921 }
922
923 fn fresh_start_request() -> Result<ProtoStartWorkflowRequest, aion_core::PayloadError> {
924 Ok(ProtoStartWorkflowRequest {
925 namespace: NAMESPACE.to_owned(),
926 workflow_type: "missing-workflow".to_owned(),
927 input: Some(proto_payload()?),
928 routing_key: None,
929 task_queue: None,
930 })
931 }
932
933 #[tokio::test]
939 async fn open_start_mints_durable_record_and_is_idempotent()
940 -> Result<(), Box<dyn std::error::Error>> {
941 let context = context().await?;
942 let store = namespace_store();
943 let minter = minter(&store, AutoCreate::Open);
944
945 assert!(store.get_namespace(NAMESPACE).await?.is_none());
947
948 let first = start_with_placement(
951 &context.guard,
952 &context.caller,
953 fresh_start_request()?,
954 None,
955 Some(&minter),
956 )
957 .await;
958 assert!(
959 first.is_err(),
960 "the fixture start has no registered workflow type"
961 );
962 let record = store
963 .get_namespace(NAMESPACE)
964 .await?
965 .ok_or("expected a durable record minted by the start")?;
966 assert_eq!(record.name, NAMESPACE);
967 assert_eq!(record.origin, NamespaceOrigin::StartMint);
968
969 let _second = start_with_placement(
971 &context.guard,
972 &context.caller,
973 fresh_start_request()?,
974 None,
975 Some(&minter),
976 )
977 .await;
978 let all = store.list_namespaces().await?;
979 assert_eq!(
980 all.iter().filter(|r| r.name == NAMESPACE).count(),
981 1,
982 "a second start must not create a duplicate namespace row"
983 );
984 Ok(())
985 }
986
987 #[tokio::test]
991 async fn closed_start_rejects_unknown_namespace_and_does_not_create_it()
992 -> Result<(), Box<dyn std::error::Error>> {
993 let context = context().await?;
994 let store = namespace_store();
995 let minter = minter(&store, AutoCreate::Closed);
996
997 let denied = start_with_placement(
998 &context.guard,
999 &context.caller,
1000 fresh_start_request()?,
1001 None,
1002 Some(&minter),
1003 )
1004 .await;
1005
1006 let error = denied
1007 .err()
1008 .ok_or_else(|| WireError::backend("expected a namespace-denied error"))?;
1009 assert_eq!(error.code, WireErrorCode::NamespaceDenied);
1010 assert!(
1011 store.get_namespace(NAMESPACE).await?.is_none(),
1012 "closed policy must NOT create the namespace it rejected"
1013 );
1014 Ok(())
1015 }
1016
1017 #[tokio::test]
1021 async fn closed_start_admits_a_known_namespace() -> Result<(), Box<dyn std::error::Error>> {
1022 let context = context().await?;
1023 let store = namespace_store();
1024 store
1025 .register_namespace(NAMESPACE, NamespaceOrigin::Explicit)
1026 .await?;
1027 let minter = minter(&store, AutoCreate::Closed);
1028
1029 let error = start_with_placement(
1032 &context.guard,
1033 &context.caller,
1034 fresh_start_request()?,
1035 None,
1036 Some(&minter),
1037 )
1038 .await
1039 .err()
1040 .ok_or_else(|| WireError::backend("expected the fixture workflow-type miss"))?;
1041 assert_eq!(
1042 error.code,
1043 WireErrorCode::NotFound,
1044 "a known namespace must pass the gate and fail only at the engine"
1045 );
1046 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
1047 Ok(())
1048 }
1049
1050 #[tokio::test]
1053 async fn no_minter_leaves_start_untouched() -> Result<(), Box<dyn std::error::Error>> {
1054 let context = context().await?;
1055 let error = start_with_placement(
1056 &context.guard,
1057 &context.caller,
1058 fresh_start_request()?,
1059 None,
1060 None,
1061 )
1062 .await
1063 .err()
1064 .ok_or_else(|| WireError::backend("expected the fixture workflow-type miss"))?;
1065 assert_eq!(error.code, WireErrorCode::NotFound);
1066 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
1067 Ok(())
1068 }
1069}