Ver código fonte

fix: fail a loop body node that pauses instead of orphaning the approval

The node-side guard in wait-for-approval only catches the case where the
node is wired directly off a loop's loop output. A node fed through
another body node (Loop -> Code -> Wait for Approval) cannot tell it is
inside a loop, so the pause marker slipped through, the node reported
completed, and the loop moved on with the approval unresumable and no
error anywhere. executeLoopBody now detects a _pause marker on a
completed body result and fails that node instead, since the engine is
the only place that can know for certain.

Also stops logging the approval token to the runner log, and adds the
missing fields key to outputSchema.
fszontagh 1 mês atrás
pai
commit
7261ac60fd

+ 3 - 2
nodes/core/wait-for-approval.js

@@ -53,7 +53,8 @@ const outputSchema = {
         expiresAt: { type: 'number', description: 'Milliseconds since the epoch, 0 when it never expires' },
         approved: { type: 'boolean', description: 'Set on the answer, after the execution resumes' },
         answeredBy: { type: 'string', description: 'Set on the answer' },
-        answeredAt: { type: 'number', description: 'Set on the answer' }
+        answeredAt: { type: 'number', description: 'Set on the answer' },
+        fields: { type: 'array', description: 'Extra values the approver was asked for' }
     }
 };
 
@@ -87,7 +88,7 @@ async function execute(config, input, context) {
     const reason = config.reason ? String(config.reason) : 'Approval required';
     const fields = Array.isArray(config.fields) ? config.fields : [];
 
-    smartbotic.log.info('Wait for Approval: pausing for "' + reason + '", token ' + token);
+    smartbotic.log.info('Wait for Approval: pausing for "' + reason + '"');
 
     return {
         token: token,

+ 14 - 0
src/runner/workflow_engine.cpp

@@ -1938,6 +1938,20 @@ bool WorkflowEngine::executeLoopBody(
             } else {
                 body_result = executeNode(evaluated_body_node, node_input, result.execution_id, workflow);
             }
+
+            // A pause inside a loop body cannot be resumed: the iteration state that
+            // would have to be restored lives in locals this execution never stores.
+            // Failing the node is the only honest outcome - letting the marker through
+            // would strip it and continue, orphaning an approval nobody can answer.
+            if (body_result.status == NodeStatus::Completed &&
+                body_result.output.contains("_pause")) {
+                body_result.status = NodeStatus::Failed;
+                body_result.error = "A node cannot pause inside a Loop body, because a "
+                                    "paused loop iteration cannot be resumed. Collect the "
+                                    "items first, approve once, then loop";
+                body_result.output.erase("_pause");
+            }
+
             iteration_results[body_node_id] = body_result;
 
             // Store in main results with iteration suffix

+ 23 - 0
tests/nodes/wait-for-approval-loop-chained.json

@@ -0,0 +1,23 @@
+{
+  "name": "verify-wait-for-approval-rejects-chained-loop",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "items", "name": "Items", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "return { items: [1, 2] };"}},
+    {"id": "loop", "name": "Loop", "type": "loop", "position": {"x": 0, "y": 200},
+     "config": {"inputField": "data.result.items"}},
+    {"id": "pass", "name": "Pass Through", "type": "code", "position": {"x": 0, "y": 300},
+     "config": {"code": "return { item: input.item };"}},
+    {"id": "approve", "name": "Approve", "type": "wait-for-approval", "position": {"x": 0, "y": 400},
+     "config": {"reason": "Should never pause", "expiresIn": 1}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "items", "targetInput": "data"},
+    {"sourceNodeId": "items", "sourceOutput": "main", "targetNodeId": "loop", "targetInput": "data"},
+    {"sourceNodeId": "loop", "sourceOutput": "loop", "targetNodeId": "pass", "targetInput": "data"},
+    {"sourceNodeId": "pass", "sourceOutput": "main", "targetNodeId": "approve", "targetInput": "data"}
+  ],
+  "expect": {
+    "approve": {"status": "failed", "errorContains": "cannot pause inside a Loop body"}
+  }
+}