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::{optional_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 arguments = optional_payload(request.arguments.clone())?;
230 let span = info_span!(
231 "engine_operation",
232 operation = "query",
233 namespace = %namespace,
234 workflow_id = %workflow_id,
235 query_name = %query_name,
236 );
237
238 let outcome = async {
239 engine
240 .query(&workflow_id, &run_id, query_name, arguments)
241 .await
242 }
243 .instrument(span)
244 .await;
245
246 match outcome {
247 Ok(result) => Ok(ProtoQueryResponse {
248 outcome: Some(proto_query_response::Outcome::Result(result.into())),
249 }),
250 Err(error @ aion::EngineError::Query(_)) => Ok(ProtoQueryResponse {
256 outcome: Some(proto_query_response::Outcome::Error(
257 ServerError::from(error).to_wire_error().into(),
258 )),
259 }),
260 Err(error) => Err(map_workflow_operation_error(error, &workflow_id)),
261 }
262}
263
264pub async fn cancel(
271 guard: &NamespaceGuard,
272 caller: &CallerIdentity,
273 request: ProtoCancelRequest,
274) -> Result<ProtoCancelResponse, WireError> {
275 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
276 let target = WorkflowTarget::workflow(&workflow_id);
277 let scoped = guard
278 .scope(caller, &NamespaceOperation::cancel(&request, target))
279 .await
280 .map_err(|error| error.to_wire_error())?;
281 let namespace = scoped.namespace().to_owned();
282 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
283 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
284 if let Some(status) = terminal_status(engine.as_ref(), &workflow_id).await? {
285 return Err(cancel_terminal_error(&workflow_id, status));
286 }
287
288 let span = info_span!(
289 "engine_operation",
290 operation = "cancel",
291 namespace = %namespace,
292 workflow_id = %workflow_id,
293 );
294
295 async {
296 engine
297 .cancel(&workflow_id, &run_id, request.reason)
298 .await
299 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
300 }
301 .instrument(span)
302 .await?;
303
304 Ok(ProtoCancelResponse {})
305}
306
307pub async fn reopen(
321 guard: &NamespaceGuard,
322 caller: &CallerIdentity,
323 request: ProtoReopenRequest,
324) -> Result<ProtoReopenResponse, WireError> {
325 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
326 let target = WorkflowTarget::workflow(&workflow_id);
327 let scoped = guard
328 .scope(caller, &NamespaceOperation::reopen(&request, target))
329 .await
330 .map_err(|error| error.to_wire_error())?;
331 let namespace = scoped.namespace().to_owned();
332 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
333 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
334
335 let span = info_span!(
336 "engine_operation",
337 operation = "reopen",
338 namespace = %namespace,
339 workflow_id = %workflow_id,
340 );
341
342 let handle = async {
343 engine
344 .reopen_workflow(&workflow_id, &run_id)
345 .await
346 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
347 }
348 .instrument(span)
349 .await?;
350
351 Ok(ProtoReopenResponse {
352 run_id: Some(handle.run_id().clone().into()),
353 status: aion_proto::ProtoWorkflowStatus::from(handle.cached_status()) as i32,
354 })
355}
356
357pub async fn pause(
370 guard: &NamespaceGuard,
371 caller: &CallerIdentity,
372 request: ProtoPauseRequest,
373) -> Result<ProtoPauseResponse, WireError> {
374 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
375 let target = WorkflowTarget::workflow(&workflow_id);
376 let scoped = guard
377 .scope(
378 caller,
379 &NamespaceOperation::pause_workflow(&request, target),
380 )
381 .await
382 .map_err(|error| error.to_wire_error())?;
383 let namespace = scoped.namespace().to_owned();
384 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
385 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
386 let reason = if request.reason.is_empty() {
387 None
388 } else {
389 Some(request.reason.clone())
390 };
391
392 let span = info_span!(
393 "engine_operation",
394 operation = "pause",
395 namespace = %namespace,
396 workflow_id = %workflow_id,
397 );
398
399 let handle = async {
400 engine
401 .pause_workflow(&workflow_id, &run_id, reason, None)
402 .await
403 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
404 }
405 .instrument(span)
406 .await?;
407
408 Ok(ProtoPauseResponse {
409 run_id: Some(handle.run_id().clone().into()),
410 status: aion_proto::ProtoWorkflowStatus::Paused as i32,
413 })
414}
415
416pub async fn resume(
428 guard: &NamespaceGuard,
429 caller: &CallerIdentity,
430 request: ProtoResumeRequest,
431) -> Result<ProtoResumeResponse, WireError> {
432 let workflow_id = required_workflow_id(request.workflow_id.clone())?;
433 let target = WorkflowTarget::workflow(&workflow_id);
434 let scoped = guard
435 .scope(
436 caller,
437 &NamespaceOperation::resume_workflow(&request, target),
438 )
439 .await
440 .map_err(|error| error.to_wire_error())?;
441 let namespace = scoped.namespace().to_owned();
442 let engine = scoped.engine().map_err(|error| error.to_wire_error())?;
443 let run_id = resolve_run_id(engine.as_ref(), &workflow_id, request.run_id.clone()).await?;
444
445 let span = info_span!(
446 "engine_operation",
447 operation = "resume",
448 namespace = %namespace,
449 workflow_id = %workflow_id,
450 );
451
452 let handle = async {
453 engine
454 .resume_paused_workflow(&workflow_id, &run_id, None)
455 .await
456 .map_err(|error| map_workflow_operation_error(error, &workflow_id))
457 }
458 .instrument(span)
459 .await?;
460
461 Ok(ProtoResumeResponse {
462 run_id: Some(handle.run_id().clone().into()),
463 status: aion_proto::ProtoWorkflowStatus::Running as i32,
464 })
465}
466
467#[cfg(test)]
468mod tests {
469 use aion_proto::{WireError, WireErrorCode};
470
471 use super::super::test_support::{
472 NAMESPACE, append_completed, append_failed, append_started, append_timed_out,
473 assert_workflow_not_found, cancel_request, context, denied_guard, proto_payload,
474 query_request, reopen_request, run_id, signal_request, workflow_id,
475 };
476 use super::*;
477
478 #[tokio::test]
479 async fn start_handler_scopes_then_invokes_engine_start()
480 -> Result<(), Box<dyn std::error::Error>> {
481 let context = context().await?;
482 let request = ProtoStartWorkflowRequest {
483 namespace: NAMESPACE.to_owned(),
484 workflow_type: "missing-workflow".to_owned(),
485 input: Some(proto_payload()?),
486 routing_key: None,
487 task_queue: None,
488 };
489
490 let error = start(&context.guard, &context.caller, request).await;
491
492 let error = error
493 .err()
494 .ok_or_else(|| WireError::backend("expected error"))?;
495 assert_eq!(error.code, WireErrorCode::NotFound);
496 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
497 assert_eq!(
498 error.message,
499 "workflow type missing-workflow is not registered"
500 );
501 Ok(())
502 }
503
504 #[test]
505 fn start_records_namespace_only_when_no_task_queue_selected() {
506 use crate::namespace::{NAMESPACE_ATTRIBUTE, TASK_QUEUE_ATTRIBUTE};
507
508 let attributes = start_search_attributes("tenant-a", None);
509 assert_eq!(
510 attributes.get(NAMESPACE_ATTRIBUTE),
511 Some(&aion_core::SearchAttributeValue::String(
512 "tenant-a".to_owned()
513 ))
514 );
515 assert!(!attributes.contains_key(TASK_QUEUE_ATTRIBUTE));
518 }
519
520 #[test]
521 fn start_records_selected_task_queue_durably_like_namespace() {
522 use crate::namespace::{NAMESPACE_ATTRIBUTE, TASK_QUEUE_ATTRIBUTE};
523
524 let attributes = start_search_attributes("tenant-a", Some("gpu"));
525 assert_eq!(
526 attributes.get(NAMESPACE_ATTRIBUTE),
527 Some(&aion_core::SearchAttributeValue::String(
528 "tenant-a".to_owned()
529 ))
530 );
531 assert_eq!(
535 attributes.get(TASK_QUEUE_ATTRIBUTE),
536 Some(&aion_core::SearchAttributeValue::String("gpu".to_owned()))
537 );
538 }
539
540 #[tokio::test]
541 async fn signal_handler_scopes_then_invokes_engine_signal()
542 -> Result<(), Box<dyn std::error::Error>> {
543 let context = context().await?;
544 context.ownership.record(workflow_id(), NAMESPACE)?;
545
546 let error = signal(&context.guard, &context.caller, signal_request()?).await;
547
548 let error = error
549 .err()
550 .ok_or_else(|| WireError::backend("expected error"))?;
551 assert_eq!(error.code, WireErrorCode::NotFound);
552 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
553 assert_eq!(
554 error.message,
555 format!("workflow {} not found", workflow_id())
556 );
557 Ok(())
558 }
559
560 #[tokio::test]
561 async fn query_handler_scopes_then_invokes_engine_query()
562 -> Result<(), Box<dyn std::error::Error>> {
563 let context = context().await?;
564 context.ownership.record(workflow_id(), NAMESPACE)?;
565
566 let error = query(&context.guard, &context.caller, query_request()).await;
567
568 let error = error
569 .err()
570 .ok_or_else(|| WireError::backend("expected error"))?;
571 assert_eq!(error.code, WireErrorCode::NotFound);
572 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
573 assert_eq!(
574 error.message,
575 format!("workflow {} not found", workflow_id())
576 );
577 Ok(())
578 }
579
580 #[tokio::test]
581 async fn query_handler_returns_not_running_outcome_for_terminal_workflow()
582 -> Result<(), Box<dyn std::error::Error>> {
583 let context = context().await?;
584 context.ownership.record(workflow_id(), NAMESPACE)?;
585 append_completed(context.store.as_ref()).await?;
586 let mut request = query_request();
589 request.run_id = None;
590
591 let response = query(&context.guard, &context.caller, request).await?;
592
593 let Some(proto_query_response::Outcome::Error(error)) = response.outcome else {
596 return Err("expected a QueryResponse.error outcome".into());
597 };
598 let error = WireError::try_from(error)?;
599 assert_eq!(error.code, WireErrorCode::NotRunning);
600 assert_eq!(error.error_type.as_deref(), Some("QueryNotRunning"));
601 Ok(())
602 }
603
604 #[tokio::test]
605 async fn query_handler_keeps_non_resident_non_terminal_workflow_as_transport_not_found()
606 -> Result<(), Box<dyn std::error::Error>> {
607 let context = context().await?;
611 context.ownership.record(workflow_id(), NAMESPACE)?;
612 append_started(context.store.as_ref()).await?;
613 let mut request = query_request();
614 request.run_id = None;
615
616 let error = query(&context.guard, &context.caller, request).await;
617
618 let error = error
619 .err()
620 .ok_or_else(|| WireError::backend("expected error"))?;
621 assert_eq!(error.code, WireErrorCode::NotFound);
622 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
623 Ok(())
624 }
625
626 #[tokio::test]
627 async fn cancel_handler_scopes_then_invokes_engine_cancel()
628 -> Result<(), Box<dyn std::error::Error>> {
629 let context = context().await?;
630 context.ownership.record(workflow_id(), NAMESPACE)?;
631
632 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
633
634 let error = error
635 .err()
636 .ok_or_else(|| WireError::backend("expected error"))?;
637 assert_eq!(error.code, WireErrorCode::NotFound);
638 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
639 assert_eq!(
640 error.message,
641 format!("workflow {} not found", workflow_id())
642 );
643 Ok(())
644 }
645
646 #[tokio::test]
647 async fn reopen_handler_maps_missing_workflow_to_not_found()
648 -> Result<(), Box<dyn std::error::Error>> {
649 let context = context().await?;
650 context.ownership.record(workflow_id(), NAMESPACE)?;
651
652 let error = reopen(&context.guard, &context.caller, reopen_request()).await;
653
654 let error = error
655 .err()
656 .ok_or_else(|| WireError::backend("expected error"))?;
657 assert_eq!(error.code, WireErrorCode::NotFound);
658 assert_eq!(error.error_type.as_deref(), Some("WorkflowNotFound"));
659 Ok(())
660 }
661
662 #[tokio::test]
663 async fn reopen_handler_rejects_completed_workflow_as_invalid_state()
664 -> Result<(), Box<dyn std::error::Error>> {
665 let context = context().await?;
666 context.ownership.record(workflow_id(), NAMESPACE)?;
667 append_completed(context.store.as_ref()).await?;
668 let mut request = reopen_request();
669 request.run_id = None;
670
671 let error = reopen(&context.guard, &context.caller, request).await;
672
673 let error = error
674 .err()
675 .ok_or_else(|| WireError::backend("expected error"))?;
676 assert_eq!(error.code, WireErrorCode::InvalidState);
677 assert_eq!(error.error_type.as_deref(), Some("InvalidState"));
678 Ok(())
679 }
680
681 #[tokio::test]
682 async fn reopen_handler_rejects_timed_out_workflow_as_invalid_state()
683 -> Result<(), Box<dyn std::error::Error>> {
684 let context = context().await?;
685 context.ownership.record(workflow_id(), NAMESPACE)?;
686 append_timed_out(context.store.as_ref()).await?;
687 let mut request = reopen_request();
688 request.run_id = None;
689
690 let error = reopen(&context.guard, &context.caller, request).await;
691
692 let error = error
693 .err()
694 .ok_or_else(|| WireError::backend("expected error"))?;
695 assert_eq!(error.code, WireErrorCode::InvalidState);
697 assert_eq!(error.error_type.as_deref(), Some("InvalidState"));
698 Ok(())
699 }
700
701 #[tokio::test]
702 async fn reopen_handler_maps_omitted_run_missing_workflow_to_not_found()
703 -> Result<(), Box<dyn std::error::Error>> {
704 let context = context().await?;
705 context.ownership.record(workflow_id(), NAMESPACE)?;
706 let mut request = reopen_request();
707 request.run_id = None;
708
709 let error = reopen(&context.guard, &context.caller, request).await;
710
711 assert_workflow_not_found(error)?;
712 Ok(())
713 }
714
715 #[tokio::test]
718 async fn denied_reopen_is_namespace_denied_before_engine_check()
719 -> Result<(), Box<dyn std::error::Error>> {
720 let (guard, caller) = denied_guard();
721 let request = ProtoReopenRequest {
722 namespace: NAMESPACE.to_owned(),
723 workflow_id: Some(workflow_id().into()),
724 run_id: Some(run_id().into()),
725 };
726
727 let error = reopen(&guard, &caller, request).await;
728
729 assert_eq!(
730 error.err().map(|error| error.code),
731 Some(WireErrorCode::NamespaceDenied)
732 );
733 Ok(())
734 }
735
736 #[tokio::test]
737 async fn signal_handler_rejects_completed_workflow() -> Result<(), Box<dyn std::error::Error>> {
738 let context = context().await?;
739 context.ownership.record(workflow_id(), NAMESPACE)?;
740 append_completed(context.store.as_ref()).await?;
741
742 let error = signal(&context.guard, &context.caller, signal_request()?).await;
743
744 let error = error
745 .err()
746 .ok_or_else(|| WireError::backend("expected error"))?;
747 assert_eq!(error.code, WireErrorCode::NotRunning);
748 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
749 assert_eq!(
750 error.message,
751 format!(
752 "workflow {} has already reached terminal state Completed",
753 workflow_id()
754 )
755 );
756 Ok(())
757 }
758
759 #[tokio::test]
760 async fn signal_handler_rejects_failed_workflow() -> Result<(), Box<dyn std::error::Error>> {
761 let context = context().await?;
762 context.ownership.record(workflow_id(), NAMESPACE)?;
763 append_failed(context.store.as_ref()).await?;
764
765 let error = signal(&context.guard, &context.caller, signal_request()?).await;
766
767 let error = error
768 .err()
769 .ok_or_else(|| WireError::backend("expected error"))?;
770 assert_eq!(error.code, WireErrorCode::NotRunning);
771 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
772 assert_eq!(
773 error.message,
774 format!(
775 "workflow {} has already reached terminal state Failed",
776 workflow_id()
777 )
778 );
779 Ok(())
780 }
781
782 #[tokio::test]
783 async fn cancel_handler_rejects_completed_workflow() -> Result<(), Box<dyn std::error::Error>> {
784 let context = context().await?;
785 context.ownership.record(workflow_id(), NAMESPACE)?;
786 append_completed(context.store.as_ref()).await?;
787
788 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
789
790 let error = error
791 .err()
792 .ok_or_else(|| WireError::backend("expected error"))?;
793 assert_eq!(error.code, WireErrorCode::NotRunning);
794 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
795 assert_eq!(
796 error.message,
797 format!(
798 "workflow {} has already completed with status Completed",
799 workflow_id()
800 )
801 );
802 assert!(!error.message.contains("process 0 is not live"));
803 Ok(())
804 }
805
806 #[tokio::test]
807 async fn cancel_handler_rejects_failed_workflow() -> Result<(), Box<dyn std::error::Error>> {
808 let context = context().await?;
809 context.ownership.record(workflow_id(), NAMESPACE)?;
810 append_failed(context.store.as_ref()).await?;
811
812 let error = cancel(&context.guard, &context.caller, cancel_request()).await;
813
814 let error = error
815 .err()
816 .ok_or_else(|| WireError::backend("expected error"))?;
817 assert_eq!(error.code, WireErrorCode::NotRunning);
818 assert_eq!(error.error_type.as_deref(), Some("WorkflowTerminal"));
819 assert_eq!(
820 error.message,
821 format!(
822 "workflow {} has already completed with status Failed",
823 workflow_id()
824 )
825 );
826 assert!(!error.message.contains("process 0 is not live"));
827 Ok(())
828 }
829
830 #[tokio::test]
831 async fn signal_handler_maps_omitted_run_missing_workflow_to_not_found()
832 -> Result<(), Box<dyn std::error::Error>> {
833 let context = context().await?;
834 context.ownership.record(workflow_id(), NAMESPACE)?;
835 let mut request = signal_request()?;
836 request.run_id = None;
837
838 let error = signal(&context.guard, &context.caller, request).await;
839
840 assert_workflow_not_found(error)?;
841 Ok(())
842 }
843
844 #[tokio::test]
845 async fn query_handler_maps_omitted_run_missing_workflow_to_not_found()
846 -> Result<(), Box<dyn std::error::Error>> {
847 let context = context().await?;
848 context.ownership.record(workflow_id(), NAMESPACE)?;
849 let mut request = query_request();
850 request.run_id = None;
851
852 let error = query(&context.guard, &context.caller, request).await;
853
854 assert_workflow_not_found(error)?;
855 Ok(())
856 }
857
858 #[tokio::test]
859 async fn cancel_handler_maps_omitted_run_missing_workflow_to_not_found()
860 -> Result<(), Box<dyn std::error::Error>> {
861 let context = context().await?;
862 context.ownership.record(workflow_id(), NAMESPACE)?;
863 let mut request = cancel_request();
864 request.run_id = None;
865
866 let error = cancel(&context.guard, &context.caller, request).await;
867
868 assert_workflow_not_found(error)?;
869 Ok(())
870 }
871
872 #[tokio::test]
873 async fn denied_start_does_not_decode_missing_payload_before_namespace_check()
874 -> Result<(), Box<dyn std::error::Error>> {
875 let (guard, caller) = denied_guard();
876 let request = ProtoStartWorkflowRequest {
877 namespace: NAMESPACE.to_owned(),
878 workflow_type: "fixture".to_owned(),
879 input: None,
880 routing_key: None,
881 task_queue: None,
882 };
883
884 let error = start(&guard, &caller, request).await;
885
886 assert_eq!(
887 error.err().map(|error| error.code),
888 Some(WireErrorCode::NamespaceDenied)
889 );
890 Ok(())
891 }
892
893 #[tokio::test]
894 async fn denied_signal_does_not_decode_missing_payload_before_namespace_check()
895 -> Result<(), Box<dyn std::error::Error>> {
896 let (guard, caller) = denied_guard();
897 let request = ProtoSignalRequest {
898 namespace: NAMESPACE.to_owned(),
899 workflow_id: Some(workflow_id().into()),
900 run_id: Some(run_id().into()),
901 signal_name: "poke".to_owned(),
902 payload: None,
903 };
904
905 let error = signal(&guard, &caller, request).await;
906
907 assert_eq!(
908 error.err().map(|error| error.code),
909 Some(WireErrorCode::NamespaceDenied)
910 );
911 Ok(())
912 }
913
914 use std::sync::Arc;
917
918 use aion_store::{NamespaceOrigin, NamespaceStore};
919
920 use crate::config::AutoCreate;
921
922 fn namespace_store() -> Arc<dyn NamespaceStore> {
923 Arc::new(aion_store::InMemoryStore::default())
924 }
925
926 fn minter(store: &Arc<dyn NamespaceStore>, policy: AutoCreate) -> NamespaceMinter {
927 NamespaceMinter::new(Arc::clone(store), policy)
928 }
929
930 fn fresh_start_request() -> Result<ProtoStartWorkflowRequest, aion_core::PayloadError> {
931 Ok(ProtoStartWorkflowRequest {
932 namespace: NAMESPACE.to_owned(),
933 workflow_type: "missing-workflow".to_owned(),
934 input: Some(proto_payload()?),
935 routing_key: None,
936 task_queue: None,
937 })
938 }
939
940 #[tokio::test]
946 async fn open_start_mints_durable_record_and_is_idempotent()
947 -> Result<(), Box<dyn std::error::Error>> {
948 let context = context().await?;
949 let store = namespace_store();
950 let minter = minter(&store, AutoCreate::Open);
951
952 assert!(store.get_namespace(NAMESPACE).await?.is_none());
954
955 let first = start_with_placement(
958 &context.guard,
959 &context.caller,
960 fresh_start_request()?,
961 None,
962 Some(&minter),
963 )
964 .await;
965 assert!(
966 first.is_err(),
967 "the fixture start has no registered workflow type"
968 );
969 let record = store
970 .get_namespace(NAMESPACE)
971 .await?
972 .ok_or("expected a durable record minted by the start")?;
973 assert_eq!(record.name, NAMESPACE);
974 assert_eq!(record.origin, NamespaceOrigin::StartMint);
975
976 let _second = start_with_placement(
978 &context.guard,
979 &context.caller,
980 fresh_start_request()?,
981 None,
982 Some(&minter),
983 )
984 .await;
985 let all = store.list_namespaces().await?;
986 assert_eq!(
987 all.iter().filter(|r| r.name == NAMESPACE).count(),
988 1,
989 "a second start must not create a duplicate namespace row"
990 );
991 Ok(())
992 }
993
994 #[tokio::test]
998 async fn closed_start_rejects_unknown_namespace_and_does_not_create_it()
999 -> Result<(), Box<dyn std::error::Error>> {
1000 let context = context().await?;
1001 let store = namespace_store();
1002 let minter = minter(&store, AutoCreate::Closed);
1003
1004 let denied = start_with_placement(
1005 &context.guard,
1006 &context.caller,
1007 fresh_start_request()?,
1008 None,
1009 Some(&minter),
1010 )
1011 .await;
1012
1013 let error = denied
1014 .err()
1015 .ok_or_else(|| WireError::backend("expected a namespace-denied error"))?;
1016 assert_eq!(error.code, WireErrorCode::NamespaceDenied);
1017 assert!(
1018 store.get_namespace(NAMESPACE).await?.is_none(),
1019 "closed policy must NOT create the namespace it rejected"
1020 );
1021 Ok(())
1022 }
1023
1024 #[tokio::test]
1028 async fn closed_start_admits_a_known_namespace() -> Result<(), Box<dyn std::error::Error>> {
1029 let context = context().await?;
1030 let store = namespace_store();
1031 store
1032 .register_namespace(NAMESPACE, NamespaceOrigin::Explicit)
1033 .await?;
1034 let minter = minter(&store, AutoCreate::Closed);
1035
1036 let error = start_with_placement(
1039 &context.guard,
1040 &context.caller,
1041 fresh_start_request()?,
1042 None,
1043 Some(&minter),
1044 )
1045 .await
1046 .err()
1047 .ok_or_else(|| WireError::backend("expected the fixture workflow-type miss"))?;
1048 assert_eq!(
1049 error.code,
1050 WireErrorCode::NotFound,
1051 "a known namespace must pass the gate and fail only at the engine"
1052 );
1053 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
1054 Ok(())
1055 }
1056
1057 #[tokio::test]
1060 async fn no_minter_leaves_start_untouched() -> Result<(), Box<dyn std::error::Error>> {
1061 let context = context().await?;
1062 let error = start_with_placement(
1063 &context.guard,
1064 &context.caller,
1065 fresh_start_request()?,
1066 None,
1067 None,
1068 )
1069 .await
1070 .err()
1071 .ok_or_else(|| WireError::backend("expected the fixture workflow-type miss"))?;
1072 assert_eq!(error.code, WireErrorCode::NotFound);
1073 assert_eq!(error.error_type.as_deref(), Some("WorkflowTypeNotFound"));
1074 Ok(())
1075 }
1076}