@gadmin2n/schematics 0.0.144 → 0.0.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/.types.d.ts +10 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/approval.prisma +2 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +140 -19
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -2
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/data-mngt.seed.ts +0 -2
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +694 -327
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-outbox.ts +49 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +35 -5
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +14 -24
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-executor.spec.ts +23 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-executor.ts +16 -5
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +98 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.ts +26 -6
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.dto.ts +17 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.spec.ts +1 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.ts +3 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.dto.ts +0 -2
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-signal-outbox.service.spec.ts +30 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-signal-outbox.service.ts +28 -6
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction.service.spec.ts +63 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction.service.ts +151 -38
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-wait.handler.ts +4 -1
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/human-approval.handler.spec.ts +16 -5
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/human-approval.handler.ts +23 -6
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.spec.ts +130 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.ts +62 -62
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.dto.ts +15 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.util.spec.ts +70 -9
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.util.ts +31 -7
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-signal-outbox.service.spec.ts +30 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-signal-outbox.service.ts +28 -6
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +50 -3
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +26 -3
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +53 -47
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.errors.ts +0 -1
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +541 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.ts +442 -200
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/outbox-retry.util.ts +1 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +150 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.ts +56 -40
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.dto.ts +16 -60
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +13 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +44 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +6 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +258 -12
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.ts +127 -14
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/manual-workflow-execution.spec.ts +85 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/manual-workflow-execution.ts +108 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/parameter-binding.ts +5 -5
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +96 -25
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +77 -34
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.spec.ts +23 -34
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.ts +0 -21
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +245 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +639 -188
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +408 -235
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.types.ts +1 -3
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-data-edge.util.spec.ts +279 -95
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-data-edge.util.ts +360 -80
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +323 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +773 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +7 -13
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +0 -16
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.spec.ts +184 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +69 -138
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.spec.ts +113 -0
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.ts +59 -13
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +86 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-switch-config.spec.ts +25 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-switch-config.ts +45 -0
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +78 -43
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +103 -40
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +57 -0
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +3 -1
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +1000 -240
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +2184 -317
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +279 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.ts +5 -1
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +4 -1
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +5 -1
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.spec.ts +12 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.ts +10 -1
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +2 -24
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.ts +3 -70
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +3 -1
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/RowActions.tsx +25 -5
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/WorkflowActionButton.test.tsx +71 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/WorkflowActionButton.tsx +89 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +52 -12
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +12 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +2 -2
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddDataModal.tsx +10 -33
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddPageModal.tsx +13 -37
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DevShell.tsx +55 -7
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/style.css +14 -0
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/http.ts +10 -0
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/list.tsx +1 -0
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/workflowAction.ts +19 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/index.ts +1 -0
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/types.ts +3 -1
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +1 -6
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useReferenceUpload.ts +95 -0
  104. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +2 -2
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +672 -110
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +2 -2
  107. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +676 -114
  108. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +13 -1
  109. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +1 -0
  110. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +38 -4
  111. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +276 -376
  112. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/CanvasAiModal.tsx +126 -6
  113. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/DecisionModal.test.tsx +50 -0
  114. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/DecisionModal.tsx +48 -11
  115. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/inbox.tsx +32 -34
  116. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +111 -0
  117. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +167 -0
  118. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/business-events.css +99 -0
  119. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +106 -0
  120. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +324 -0
  121. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +73 -0
  122. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +359 -0
  123. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CanvasZoomControls.tsx +46 -0
  124. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +9 -1
  125. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.test.tsx +105 -4
  126. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +90 -48
  127. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DeletableEdge.tsx +3 -2
  128. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.test.tsx +259 -0
  129. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +167 -103
  130. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +142 -140
  131. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.test.tsx +60 -15
  132. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +25 -12
  133. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +364 -200
  134. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NewNodeDefaults.test.tsx +220 -52
  135. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +260 -0
  136. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.tsx +300 -130
  137. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +507 -33
  138. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +630 -592
  139. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ParameterBindingEditor.tsx +67 -70
  140. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +90 -0
  141. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.tsx +325 -0
  142. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/RunWorkflowModal.test.tsx +52 -0
  143. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/RunWorkflowModal.tsx +14 -9
  144. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +2 -1
  145. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/VersionPanel.tsx +2 -1
  146. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +42 -0
  147. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +76 -0
  148. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowWorkspaceHeader.tsx +56 -0
  149. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/parameter-binding-editor.css +46 -0
  150. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/workflow-code-editor.css +34 -0
  151. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/ConnectionModal.test.tsx +53 -7
  152. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/ConnectionModal.tsx +123 -62
  153. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +85 -0
  154. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +29 -0
  155. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +75 -0
  156. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +69 -36
  157. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.test.tsx +494 -0
  158. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +665 -194
  159. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useReferencedNodeInstanceConfigs.test.tsx +92 -0
  160. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useReferencedNodeInstanceConfigs.ts +96 -0
  161. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.test.tsx +297 -0
  162. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +135 -65
  163. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +53 -42
  164. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +344 -155
  165. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +79 -58
  166. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/CreateNodeInstanceModal.tsx +123 -144
  167. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +169 -98
  168. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/node-instance-form.css +91 -0
  169. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/index.tsx +54 -31
  170. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +854 -0
  171. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/SchemaField.tsx +41 -14
  172. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/SchemaForm.test.tsx +75 -0
  173. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/SchemaForm.tsx +3 -0
  174. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +20 -2
  175. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/widget-registry.tsx +2 -1
  176. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +61 -66
  177. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApprovalRuleEditor.test.tsx +33 -1
  178. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApprovalRuleEditor.tsx +47 -15
  179. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/BranchConfigEditors.test.tsx +104 -0
  180. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CallableInputParametersEditor.tsx +22 -52
  181. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +72 -0
  182. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.tsx +78 -70
  183. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CronTriggerConfigEditor.tsx +569 -0
  184. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +130 -0
  185. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DelayConfigEditor.tsx +100 -0
  186. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +620 -0
  187. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.test.tsx +22 -45
  188. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.tsx +25 -92
  189. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalWaitConfigEditor.tsx +3 -11
  190. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ForEachConfigEditor.test.tsx +58 -0
  191. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ForEachConfigEditor.tsx +107 -0
  192. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HttpRequestConfigEditor.test.tsx +212 -31
  193. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HttpRequestConfigEditor.tsx +214 -304
  194. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +175 -40
  195. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +693 -293
  196. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/IfElseConfigEditor.tsx +190 -0
  197. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ManualTriggerConfigEditor.test.tsx +152 -0
  198. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ManualTriggerConfigEditor.tsx +286 -0
  199. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ParallelConfigEditor.tsx +101 -0
  200. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ParameterBindingListEditor.tsx +99 -0
  201. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +495 -0
  202. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubWorkflowConfigEditor.test.tsx +124 -0
  203. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubWorkflowConfigEditor.tsx +222 -0
  204. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +2 -4
  205. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SwitchConfigEditor.tsx +204 -0
  206. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +3 -1
  207. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupEditModal.tsx +9 -6
  208. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/callable-workflow.ts +4 -0
  209. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/code-config.css +96 -0
  210. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +117 -0
  211. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/http-request-config.css +242 -0
  212. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +194 -0
  213. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +264 -167
  214. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +6 -9
  215. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types/parameter-binding.ts +19 -16
  216. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +28 -4
  217. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/useWorkflowFormValidation.test.tsx +59 -0
  218. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/useWorkflowFormValidation.ts +24 -0
  219. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.test.ts +311 -0
  220. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +198 -0
  221. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/mainFlowConnection.test.ts +143 -0
  222. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/mainFlowConnection.ts +19 -0
  223. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +103 -5
  224. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/removeWorkflowEdges.test.ts +158 -3
  225. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/removeWorkflowEdges.ts +117 -9
  226. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveEffectiveOutput.test.ts +70 -0
  227. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveEffectiveOutput.ts +48 -0
  228. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveLookupPorts.test.ts +55 -0
  229. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveLookupPorts.ts +95 -22
  230. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.test.ts +148 -1
  231. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +62 -11
  232. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveWorkflowDisplay.test.ts +19 -0
  233. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveWorkflowDisplay.ts +1 -1
  234. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/subWorkflowChange.test.ts +45 -0
  235. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/subWorkflowChange.ts +26 -0
  236. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/validateWorkflowDsl.test.ts +502 -0
  237. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/validateWorkflowDsl.ts +694 -0
  238. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowDateTime.test.ts +27 -0
  239. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowDateTime.ts +33 -0
  240. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowHistory.test.ts +34 -0
  241. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowHistory.ts +52 -0
  242. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowPublishError.test.ts +99 -0
  243. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowPublishError.ts +91 -0
  244. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowSkillPrompt.test.ts +74 -0
  245. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/workflowSkillPrompt.ts +120 -0
  246. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/workflow-ui.css +481 -0
  247. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +1 -1
  248. package/package.json +1 -1
  249. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowEventOutbox.ts +0 -62
  250. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/migrate-human-approval-configs.ts +0 -86
  251. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-event-trigger.ts +0 -60
  252. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +0 -698
  253. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.external-decision.spec.ts +0 -183
  254. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/legacy-approval-config-migration.ts +0 -227
  255. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.spec.ts +0 -82
  256. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.ts +0 -111
  257. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +0 -54
  258. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.ts +0 -147
  259. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.module.ts +0 -10
  260. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +0 -350
  261. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.ts +0 -110
  262. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/index.ts +0 -1
  263. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/InstanceRequiredDialog.tsx +0 -106
  264. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/LookupConnectionDialog.test.tsx +0 -45
  265. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/LookupConnectionDialog.tsx +0 -43
  266. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeTypeBadge.tsx +0 -36
  267. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useNodeInstanceLibrary.ts +0 -41
  268. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApproverSourceEditor.test.tsx +0 -47
  269. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApproverSourceEditor.tsx +0 -182
  270. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.test.tsx +0 -34
  271. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.tsx +0 -16
  272. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/approver-source.test.ts +0 -25
  273. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/approver-source.ts +0 -42
  274. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/lookupConnection.test.ts +0 -208
  275. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/lookupConnection.ts +0 -246
  276. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/create.tsx +0 -111
  277. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/edit.tsx +0 -127
  278. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/index.tsx +0 -4
  279. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/list.tsx +0 -282
  280. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/show.tsx +0 -74
@@ -11,17 +11,21 @@ describe('WorkflowService — cron lifecycle hooks', () => {
11
11
  let service: WorkflowService;
12
12
  let prisma: any;
13
13
  let temporal: any;
14
+ let compiler: any;
14
15
 
15
16
  beforeEach(async () => {
16
17
  prisma = {
17
18
  workflow: {
18
19
  findUnique: jest.fn(),
20
+ findUniqueOrThrow: jest.fn(),
19
21
  update: jest.fn(),
22
+ updateMany: jest.fn().mockResolvedValue({ count: 1 }),
20
23
  delete: jest.fn(),
21
24
  findMany: jest.fn(),
22
25
  },
23
26
  workflowVersion: {
24
27
  findFirst: jest.fn(),
28
+ findMany: jest.fn().mockResolvedValue([]),
25
29
  create: jest.fn(),
26
30
  },
27
31
  workflowNodeType: {
@@ -30,12 +34,19 @@ describe('WorkflowService — cron lifecycle hooks', () => {
30
34
  { type: 'manual_trigger', outputs: [] },
31
35
  { type: 'webhook_trigger', outputs: [] },
32
36
  { type: 'http_request', outputs: [] },
37
+ { type: 'code', outputs: [] },
33
38
  { type: 'if_else', outputs: ['true', 'false'] },
34
39
  { type: 'switch', outputs: null },
35
40
  { type: 'approval', outputs: ['true', 'false'] },
36
41
  { type: 'custom_branch', outputs: ['success', 'failure'] },
37
42
  ]),
38
43
  },
44
+ workflowNodeInstance: {
45
+ findMany: jest.fn().mockResolvedValue([]),
46
+ },
47
+ workflowInstance: {
48
+ count: jest.fn().mockResolvedValue(0),
49
+ },
39
50
  externalInteraction: {
40
51
  findMany: jest.fn().mockResolvedValue([]),
41
52
  deleteMany: jest.fn(),
@@ -44,17 +55,31 @@ describe('WorkflowService — cron lifecycle hooks', () => {
44
55
  externalInteractionSignalOutbox: { deleteMany: jest.fn() },
45
56
  $transaction: jest.fn(async (cb: any) =>
46
57
  cb({
58
+ $queryRaw: jest.fn().mockImplementation(async () => [
59
+ {
60
+ isEnabled:
61
+ prisma.workflow.updateMany.mock.calls.at(-1)?.[0]?.data
62
+ ?.isEnabled ?? true,
63
+ status: 'PUBLISHED',
64
+ },
65
+ ]),
47
66
  workflowVersion: {
48
67
  findFirst: prisma.workflowVersion.findFirst,
68
+ findMany: prisma.workflowVersion.findMany,
49
69
  create: async (args: any) => ({
50
70
  ...(await prisma.workflowVersion.create(args)),
51
71
  dsl: args.data.dsl,
52
72
  }),
53
73
  },
54
74
  workflow: {
75
+ findUnique: prisma.workflow.findUnique,
76
+ findUniqueOrThrow: prisma.workflow.findUniqueOrThrow,
55
77
  update: prisma.workflow.update,
78
+ updateMany: prisma.workflow.updateMany,
56
79
  delete: prisma.workflow.delete,
80
+ findMany: prisma.workflow.findMany,
57
81
  },
82
+ workflowInstance: prisma.workflowInstance,
58
83
  externalInteraction: prisma.externalInteraction,
59
84
  externalInteractionEvent: prisma.externalInteractionEvent,
60
85
  externalInteractionSignalOutbox:
@@ -63,12 +88,18 @@ describe('WorkflowService — cron lifecycle hooks', () => {
63
88
  }),
64
89
  ),
65
90
  };
91
+ prisma.workflow.findUniqueOrThrow.mockImplementation((args: any) =>
92
+ prisma.workflow.findUnique(args),
93
+ );
66
94
  temporal = {
67
95
  upsertCronSchedule: jest.fn(),
68
96
  pauseCronSchedule: jest.fn(),
69
97
  unpauseCronSchedule: jest.fn(),
70
98
  deleteCronSchedule: jest.fn(),
99
+ getCronScheduleStatus: jest.fn().mockResolvedValue('ACTIVE'),
100
+ inspectWorkflow: jest.fn(),
71
101
  };
102
+ compiler = { compile: jest.fn(async (dsl: any) => dsl) };
72
103
 
73
104
  const module: TestingModule = await Test.createTestingModule({
74
105
  providers: [
@@ -76,10 +107,7 @@ describe('WorkflowService — cron lifecycle hooks', () => {
76
107
  { provide: PrismaService, useValue: prisma },
77
108
  { provide: TemporalService, useValue: temporal },
78
109
  { provide: HumanApprovalService, useValue: {} },
79
- {
80
- provide: WorkflowCompilerService,
81
- useValue: { compile: jest.fn(async (dsl: any) => dsl) },
82
- },
110
+ { provide: WorkflowCompilerService, useValue: compiler },
83
111
  ],
84
112
  }).compile();
85
113
 
@@ -119,21 +147,24 @@ describe('WorkflowService — cron lifecycle hooks', () => {
119
147
  edges: [],
120
148
  };
121
149
 
122
- it('keeps a saved workflow published when its DSL matches the latest version', async () => {
150
+ it('uses the locked latest version when a first publication races with saving the draft', async () => {
123
151
  prisma.workflow.findUnique.mockResolvedValueOnce({
124
152
  id: BigInt(42),
125
- versions: [{ dsl: publishedDsl }],
153
+ versions: [],
126
154
  });
127
155
  prisma.workflow.update.mockImplementationOnce(({ data }: any) =>
128
156
  Promise.resolve(data),
129
157
  );
158
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce({
159
+ dsl: publishedDsl,
160
+ });
130
161
 
131
162
  const result = await service.saveDraft(BigInt(42), savedDsl);
132
163
 
133
164
  expect(result).toMatchObject({ dsl: savedDsl, status: 'PUBLISHED' });
134
165
  });
135
166
 
136
- it('marks a saved workflow as draft when a node configuration changed', async () => {
167
+ it('keeps the published version executable while marking saved changes as unpublished', async () => {
137
168
  prisma.workflow.findUnique.mockResolvedValueOnce({
138
169
  id: BigInt(42),
139
170
  versions: [{ dsl: publishedDsl }],
@@ -141,6 +172,9 @@ describe('WorkflowService — cron lifecycle hooks', () => {
141
172
  prisma.workflow.update.mockImplementationOnce(({ data }: any) =>
142
173
  Promise.resolve(data),
143
174
  );
175
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce({
176
+ dsl: publishedDsl,
177
+ });
144
178
  const changedDsl = {
145
179
  ...savedDsl,
146
180
  nodes: [
@@ -153,17 +187,31 @@ describe('WorkflowService — cron lifecycle hooks', () => {
153
187
 
154
188
  const result = await service.saveDraft(BigInt(42), changedDsl);
155
189
 
156
- expect(result).toMatchObject({ dsl: changedDsl, status: 'DRAFT' });
190
+ expect(result).toMatchObject({
191
+ dsl: changedDsl,
192
+ status: 'PUBLISHED',
193
+ hasUnpublishedChanges: true,
194
+ });
157
195
  });
158
196
 
159
- it('rejects saving an internally callable Workflow without a return node', async () => {
197
+ it('saves an incomplete internally callable Workflow as a draft', async () => {
160
198
  prisma.workflow.findUnique.mockResolvedValueOnce({
161
199
  id: BigInt(42),
162
200
  versions: [],
163
201
  });
202
+ prisma.workflow.update.mockImplementationOnce(({ data }: any) =>
203
+ Promise.resolve(data),
204
+ );
205
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
164
206
  const callableDsl = {
165
207
  nodes: [
166
- { id: 'input', type: 'callable_trigger', config: {} },
208
+ {
209
+ id: 'input',
210
+ type: 'callable_trigger',
211
+ config: {
212
+ inputParameters: [{ key: 'orderId' }, { key: 'orderId' }],
213
+ },
214
+ },
167
215
  { id: 'work', type: 'code', config: {} },
168
216
  ],
169
217
  edges: [{ source: 'input', target: 'work', kind: 'FLOW' }],
@@ -171,16 +219,10 @@ describe('WorkflowService — cron lifecycle hooks', () => {
171
219
 
172
220
  await expect(
173
221
  service.saveDraft(BigInt(42), callableDsl),
174
- ).rejects.toMatchObject({
175
- response: expect.objectContaining({
176
- code: 'CALLABLE_WORKFLOW_INVALID',
177
- message: '可调用 Workflow 必须且只能包含一个“返回调用”节点',
178
- }),
179
- });
180
- expect(prisma.workflow.update).not.toHaveBeenCalled();
222
+ ).resolves.toMatchObject({ dsl: callableDsl, status: 'DRAFT' });
181
223
  });
182
224
 
183
- it('does not create a version when the saved DSL is already published', async () => {
225
+ it('does not touch Temporal scheduling when an unchanged Workflow has no cron history', async () => {
184
226
  prisma.workflow.findUnique.mockResolvedValueOnce({
185
227
  id: BigInt(42),
186
228
  dsl: savedDsl,
@@ -200,26 +242,141 @@ describe('WorkflowService — cron lifecycle hooks', () => {
200
242
 
201
243
  await expect(
202
244
  service.publish(BigInt(42), { changeSummary: 'duplicate' }, 'tester'),
203
- ).rejects.toMatchObject({
204
- response: expect.objectContaining({
205
- code: 'WORKFLOW_DSL_UNCHANGED',
206
- }),
207
- });
245
+ ).resolves.toMatchObject({ version: 1 });
208
246
  expect(prisma.workflowVersion.create).not.toHaveBeenCalled();
247
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
248
+ });
249
+ });
250
+
251
+ describe('schedule observability', () => {
252
+ it('returns the live Temporal schedule state for a cron workflow', async () => {
253
+ prisma.workflow.findUnique.mockResolvedValueOnce({
254
+ id: BigInt(42),
255
+ dsl: null,
256
+ versions: [
257
+ {
258
+ dsl: {
259
+ nodes: [
260
+ {
261
+ id: 'cron',
262
+ type: 'cron_trigger',
263
+ config: { cronExpressions: ['0 2 * * *'] },
264
+ },
265
+ ],
266
+ edges: [],
267
+ },
268
+ },
269
+ ],
270
+ });
271
+ temporal.getCronScheduleStatus.mockResolvedValueOnce('MISSING');
272
+
273
+ await expect(service.findOne(BigInt(42))).resolves.toMatchObject({
274
+ scheduleStatus: 'MISSING',
275
+ });
276
+ expect(temporal.getCronScheduleStatus).toHaveBeenCalledWith(42);
209
277
  });
210
278
  });
211
279
 
280
+ it('lists every callable version and marks the latest explicit version', async () => {
281
+ prisma.workflow.findMany.mockResolvedValueOnce([
282
+ {
283
+ id: BigInt(42),
284
+ name: '审批查询',
285
+ description: null,
286
+ versions: [
287
+ {
288
+ id: BigInt(102),
289
+ version: 2,
290
+ dsl: {
291
+ nodes: [
292
+ {
293
+ id: 'input',
294
+ type: 'callable_trigger',
295
+ config: {
296
+ inputParameters: [{ key: 'employeeId', required: true }],
297
+ },
298
+ },
299
+ ],
300
+ edges: [],
301
+ },
302
+ },
303
+ {
304
+ id: BigInt(101),
305
+ version: 1,
306
+ dsl: {
307
+ nodes: [
308
+ {
309
+ id: 'input',
310
+ type: 'callable_trigger',
311
+ config: {
312
+ inputParameters: [{ key: 'userId', required: true }],
313
+ },
314
+ },
315
+ ],
316
+ edges: [],
317
+ },
318
+ },
319
+ ],
320
+ },
321
+ ]);
322
+
323
+ await expect(service.findCallableWorkflows()).resolves.toEqual([
324
+ {
325
+ id: '42',
326
+ name: '审批查询',
327
+ description: null,
328
+ latestVersionId: '102',
329
+ versions: [
330
+ {
331
+ id: '102',
332
+ version: 2,
333
+ inputParameters: [{ key: 'employeeId', required: true }],
334
+ },
335
+ {
336
+ id: '101',
337
+ version: 1,
338
+ inputParameters: [{ key: 'userId', required: true }],
339
+ },
340
+ ],
341
+ },
342
+ ]);
343
+ expect(prisma.workflow.findMany).toHaveBeenCalledWith(
344
+ expect.objectContaining({
345
+ where: { isEnabled: true, versions: { some: {} } },
346
+ }),
347
+ );
348
+ });
349
+
212
350
  describe('publish — cron schedule sync', () => {
213
351
  const ID = BigInt(42);
214
352
  const mkWorkflow = (dsl: any) => ({ id: ID, dsl, status: 'DRAFT' });
215
353
 
354
+ it('rejects publication after deletion has started', async () => {
355
+ prisma.workflow.findUnique.mockResolvedValueOnce({
356
+ id: ID,
357
+ dsl: { nodes: [], edges: [] },
358
+ status: 'DELETING',
359
+ versions: [],
360
+ });
361
+
362
+ await expect(
363
+ service.publish(ID, { changeSummary: 'late publish' }, 'tester'),
364
+ ).rejects.toMatchObject({
365
+ response: expect.objectContaining({ code: 'WORKFLOW_DELETING' }),
366
+ });
367
+ expect(compiler.compile).not.toHaveBeenCalled();
368
+ });
369
+
216
370
  it('upserts cron schedule when DSL contains cron_trigger', async () => {
217
371
  const dsl = {
218
372
  nodes: [
219
373
  {
220
374
  id: 't1',
221
375
  type: 'cron_trigger',
222
- config: { cron: '0 2 * * *', timezone: 'Asia/Shanghai' },
376
+ config: {
377
+ cronExpressions: ['0 2 * * *', '0 14 * * *'],
378
+ timezone: 'Asia/Shanghai',
379
+ },
223
380
  },
224
381
  ],
225
382
  edges: [],
@@ -230,6 +387,9 @@ describe('WorkflowService — cron lifecycle hooks', () => {
230
387
  id: BigInt(1),
231
388
  version: 1,
232
389
  });
390
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
391
+ { id: BigInt(1), dsl },
392
+ ]);
233
393
  prisma.workflow.update.mockResolvedValueOnce({});
234
394
 
235
395
  await service.publish(ID, { changeSummary: 'init' }, 'tester');
@@ -237,20 +397,98 @@ describe('WorkflowService — cron lifecycle hooks', () => {
237
397
  expect(temporal.upsertCronSchedule).toHaveBeenCalledWith(
238
398
  expect.objectContaining({
239
399
  workflowId: 42,
240
- cron: '0 2 * * *',
400
+ cronExpressions: ['0 2 * * *', '0 14 * * *'],
241
401
  timezone: 'Asia/Shanghai',
242
402
  }),
243
403
  );
244
404
  expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
245
405
  });
246
406
 
247
- it('deletes cron schedule when published DSL has no cron_trigger', async () => {
407
+ it('rejects an invalid Cron expression or IANA timezone before publishing', async () => {
408
+ const dsl = {
409
+ nodes: [
410
+ {
411
+ id: 't1',
412
+ type: 'cron_trigger',
413
+ config: {
414
+ cronExpressions: ['0 9 * * *', 'not-a-cron'],
415
+ timezone: 'Asia/Shanghai',
416
+ },
417
+ },
418
+ ],
419
+ edges: [],
420
+ };
421
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
422
+
423
+ await expect(
424
+ service.publish(ID, { changeSummary: 'invalid schedule' }, 'tester'),
425
+ ).rejects.toMatchObject({
426
+ response: expect.objectContaining({ code: 'WORKFLOW_CRON_INVALID' }),
427
+ });
428
+ expect(prisma.workflowVersion.create).not.toHaveBeenCalled();
429
+ });
430
+
431
+ it('surfaces schedule sync failure and allows the same DSL to reconcile on retry', async () => {
432
+ const dsl = {
433
+ nodes: [
434
+ {
435
+ id: 't1',
436
+ type: 'cron_trigger',
437
+ config: {
438
+ cronExpressions: ['0 2 * * *'],
439
+ timezone: 'Asia/Shanghai',
440
+ },
441
+ },
442
+ ],
443
+ edges: [],
444
+ };
445
+ prisma.workflow.findUnique
446
+ .mockResolvedValueOnce(mkWorkflow(dsl))
447
+ .mockResolvedValueOnce({
448
+ id: ID,
449
+ dsl,
450
+ status: 'PUBLISHED',
451
+ versions: [{ id: BigInt(1), version: 1, dsl }],
452
+ });
453
+ prisma.workflowVersion.findFirst
454
+ .mockResolvedValueOnce(null)
455
+ .mockResolvedValue({ id: BigInt(1), version: 1, dsl });
456
+ prisma.workflowVersion.create.mockResolvedValueOnce({
457
+ id: BigInt(1),
458
+ version: 1,
459
+ });
460
+ prisma.workflowVersion.findMany.mockResolvedValue([
461
+ { id: BigInt(1), dsl },
462
+ ]);
463
+ prisma.workflow.update.mockResolvedValueOnce({});
464
+ temporal.upsertCronSchedule
465
+ .mockRejectedValueOnce(new Error('Temporal unavailable'))
466
+ .mockResolvedValueOnce(undefined);
467
+
468
+ await expect(
469
+ service.publish(ID, { changeSummary: 'schedule' }, 'tester'),
470
+ ).rejects.toThrow('Temporal unavailable');
471
+ await expect(
472
+ service.publish(ID, { changeSummary: 'schedule retry' }, 'tester'),
473
+ ).resolves.toMatchObject({ id: BigInt(1), version: 1 });
474
+ expect(prisma.workflowVersion.create).toHaveBeenCalledTimes(1);
475
+ expect(temporal.upsertCronSchedule).toHaveBeenCalledTimes(2);
476
+ });
477
+
478
+ it('does not touch Temporal scheduling when a Workflow has never used cron', async () => {
248
479
  const dsl = {
249
480
  nodes: [
250
481
  { id: 't1', type: 'manual_trigger', config: {} },
251
482
  { id: 'n1', type: 'http_request', config: {} },
252
483
  ],
253
- edges: [{ source: 't1', target: 'n1' }],
484
+ edges: [
485
+ {
486
+ id: 't1-n1',
487
+ source: 't1',
488
+ target: 'n1',
489
+ targetHandle: 'request-in',
490
+ },
491
+ ],
254
492
  };
255
493
  prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
256
494
  prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
@@ -262,6 +500,38 @@ describe('WorkflowService — cron lifecycle hooks', () => {
262
500
 
263
501
  await service.publish(ID, { changeSummary: 'init' }, 'tester');
264
502
 
503
+ expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
504
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
505
+ });
506
+
507
+ it('deletes an existing schedule when the latest publication removes cron', async () => {
508
+ const dsl = {
509
+ nodes: [{ id: 't1', type: 'manual_trigger', config: {} }],
510
+ edges: [],
511
+ };
512
+ const oldCronDsl = {
513
+ nodes: [
514
+ {
515
+ id: 'cron',
516
+ type: 'cron_trigger',
517
+ config: { cronExpressions: ['0 2 * * *'] },
518
+ },
519
+ ],
520
+ edges: [],
521
+ };
522
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
523
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
524
+ prisma.workflowVersion.create.mockResolvedValueOnce({
525
+ id: BigInt(9),
526
+ version: 2,
527
+ });
528
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
529
+ { id: BigInt(9), dsl },
530
+ { id: BigInt(8), dsl: oldCronDsl },
531
+ ]);
532
+
533
+ await service.publish(ID, { changeSummary: 'remove cron' }, 'tester');
534
+
265
535
  expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
266
536
  expect(temporal.deleteCronSchedule).toHaveBeenCalledWith(42);
267
537
  });
@@ -285,6 +555,32 @@ describe('WorkflowService — cron lifecycle hooks', () => {
285
555
  ).rejects.toThrow(/requires a secret/);
286
556
  });
287
557
 
558
+ it.each(['', 'foo/bar'])('rejects webhook path %j', async (path) => {
559
+ const dsl = {
560
+ nodes: [
561
+ {
562
+ id: 't1',
563
+ type: 'webhook_trigger',
564
+ config: {
565
+ path,
566
+ method: 'POST',
567
+ verificationMode: 'NONE',
568
+ },
569
+ },
570
+ ],
571
+ edges: [],
572
+ };
573
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
574
+
575
+ await expect(
576
+ service.publish(ID, { changeSummary: 'invalid webhook' }, 'tester'),
577
+ ).rejects.toMatchObject({
578
+ response: expect.objectContaining({
579
+ code: 'WORKFLOW_WEBHOOK_PATH_INVALID',
580
+ }),
581
+ });
582
+ });
583
+
288
584
  it('allows a direct webhook trigger to publish without an HMAC secret', async () => {
289
585
  const dsl = {
290
586
  nodes: [
@@ -314,7 +610,7 @@ describe('WorkflowService — cron lifecycle hooks', () => {
314
610
  ).resolves.toBeDefined();
315
611
  });
316
612
 
317
- it('rejects webhook path collision with another published workflow', async () => {
613
+ it('allows multiple published Workflows to subscribe to one webhook path', async () => {
318
614
  const dsl = {
319
615
  nodes: [
320
616
  {
@@ -350,9 +646,16 @@ describe('WorkflowService — cron lifecycle hooks', () => {
350
646
  },
351
647
  ]);
352
648
 
649
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
650
+ prisma.workflowVersion.create.mockResolvedValueOnce({
651
+ id: BigInt(1),
652
+ version: 1,
653
+ });
654
+ prisma.workflow.update.mockResolvedValueOnce({});
655
+
353
656
  await expect(
354
657
  service.publish(ID, { changeSummary: 'init' }, 'tester'),
355
- ).rejects.toThrow(/already used by workflow #999/);
658
+ ).resolves.toBeDefined();
356
659
  });
357
660
 
358
661
  it('proceeds when webhook path differs from existing one', async () => {
@@ -417,12 +720,14 @@ describe('WorkflowService — cron lifecycle hooks', () => {
417
720
  source: 'approval1',
418
721
  target: 'approved',
419
722
  sourceHandle: 'true',
723
+ targetHandle: 'data-in',
420
724
  },
421
725
  {
422
726
  id: 'e3',
423
727
  source: 'approval1',
424
728
  target: 'rejected',
425
729
  sourceHandle: 'false',
730
+ targetHandle: 'data-in',
426
731
  },
427
732
  ],
428
733
  };
@@ -439,23 +744,38 @@ describe('WorkflowService — cron lifecycle hooks', () => {
439
744
  ).resolves.toBeDefined();
440
745
  });
441
746
 
442
- it('rejects an output handle not declared by Workflow node metadata', async () => {
747
+ it('validates dynamic switch handles from the compiled effective config', async () => {
443
748
  const dsl = {
444
749
  nodes: [
445
750
  { id: 't1', type: 'manual_trigger', config: {} },
446
- { id: 'branch1', type: 'custom_branch', config: {} },
751
+ {
752
+ id: 'switch1',
753
+ type: 'switch',
754
+ config: { cases: [{ id: 'stale-case' }] },
755
+ configOverride: { cases: [{ id: 'effective-case' }] },
756
+ instanceRef: { instanceId: '7', instanceCode: 'SWITCH_TEMPLATE' },
757
+ },
447
758
  { id: 'next', type: 'code', config: {} },
448
759
  ],
449
760
  edges: [
450
- { id: 'e1', source: 't1', target: 'branch1' },
761
+ { id: 'e1', source: 't1', target: 'switch1' },
451
762
  {
452
763
  id: 'e2',
453
- source: 'branch1',
764
+ source: 'switch1',
454
765
  target: 'next',
455
- sourceHandle: 'unknown',
766
+ sourceHandle: 'effective-case',
767
+ targetHandle: 'data-in',
456
768
  },
457
769
  ],
458
770
  };
771
+ compiler.compile.mockImplementationOnce(async (draft: any) => ({
772
+ ...draft,
773
+ nodes: draft.nodes.map((node: any) => ({
774
+ ...node,
775
+ config: node.configOverride ?? node.config,
776
+ runtimeKey: node.type,
777
+ })),
778
+ }));
459
779
  prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
460
780
  prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
461
781
  prisma.workflowVersion.create.mockResolvedValueOnce({
@@ -466,33 +786,67 @@ describe('WorkflowService — cron lifecycle hooks', () => {
466
786
 
467
787
  await expect(
468
788
  service.publish(ID, { changeSummary: 'init' }, 'tester'),
469
- ).rejects.toMatchObject({
470
- status: 400,
471
- response: expect.objectContaining({
472
- code: 'DSL_HANDLE_VALIDATION',
473
- errors: expect.arrayContaining([
474
- expect.objectContaining({
475
- edgeId: 'e2',
476
- code: 'UNKNOWN_HANDLE',
477
- }),
478
- ]),
479
- }),
789
+ ).resolves.toBeDefined();
790
+ expect(compiler.compile).toHaveBeenCalledTimes(1);
791
+ });
792
+
793
+ it('accepts the canonical HTTP and Code main-flow ports', async () => {
794
+ const dsl = {
795
+ nodes: [
796
+ { id: 't1', type: 'manual_trigger', config: {} },
797
+ { id: 'request', type: 'http_request', config: {} },
798
+ { id: 'transform', type: 'code', config: {} },
799
+ { id: 'next', type: 'http_request', config: {} },
800
+ ],
801
+ edges: [
802
+ {
803
+ id: 'e1',
804
+ source: 't1',
805
+ target: 'request',
806
+ targetHandle: 'request-in',
807
+ },
808
+ {
809
+ id: 'e2',
810
+ source: 'request',
811
+ target: 'transform',
812
+ sourceHandle: 'response-out',
813
+ targetHandle: 'data-in',
814
+ },
815
+ {
816
+ id: 'e3',
817
+ source: 'transform',
818
+ target: 'next',
819
+ sourceHandle: 'result-out',
820
+ targetHandle: 'request-in',
821
+ },
822
+ ],
823
+ };
824
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
825
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
826
+ prisma.workflowVersion.create.mockResolvedValueOnce({
827
+ id: BigInt(1),
828
+ version: 1,
480
829
  });
830
+ prisma.workflow.update.mockResolvedValueOnce({});
831
+
832
+ await expect(
833
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
834
+ ).resolves.toBeDefined();
481
835
  });
482
836
 
483
- it('rejects with 400 BadRequestException when DSL has invalid sourceHandle', async () => {
484
- // if_else node has enumerated outputs ['true','false']; sending 'maybe'
485
- // must trigger the new sourceHandle validator and produce a 400 with a
486
- // structured error payload the frontend can use to highlight bad edges.
837
+ it('rejects a runtime main-flow port forged by a manual trigger', async () => {
487
838
  const dsl = {
488
839
  nodes: [
489
840
  { id: 't1', type: 'manual_trigger', config: {} },
490
- { id: 'if1', type: 'if_else', config: {} },
491
- { id: 'h1', type: 'http_request', config: {} },
841
+ { id: 'next', type: 'code', config: {} },
492
842
  ],
493
843
  edges: [
494
- { id: 'e1', source: 't1', target: 'if1' },
495
- { id: 'e2', source: 'if1', target: 'h1', sourceHandle: 'maybe' },
844
+ {
845
+ id: 'e1',
846
+ source: 't1',
847
+ target: 'next',
848
+ sourceHandle: 'response-out',
849
+ },
496
850
  ],
497
851
  };
498
852
  prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
@@ -504,24 +858,210 @@ describe('WorkflowService — cron lifecycle hooks', () => {
504
858
  response: expect.objectContaining({
505
859
  code: 'DSL_HANDLE_VALIDATION',
506
860
  errors: expect.arrayContaining([
507
- expect.objectContaining({
508
- edgeId: 'e2',
509
- code: 'UNKNOWN_HANDLE',
510
- }),
861
+ expect.objectContaining({ edgeId: 'e1', code: 'EXTRA_HANDLE' }),
511
862
  ]),
512
863
  }),
513
864
  });
514
-
515
- // Status flip must NOT have happened
516
- expect(prisma.workflow.update).not.toHaveBeenCalled();
517
865
  });
518
- });
519
-
520
- describe('toggleEnabled', () => {
521
- const ID = BigInt(42);
866
+
867
+ it('rejects an output handle not declared by Workflow node metadata', async () => {
868
+ const dsl = {
869
+ nodes: [
870
+ { id: 't1', type: 'manual_trigger', config: {} },
871
+ { id: 'branch1', type: 'custom_branch', config: {} },
872
+ { id: 'next', type: 'code', config: {} },
873
+ ],
874
+ edges: [
875
+ { id: 'e1', source: 't1', target: 'branch1' },
876
+ {
877
+ id: 'e2',
878
+ source: 'branch1',
879
+ target: 'next',
880
+ sourceHandle: 'unknown',
881
+ },
882
+ ],
883
+ };
884
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
885
+ prisma.workflowVersion.findFirst.mockResolvedValueOnce(null);
886
+ prisma.workflowVersion.create.mockResolvedValueOnce({
887
+ id: BigInt(1),
888
+ version: 1,
889
+ });
890
+ prisma.workflow.update.mockResolvedValueOnce({});
891
+
892
+ await expect(
893
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
894
+ ).rejects.toMatchObject({
895
+ status: 400,
896
+ response: expect.objectContaining({
897
+ code: 'DSL_HANDLE_VALIDATION',
898
+ errors: expect.arrayContaining([
899
+ expect.objectContaining({
900
+ edgeId: 'e2',
901
+ code: 'UNKNOWN_HANDLE',
902
+ }),
903
+ ]),
904
+ }),
905
+ });
906
+ });
907
+
908
+ it('rejects with 400 BadRequestException when DSL has invalid sourceHandle', async () => {
909
+ // if_else node has enumerated outputs ['true','false']; sending 'maybe'
910
+ // must trigger the new sourceHandle validator and produce a 400 with a
911
+ // structured error payload the frontend can use to highlight bad edges.
912
+ const dsl = {
913
+ nodes: [
914
+ { id: 't1', type: 'manual_trigger', config: {} },
915
+ { id: 'if1', type: 'if_else', config: {} },
916
+ { id: 'h1', type: 'http_request', config: {} },
917
+ ],
918
+ edges: [
919
+ { id: 'e1', source: 't1', target: 'if1' },
920
+ { id: 'e2', source: 'if1', target: 'h1', sourceHandle: 'maybe' },
921
+ ],
922
+ };
923
+ prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
924
+
925
+ await expect(
926
+ service.publish(ID, { changeSummary: 'init' }, 'tester'),
927
+ ).rejects.toMatchObject({
928
+ status: 400,
929
+ response: expect.objectContaining({
930
+ code: 'DSL_HANDLE_VALIDATION',
931
+ errors: expect.arrayContaining([
932
+ expect.objectContaining({
933
+ edgeId: 'e2',
934
+ code: 'UNKNOWN_HANDLE',
935
+ }),
936
+ ]),
937
+ }),
938
+ });
939
+
940
+ // Status flip must NOT have happened
941
+ expect(prisma.workflow.update).not.toHaveBeenCalled();
942
+ });
943
+ });
944
+
945
+ describe('toggleEnabled', () => {
946
+ const ID = BigInt(42);
947
+
948
+ it('does not require Temporal for a Workflow that has never used cron', async () => {
949
+ prisma.workflow.findUnique.mockResolvedValueOnce({
950
+ id: ID,
951
+ isEnabled: true,
952
+ versions: [
953
+ {
954
+ id: BigInt(9),
955
+ dsl: { nodes: [{ type: 'manual_trigger', config: {} }], edges: [] },
956
+ },
957
+ ],
958
+ });
959
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
960
+ {
961
+ id: BigInt(10),
962
+ dsl: { nodes: [{ type: 'manual_trigger', config: {} }], edges: [] },
963
+ },
964
+ ]);
965
+
966
+ await expect(service.toggleEnabled(ID, true)).resolves.toMatchObject({
967
+ id: ID,
968
+ isEnabled: true,
969
+ });
970
+
971
+ expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
972
+ expect(temporal.unpauseCronSchedule).not.toHaveBeenCalled();
973
+ expect(temporal.pauseCronSchedule).not.toHaveBeenCalled();
974
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
975
+ });
976
+
977
+ it('cleans up a historical cron schedule when the latest version is not cron', async () => {
978
+ prisma.workflow.findUnique.mockResolvedValueOnce({
979
+ id: ID,
980
+ versions: [
981
+ {
982
+ id: BigInt(10),
983
+ dsl: { nodes: [{ type: 'manual_trigger', config: {} }], edges: [] },
984
+ },
985
+ {
986
+ id: BigInt(9),
987
+ dsl: {
988
+ nodes: [
989
+ {
990
+ type: 'cron_trigger',
991
+ config: { cronExpressions: ['0 2 * * *'] },
992
+ },
993
+ ],
994
+ edges: [],
995
+ },
996
+ },
997
+ ],
998
+ });
999
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1000
+ {
1001
+ id: BigInt(10),
1002
+ dsl: { nodes: [{ type: 'manual_trigger', config: {} }], edges: [] },
1003
+ },
1004
+ {
1005
+ id: BigInt(9),
1006
+ dsl: {
1007
+ nodes: [
1008
+ {
1009
+ type: 'cron_trigger',
1010
+ config: { cronExpressions: ['0 2 * * *'] },
1011
+ },
1012
+ ],
1013
+ edges: [],
1014
+ },
1015
+ },
1016
+ ]);
1017
+
1018
+ await service.toggleEnabled(ID, false);
1019
+
1020
+ expect(temporal.deleteCronSchedule).toHaveBeenCalledWith(42);
1021
+ expect(temporal.pauseCronSchedule).not.toHaveBeenCalled();
1022
+ });
1023
+
1024
+ it('rejects changes after deletion has started', async () => {
1025
+ prisma.workflow.updateMany.mockResolvedValueOnce({ count: 0 });
1026
+ prisma.workflow.findUnique.mockResolvedValueOnce({
1027
+ id: ID,
1028
+ status: 'DELETING',
1029
+ });
1030
+ await expect(service.toggleEnabled(ID, true)).rejects.toMatchObject({
1031
+ response: expect.objectContaining({ code: 'WORKFLOW_DELETING' }),
1032
+ });
1033
+ expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
1034
+ });
522
1035
 
523
1036
  it('pauses cron schedule when disabling', async () => {
524
- prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1037
+ prisma.workflow.findUnique.mockResolvedValueOnce({
1038
+ id: ID,
1039
+ versions: [
1040
+ {
1041
+ dsl: {
1042
+ nodes: [
1043
+ {
1044
+ type: 'cron_trigger',
1045
+ config: { cronExpressions: ['0 2 * * *'] },
1046
+ },
1047
+ ],
1048
+ },
1049
+ },
1050
+ ],
1051
+ });
1052
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1053
+ {
1054
+ id: BigInt(9),
1055
+ dsl: {
1056
+ nodes: [
1057
+ {
1058
+ type: 'cron_trigger',
1059
+ config: { cronExpressions: ['0 2 * * *'] },
1060
+ },
1061
+ ],
1062
+ },
1063
+ },
1064
+ ]);
525
1065
  prisma.workflow.update.mockResolvedValueOnce({});
526
1066
 
527
1067
  await service.toggleEnabled(ID, false);
@@ -531,27 +1071,297 @@ describe('WorkflowService — cron lifecycle hooks', () => {
531
1071
  });
532
1072
 
533
1073
  it('unpauses cron schedule when enabling', async () => {
534
- prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1074
+ let transactionDepth = 0;
1075
+ const runTransaction = prisma.$transaction.getMockImplementation();
1076
+ prisma.$transaction.mockImplementation(async (callback: any) => {
1077
+ transactionDepth++;
1078
+ try {
1079
+ return await runTransaction(callback);
1080
+ } finally {
1081
+ transactionDepth--;
1082
+ }
1083
+ });
1084
+ temporal.upsertCronSchedule.mockImplementation(async () => {
1085
+ expect(transactionDepth).toBe(0);
1086
+ });
1087
+ prisma.workflow.findUnique.mockResolvedValueOnce({
1088
+ id: ID,
1089
+ versions: [
1090
+ {
1091
+ id: BigInt(9),
1092
+ dsl: {
1093
+ nodes: [
1094
+ {
1095
+ id: 'cron',
1096
+ type: 'cron_trigger',
1097
+ config: {
1098
+ cronExpressions: ['0 2 * * *'],
1099
+ timezone: 'Asia/Shanghai',
1100
+ },
1101
+ },
1102
+ ],
1103
+ edges: [],
1104
+ },
1105
+ },
1106
+ ],
1107
+ });
1108
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1109
+ {
1110
+ id: BigInt(9),
1111
+ dsl: {
1112
+ nodes: [
1113
+ {
1114
+ id: 'cron',
1115
+ type: 'cron_trigger',
1116
+ config: {
1117
+ cronExpressions: ['0 2 * * *'],
1118
+ timezone: 'Asia/Shanghai',
1119
+ },
1120
+ },
1121
+ ],
1122
+ edges: [],
1123
+ },
1124
+ },
1125
+ ]);
535
1126
  prisma.workflow.update.mockResolvedValueOnce({});
536
1127
 
537
1128
  await service.toggleEnabled(ID, true);
538
1129
 
1130
+ expect(temporal.upsertCronSchedule).toHaveBeenCalled();
539
1131
  expect(temporal.unpauseCronSchedule).toHaveBeenCalledWith(42);
540
1132
  expect(temporal.pauseCronSchedule).not.toHaveBeenCalled();
541
1133
  });
542
1134
 
543
- it('does not throw if schedule operation fails', async () => {
544
- prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1135
+ it('surfaces a schedule operation failure', async () => {
1136
+ prisma.workflow.findUnique.mockResolvedValueOnce({
1137
+ id: ID,
1138
+ versions: [
1139
+ {
1140
+ dsl: {
1141
+ nodes: [
1142
+ {
1143
+ type: 'cron_trigger',
1144
+ config: { cronExpressions: ['0 2 * * *'] },
1145
+ },
1146
+ ],
1147
+ },
1148
+ },
1149
+ ],
1150
+ });
1151
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1152
+ {
1153
+ id: BigInt(9),
1154
+ dsl: {
1155
+ nodes: [
1156
+ {
1157
+ type: 'cron_trigger',
1158
+ config: { cronExpressions: ['0 2 * * *'] },
1159
+ },
1160
+ ],
1161
+ },
1162
+ },
1163
+ ]);
545
1164
  prisma.workflow.update.mockResolvedValueOnce({});
546
1165
  temporal.pauseCronSchedule.mockRejectedValueOnce(new Error('boom'));
547
1166
 
548
- await expect(service.toggleEnabled(ID, false)).resolves.toBeDefined();
1167
+ await expect(service.toggleEnabled(ID, false)).rejects.toThrow('boom');
1168
+ });
1169
+ });
1170
+
1171
+ describe('cron reconciliation', () => {
1172
+ it('recreates and unpauses an enabled cron schedule from the latest published version', async () => {
1173
+ const dsl = {
1174
+ nodes: [
1175
+ {
1176
+ id: 'cron',
1177
+ type: 'cron_trigger',
1178
+ config: {
1179
+ cronExpressions: ['0 2 * * *'],
1180
+ timezone: 'Asia/Shanghai',
1181
+ },
1182
+ },
1183
+ ],
1184
+ edges: [],
1185
+ };
1186
+ prisma.workflow.findMany.mockResolvedValueOnce([
1187
+ {
1188
+ id: BigInt(42),
1189
+ isEnabled: true,
1190
+ versions: [{ id: BigInt(9), dsl }],
1191
+ },
1192
+ ]);
1193
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1194
+ { id: BigInt(9), dsl },
1195
+ ]);
1196
+
1197
+ await service.reconcileCronSchedules();
1198
+
1199
+ expect(temporal.upsertCronSchedule).toHaveBeenCalledWith(
1200
+ expect.objectContaining({
1201
+ workflowId: 42,
1202
+ versionId: 9,
1203
+ dsl,
1204
+ cronExpressions: ['0 2 * * *'],
1205
+ timezone: 'Asia/Shanghai',
1206
+ }),
1207
+ );
1208
+ expect(temporal.unpauseCronSchedule).toHaveBeenCalledWith(42);
1209
+ });
1210
+
1211
+ it('re-reads the latest version under lock before restoring a schedule', async () => {
1212
+ const staleCronDsl = {
1213
+ nodes: [
1214
+ {
1215
+ id: 'cron',
1216
+ type: 'cron_trigger',
1217
+ config: {
1218
+ cronExpressions: ['0 2 * * *'],
1219
+ timezone: 'Asia/Shanghai',
1220
+ },
1221
+ },
1222
+ ],
1223
+ edges: [],
1224
+ };
1225
+ const latestManualDsl = {
1226
+ nodes: [{ id: 'manual', type: 'manual_trigger', config: {} }],
1227
+ edges: [],
1228
+ };
1229
+ prisma.workflow.findMany.mockResolvedValueOnce([
1230
+ {
1231
+ id: BigInt(42),
1232
+ isEnabled: true,
1233
+ versions: [{ id: BigInt(9), dsl: staleCronDsl }],
1234
+ },
1235
+ ]);
1236
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1237
+ { id: BigInt(10), dsl: latestManualDsl },
1238
+ { id: BigInt(9), dsl: staleCronDsl },
1239
+ ]);
1240
+
1241
+ await service.reconcileCronSchedules();
1242
+
1243
+ expect(temporal.deleteCronSchedule).toHaveBeenCalledWith(42);
1244
+ expect(temporal.upsertCronSchedule).not.toHaveBeenCalled();
1245
+ expect(temporal.unpauseCronSchedule).not.toHaveBeenCalled();
549
1246
  });
550
1247
  });
551
1248
 
552
1249
  describe('remove', () => {
553
1250
  const ID = BigInt(42);
554
1251
 
1252
+ it('rejects deletion while another Workflow still calls it', async () => {
1253
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1254
+ prisma.workflow.findMany.mockResolvedValueOnce([
1255
+ {
1256
+ id: BigInt(7),
1257
+ name: '采购审批',
1258
+ dsl: {
1259
+ nodes: [
1260
+ {
1261
+ type: 'sub_workflow',
1262
+ config: { workflowId: '42', workflowVersionId: '101' },
1263
+ },
1264
+ ],
1265
+ },
1266
+ versions: [],
1267
+ },
1268
+ ]);
1269
+
1270
+ await expect(service.remove(ID)).rejects.toMatchObject({
1271
+ response: expect.objectContaining({
1272
+ code: 'WORKFLOW_IS_REFERENCED',
1273
+ callers: [
1274
+ expect.objectContaining({
1275
+ workflowId: '7',
1276
+ workflowName: '采购审批',
1277
+ source: 'DRAFT',
1278
+ }),
1279
+ ],
1280
+ }),
1281
+ });
1282
+ expect(prisma.workflow.update).not.toHaveBeenCalled();
1283
+ expect(prisma.workflow.delete).not.toHaveBeenCalled();
1284
+ });
1285
+
1286
+ it('uses a caller draft config override when checking deletion', async () => {
1287
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1288
+ prisma.workflow.findMany.mockResolvedValueOnce([
1289
+ {
1290
+ id: BigInt(7),
1291
+ name: '采购审批',
1292
+ dsl: {
1293
+ nodes: [
1294
+ {
1295
+ type: 'sub_workflow',
1296
+ config: { workflowId: '99', workflowVersionId: '201' },
1297
+ configOverride: {
1298
+ workflowId: '42',
1299
+ workflowVersionId: '101',
1300
+ },
1301
+ },
1302
+ ],
1303
+ },
1304
+ versions: [],
1305
+ },
1306
+ ]);
1307
+
1308
+ await expect(service.remove(ID)).rejects.toMatchObject({
1309
+ response: expect.objectContaining({ code: 'WORKFLOW_IS_REFERENCED' }),
1310
+ });
1311
+ expect(prisma.workflow.delete).not.toHaveBeenCalled();
1312
+ });
1313
+
1314
+ it('uses a shared caller node instance config when checking deletion', async () => {
1315
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1316
+ prisma.workflow.findMany.mockResolvedValueOnce([
1317
+ {
1318
+ id: BigInt(7),
1319
+ name: '采购审批',
1320
+ dsl: {
1321
+ nodes: [
1322
+ {
1323
+ type: 'sub_workflow',
1324
+ config: {},
1325
+ instanceRef: {
1326
+ instanceId: '12',
1327
+ instanceCode: 'CALL_PURCHASE',
1328
+ },
1329
+ },
1330
+ ],
1331
+ },
1332
+ versions: [],
1333
+ },
1334
+ ]);
1335
+ prisma.workflowNodeInstance.findMany.mockResolvedValueOnce([
1336
+ {
1337
+ id: BigInt(12),
1338
+ config: { workflowId: '42', workflowVersionId: '101' },
1339
+ },
1340
+ ]);
1341
+
1342
+ await expect(service.remove(ID)).rejects.toMatchObject({
1343
+ response: expect.objectContaining({ code: 'WORKFLOW_IS_REFERENCED' }),
1344
+ });
1345
+ expect(prisma.workflowNodeInstance.findMany).toHaveBeenCalledWith({
1346
+ where: { id: { in: [BigInt(12)] } },
1347
+ select: { id: true, config: true },
1348
+ });
1349
+ expect(prisma.workflow.delete).not.toHaveBeenCalled();
1350
+ });
1351
+
1352
+ it('deletes a never-cron Workflow without calling Temporal', async () => {
1353
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1354
+ prisma.workflow.delete.mockResolvedValueOnce({ id: ID });
1355
+ temporal.deleteCronSchedule.mockRejectedValueOnce(
1356
+ new Error('Temporal unavailable'),
1357
+ );
1358
+
1359
+ await expect(service.remove(ID)).resolves.toEqual({ id: ID });
1360
+
1361
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
1362
+ expect(prisma.workflow.delete).toHaveBeenCalled();
1363
+ });
1364
+
555
1365
  it('deletes external interaction history before deleting the workflow', async () => {
556
1366
  const rows = {
557
1367
  interactionEvents: 1,
@@ -567,8 +1377,9 @@ describe('WorkflowService — cron lifecycle hooks', () => {
567
1377
  });
568
1378
  prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
569
1379
  prisma.workflow.delete.mockImplementationOnce(deleteWorkflow);
570
- prisma.$transaction.mockImplementationOnce(async (callback: any) =>
1380
+ prisma.$transaction.mockImplementation(async (callback: any) =>
571
1381
  callback({
1382
+ workflowInstance: prisma.workflowInstance,
572
1383
  externalInteraction: {
573
1384
  findMany: jest.fn(async () => [{ id: BigInt(100) }]),
574
1385
  deleteMany: jest.fn(async () => {
@@ -587,7 +1398,13 @@ describe('WorkflowService — cron lifecycle hooks', () => {
587
1398
  rows.signalOutboxes = 0;
588
1399
  }),
589
1400
  },
590
- workflow: { delete: deleteWorkflow },
1401
+ workflow: {
1402
+ update: prisma.workflow.update,
1403
+ delete: deleteWorkflow,
1404
+ },
1405
+ workflowVersion: {
1406
+ findMany: prisma.workflowVersion.findMany,
1407
+ },
591
1408
  }),
592
1409
  );
593
1410
 
@@ -600,8 +1417,33 @@ describe('WorkflowService — cron lifecycle hooks', () => {
600
1417
  });
601
1418
  });
602
1419
 
1420
+ it('rejects deletion while an instance is still active', async () => {
1421
+ prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1422
+ prisma.workflowInstance.count.mockResolvedValueOnce(1);
1423
+
1424
+ await expect(service.remove(ID)).rejects.toMatchObject({
1425
+ response: expect.objectContaining({
1426
+ code: 'WORKFLOW_HAS_ACTIVE_INSTANCES',
1427
+ }),
1428
+ });
1429
+ expect(temporal.deleteCronSchedule).not.toHaveBeenCalled();
1430
+ expect(prisma.workflow.delete).not.toHaveBeenCalled();
1431
+ });
1432
+
603
1433
  it('deletes cron schedule before deleting workflow', async () => {
604
1434
  prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1435
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1436
+ {
1437
+ dsl: {
1438
+ nodes: [
1439
+ {
1440
+ type: 'cron_trigger',
1441
+ config: { cronExpressions: ['0 2 * * *'] },
1442
+ },
1443
+ ],
1444
+ },
1445
+ },
1446
+ ]);
605
1447
  prisma.workflow.delete.mockResolvedValueOnce({});
606
1448
 
607
1449
  await service.remove(ID);
@@ -610,14 +1452,30 @@ describe('WorkflowService — cron lifecycle hooks', () => {
610
1452
  expect(prisma.workflow.delete).toHaveBeenCalled();
611
1453
  });
612
1454
 
613
- it('still deletes the workflow even if schedule deletion fails', async () => {
1455
+ it('keeps the disabled workflow when schedule deletion fails', async () => {
614
1456
  prisma.workflow.findUnique.mockResolvedValueOnce({ id: ID });
1457
+ prisma.workflowVersion.findMany.mockResolvedValueOnce([
1458
+ {
1459
+ dsl: {
1460
+ nodes: [
1461
+ {
1462
+ type: 'cron_trigger',
1463
+ config: { cronExpressions: ['0 2 * * *'] },
1464
+ },
1465
+ ],
1466
+ },
1467
+ },
1468
+ ]);
615
1469
  temporal.deleteCronSchedule.mockRejectedValueOnce(new Error('boom'));
616
1470
  prisma.workflow.delete.mockResolvedValueOnce({});
617
1471
 
618
- await service.remove(ID);
1472
+ await expect(service.remove(ID)).rejects.toThrow('boom');
619
1473
 
620
- expect(prisma.workflow.delete).toHaveBeenCalled();
1474
+ expect(prisma.workflow.update).toHaveBeenCalledWith({
1475
+ where: { id: ID },
1476
+ data: { isEnabled: false, status: 'DELETING' },
1477
+ });
1478
+ expect(prisma.workflow.delete).not.toHaveBeenCalled();
621
1479
  });
622
1480
  });
623
1481
  });
@@ -636,12 +1494,19 @@ describe('WorkflowService — receiveWebhook', () => {
636
1494
  workflowVersion: { findFirst: jest.fn() },
637
1495
  workflowInstance: {
638
1496
  create: jest.fn(),
1497
+ findUnique: jest.fn(),
639
1498
  update: jest.fn(),
1499
+ updateMany: jest.fn(),
640
1500
  },
641
1501
  };
1502
+ prisma.$transaction = jest.fn(async (callback: any) =>
1503
+ callback({
1504
+ $queryRaw: jest.fn().mockResolvedValue([{ isEnabled: true }]),
1505
+ workflowInstance: prisma.workflowInstance,
1506
+ }),
1507
+ );
642
1508
  temporal = {
643
1509
  startWorkflow: jest.fn(async () => ({ temporalRunId: 'run-1' })),
644
- waitForWorkflowResult: jest.fn(),
645
1510
  };
646
1511
 
647
1512
  const module: TestingModule = await Test.createTestingModule({
@@ -665,6 +1530,7 @@ describe('WorkflowService — receiveWebhook', () => {
665
1530
  const METHOD = 'POST';
666
1531
  const BODY = '{"orderId":42}';
667
1532
  const RAW = Buffer.from(BODY);
1533
+ const WEBHOOK_HEADERS = { 'idempotency-key': 'webhook-event-1' };
668
1534
 
669
1535
  const nowSec = () => Math.floor(Date.now() / 1000).toString();
670
1536
 
@@ -673,8 +1539,6 @@ describe('WorkflowService — receiveWebhook', () => {
673
1539
  secret = SECRET,
674
1540
  path = PATH,
675
1541
  verificationMode?: 'HMAC' | 'NONE',
676
- responseMode?: 'IMMEDIATE' | 'WAIT_FOR_RESULT',
677
- responseTimeoutSeconds?: number,
678
1542
  ) => ({
679
1543
  id: BigInt(id),
680
1544
  status: 'PUBLISHED',
@@ -694,8 +1558,6 @@ describe('WorkflowService — receiveWebhook', () => {
694
1558
  method: METHOD,
695
1559
  secret,
696
1560
  ...(verificationMode && { verificationMode }),
697
- ...(responseMode && { responseMode }),
698
- ...(responseTimeoutSeconds && { responseTimeoutSeconds }),
699
1561
  },
700
1562
  },
701
1563
  ],
@@ -724,8 +1586,20 @@ describe('WorkflowService — receiveWebhook', () => {
724
1586
  version: 1,
725
1587
  dsl: { nodes: [], edges: [] },
726
1588
  }));
727
- prisma.workflowInstance.create.mockResolvedValueOnce({ id: BigInt(7777) });
728
- prisma.workflowInstance.update.mockResolvedValueOnce({});
1589
+ let createdInstance: any;
1590
+ prisma.workflowInstance.create.mockImplementation(({ data }: any) => {
1591
+ createdInstance = {
1592
+ id: BigInt(7777),
1593
+ ...data,
1594
+ temporalRunId: null,
1595
+ };
1596
+ return createdInstance;
1597
+ });
1598
+ prisma.workflowInstance.updateMany.mockResolvedValue({ count: 1 });
1599
+ prisma.workflowInstance.findUnique.mockImplementation(() => ({
1600
+ ...createdInstance,
1601
+ temporalRunId: 'wf-1-7777',
1602
+ }));
729
1603
  };
730
1604
 
731
1605
  it('returns 401 (UnauthorizedException) when timestamp is missing', async () => {
@@ -739,7 +1613,7 @@ describe('WorkflowService — receiveWebhook', () => {
739
1613
  timestampHeader: undefined,
740
1614
  rawBody: RAW,
741
1615
  body: { orderId: 42 },
742
- headers: {},
1616
+ headers: WEBHOOK_HEADERS,
743
1617
  query: {},
744
1618
  }),
745
1619
  ).rejects.toBeInstanceOf(UnauthorizedException);
@@ -757,7 +1631,7 @@ describe('WorkflowService — receiveWebhook', () => {
757
1631
  timestampHeader: oldTs,
758
1632
  rawBody: RAW,
759
1633
  body: {},
760
- headers: {},
1634
+ headers: WEBHOOK_HEADERS,
761
1635
  query: {},
762
1636
  }),
763
1637
  ).rejects.toBeInstanceOf(UnauthorizedException);
@@ -775,7 +1649,7 @@ describe('WorkflowService — receiveWebhook', () => {
775
1649
  timestampHeader: ts,
776
1650
  rawBody: RAW,
777
1651
  body: {},
778
- headers: {},
1652
+ headers: WEBHOOK_HEADERS,
779
1653
  query: {},
780
1654
  }),
781
1655
  ).rejects.toBeInstanceOf(NotFoundException);
@@ -792,7 +1666,7 @@ describe('WorkflowService — receiveWebhook', () => {
792
1666
  timestampHeader: ts,
793
1667
  rawBody: RAW,
794
1668
  body: {},
795
- headers: {},
1669
+ headers: WEBHOOK_HEADERS,
796
1670
  query: {},
797
1671
  }),
798
1672
  ).rejects.toBeInstanceOf(UnauthorizedException);
@@ -812,7 +1686,7 @@ describe('WorkflowService — receiveWebhook', () => {
812
1686
  timestampHeader: ts,
813
1687
  rawBody: RAW,
814
1688
  body: { orderId: 42 },
815
- headers: { 'x-workflow-signature': sig },
1689
+ headers: { ...WEBHOOK_HEADERS, 'x-workflow-signature': sig },
816
1690
  query: { foo: 'bar' },
817
1691
  });
818
1692
 
@@ -841,7 +1715,7 @@ describe('WorkflowService — receiveWebhook', () => {
841
1715
  timestampHeader: undefined,
842
1716
  rawBody: RAW,
843
1717
  body: { id: 'myoa-work-item-1' },
844
- headers: { 'content-type': 'text/json' },
1718
+ headers: { ...WEBHOOK_HEADERS, 'content-type': 'text/json' },
845
1719
  query: {},
846
1720
  });
847
1721
 
@@ -854,126 +1728,6 @@ describe('WorkflowService — receiveWebhook', () => {
854
1728
  });
855
1729
  });
856
1730
 
857
- it('waits for the Workflow return result when the trigger uses function mode', async () => {
858
- prisma.workflow.findMany.mockResolvedValueOnce([
859
- mkPublishedWorkflow(1, '', PATH, 'NONE', 'WAIT_FOR_RESULT', 45),
860
- ]);
861
- mkExecuteSetup();
862
- temporal.waitForWorkflowResult.mockResolvedValueOnce({
863
- kind: 'COMPLETED',
864
- result: {
865
- status: 'COMPLETED',
866
- output: {
867
- ticketId: 'T-42',
868
- accepted: true,
869
- },
870
- httpResponse: {
871
- statusCode: 201,
872
- headers: {
873
- 'x-workflow-result': 'created',
874
- },
875
- },
876
- },
877
- });
878
-
879
- const result = await service.receiveWebhook({
880
- path: PATH,
881
- method: METHOD,
882
- signatureHeader: undefined,
883
- timestampHeader: undefined,
884
- rawBody: RAW,
885
- body: { orderId: 42 },
886
- headers: {},
887
- query: {},
888
- });
889
-
890
- expect(result).toEqual({
891
- instanceIds: ['7777'],
892
- response: {
893
- statusCode: 201,
894
- headers: {
895
- 'x-workflow-result': 'created',
896
- },
897
- body: {
898
- ticketId: 'T-42',
899
- accepted: true,
900
- },
901
- },
902
- });
903
- expect(temporal.waitForWorkflowResult).toHaveBeenCalledWith(
904
- 'run-1',
905
- 45_000,
906
- );
907
- });
908
-
909
- it('returns a gateway timeout without cancelling a still-running Workflow', async () => {
910
- prisma.workflow.findMany.mockResolvedValueOnce([
911
- mkPublishedWorkflow(1, '', PATH, 'NONE', 'WAIT_FOR_RESULT', 60),
912
- ]);
913
- mkExecuteSetup();
914
- temporal.waitForWorkflowResult.mockResolvedValueOnce({
915
- kind: 'TIMED_OUT',
916
- });
917
-
918
- const result = await service.receiveWebhook({
919
- path: PATH,
920
- method: METHOD,
921
- signatureHeader: undefined,
922
- timestampHeader: undefined,
923
- rawBody: RAW,
924
- body: {},
925
- headers: {},
926
- query: {},
927
- });
928
-
929
- expect(result).toEqual({
930
- instanceIds: ['7777'],
931
- response: {
932
- statusCode: 504,
933
- headers: {},
934
- body: {
935
- code: 'WORKFLOW_RESPONSE_TIMEOUT',
936
- message: 'Workflow did not return a result within 60 seconds',
937
- instanceId: '7777',
938
- },
939
- },
940
- });
941
- });
942
-
943
- it('returns an internal error response when the Workflow execution fails', async () => {
944
- prisma.workflow.findMany.mockResolvedValueOnce([
945
- mkPublishedWorkflow(1, '', PATH, 'NONE', 'WAIT_FOR_RESULT'),
946
- ]);
947
- mkExecuteSetup();
948
- temporal.waitForWorkflowResult.mockRejectedValueOnce(
949
- new Error('private Temporal failure'),
950
- );
951
-
952
- const result = await service.receiveWebhook({
953
- path: PATH,
954
- method: METHOD,
955
- signatureHeader: undefined,
956
- timestampHeader: undefined,
957
- rawBody: RAW,
958
- body: {},
959
- headers: {},
960
- query: {},
961
- });
962
-
963
- expect(result).toEqual({
964
- instanceIds: ['7777'],
965
- response: {
966
- statusCode: 500,
967
- headers: {},
968
- body: {
969
- code: 'WORKFLOW_EXECUTION_FAILED',
970
- message: 'Workflow execution failed',
971
- instanceId: '7777',
972
- },
973
- },
974
- });
975
- });
976
-
977
1731
  it('rejects with 401 when one of multiple matches has a different secret', async () => {
978
1732
  // Strict mode: every match must verify against the same signature
979
1733
  prisma.workflow.findMany.mockResolvedValueOnce([
@@ -1002,31 +1756,35 @@ describe('WorkflowService — approveInstance', () => {
1002
1756
  let service: WorkflowService;
1003
1757
  let prisma: any;
1004
1758
  let temporal: any;
1759
+ let humanApproval: any;
1005
1760
 
1006
1761
  beforeEach(async () => {
1007
1762
  prisma = {
1008
- workflowInstance: { findUnique: jest.fn() },
1009
- workflowNodeExecution: { findUnique: jest.fn() },
1763
+ approvalRequest: { findUnique: jest.fn() },
1010
1764
  };
1011
1765
  temporal = { signalWorkflow: jest.fn() };
1766
+ humanApproval = { submitDecision: jest.fn() };
1012
1767
  const module: TestingModule = await Test.createTestingModule({
1013
1768
  providers: [
1014
1769
  WorkflowService,
1015
1770
  { provide: PrismaService, useValue: prisma },
1016
1771
  { provide: TemporalService, useValue: temporal },
1772
+ { provide: HumanApprovalService, useValue: humanApproval },
1773
+ { provide: WorkflowCompilerService, useValue: {} },
1017
1774
  ],
1018
1775
  }).compile();
1019
1776
  service = module.get(WorkflowService);
1020
1777
  });
1021
1778
 
1022
1779
  it('rejects when caller is not in the resolved approver list', async () => {
1023
- prisma.workflowInstance.findUnique.mockResolvedValue({
1024
- id: BigInt(1),
1025
- temporalRunId: 'run-1',
1026
- });
1027
- prisma.workflowNodeExecution.findUnique.mockResolvedValue({
1028
- input: { approvers: ['alice'] },
1780
+ prisma.approvalRequest.findUnique.mockResolvedValue({
1781
+ id: BigInt(7),
1782
+ status: 'PENDING',
1029
1783
  });
1784
+ humanApproval.submitDecision.mockRejectedValue(
1785
+ new Error('not an approver'),
1786
+ );
1787
+
1030
1788
  await expect(
1031
1789
  service.approveInstance(
1032
1790
  BigInt(1),
@@ -1036,36 +1794,38 @@ describe('WorkflowService — approveInstance', () => {
1036
1794
  ),
1037
1795
  ).rejects.toThrow('not an approver');
1038
1796
  expect(temporal.signalWorkflow).not.toHaveBeenCalled();
1797
+ expect(humanApproval.submitDecision).toHaveBeenCalledWith(
1798
+ BigInt(7),
1799
+ { decision: 'APPROVE', comment: undefined },
1800
+ { userId: 'bob' },
1801
+ );
1039
1802
  });
1040
1803
 
1041
- it('forwards signal with real approverId when caller is authorized', async () => {
1042
- prisma.workflowInstance.findUnique.mockResolvedValue({
1043
- id: BigInt(1),
1044
- temporalRunId: 'run-1',
1045
- });
1046
- prisma.workflowNodeExecution.findUnique.mockResolvedValue({
1047
- input: { approvers: ['alice'] },
1804
+ it('submits the real approver identity through the approval service', async () => {
1805
+ prisma.approvalRequest.findUnique.mockResolvedValue({
1806
+ id: BigInt(7),
1807
+ status: 'PENDING',
1048
1808
  });
1049
- await service.approveInstance(
1050
- BigInt(1),
1051
- { nodeId: 'n1', approved: true, comment: 'ok' },
1052
- 'alice',
1053
- temporal,
1809
+ humanApproval.submitDecision.mockResolvedValue({ status: 'APPROVED' });
1810
+
1811
+ await expect(
1812
+ service.approveInstance(
1813
+ BigInt(1),
1814
+ { nodeId: 'n1', approved: true, comment: 'ok' },
1815
+ 'alice',
1816
+ temporal,
1817
+ ),
1818
+ ).resolves.toEqual({ status: 'APPROVED' });
1819
+ expect(humanApproval.submitDecision).toHaveBeenCalledWith(
1820
+ BigInt(7),
1821
+ { decision: 'APPROVE', comment: 'ok' },
1822
+ { userId: 'alice' },
1054
1823
  );
1055
- expect(temporal.signalWorkflow).toHaveBeenCalledWith('run-1', 'approval', {
1056
- nodeId: 'n1',
1057
- approved: true,
1058
- approvedBy: 'alice',
1059
- comment: 'ok',
1060
- });
1061
1824
  });
1062
1825
 
1063
- it('throws NotFoundException when no node execution exists for nodeId', async () => {
1064
- prisma.workflowInstance.findUnique.mockResolvedValue({
1065
- id: BigInt(1),
1066
- temporalRunId: 'run-1',
1067
- });
1068
- prisma.workflowNodeExecution.findUnique.mockResolvedValue(null);
1826
+ it('throws NotFoundException when no open approval request exists', async () => {
1827
+ prisma.approvalRequest.findUnique.mockResolvedValue(null);
1828
+
1069
1829
  await expect(
1070
1830
  service.approveInstance(
1071
1831
  BigInt(1),
@@ -1073,6 +1833,6 @@ describe('WorkflowService — approveInstance', () => {
1073
1833
  'alice',
1074
1834
  temporal,
1075
1835
  ),
1076
- ).rejects.toThrow('Node execution not found');
1836
+ ).rejects.toThrow('Open approval request not found');
1077
1837
  });
1078
1838
  });