diff --git a/nodes/workflow-monitor.html b/nodes/workflow-monitor.html index 77b8fed..aedbd74 100644 --- a/nodes/workflow-monitor.html +++ b/nodes/workflow-monitor.html @@ -48,18 +48,20 @@ ### Outputs -1. Status updates (active workflows) -: payload (object) : The workflow details from the API, sent on every status poll while workflow is active. +1. All status updates +: payload (object) : The workflow details from the API, sent on **every** status poll including terminal states. Use this output for tracking/monitoring all workflow state changes. : workflowId (string) : The ID of the workflow. 2. Success -: payload (object) : The workflow details from the API, sent once when the workflow completes successfully. +: payload (object) : The workflow details from the API, sent once when the workflow completes successfully. Use this output for flow control to trigger downstream success actions. : workflowId (string) : The ID of the workflow. 3. Error -: payload (object) : The workflow details from the API, sent once when the workflow fails or is cancelled. +: payload (object) : The workflow details from the API, sent once when the workflow fails or is cancelled. Use this output for flow control to trigger downstream error handling. : workflowId (string) : The ID of the workflow. +**Note:** When a workflow succeeds, both outputs 1 and 2 fire. When a workflow fails/cancels, both outputs 1 and 3 fire. This allows output 1 to be used for comprehensive status tracking while outputs 2 and 3 can be used for flow control. + ### Details This node queries the Seqera Platform API for the current status of a workflow run. When *Keep polling status* is enabled (default) the node will keep polling at the configured interval until the workflow reaches a terminal state (succeeded, failed, cancelled, or unknown). diff --git a/nodes/workflow-monitor.js b/nodes/workflow-monitor.js index 49ec77a..ffd6d27 100644 --- a/nodes/workflow-monitor.js +++ b/nodes/workflow-monitor.js @@ -122,16 +122,17 @@ module.exports = function (RED) { }; // Decide which output to send to - // Output 1: Active (submitted, running) - // Output 2: Succeeded - // Output 3: Failed/Cancelled/Unknown + // Output 1: All status updates (always fires for tracking/monitoring) + // Output 2: Succeeded (terminal state, for flow control) + // Output 3: Failed/Cancelled/Unknown (terminal state, for flow control) if (/^(submitted|running)$/.test(statusLower)) { send([outMsg, null, null]); } else if (/^(succeeded)$/.test(statusLower)) { - send([null, outMsg, null]); + // Send to both output 1 (status tracking) and output 2 (success flow) + send([outMsg, outMsg, null]); } else { - // failed, cancelled, unknown - send([null, null, outMsg]); + // failed, cancelled, unknown - send to both output 1 (status tracking) and output 3 (failure flow) + send([outMsg, null, outMsg]); } // If keepPolling disabled OR workflow reached a final state, stop polling THIS workflow diff --git a/test/workflow-monitor_spec.js b/test/workflow-monitor_spec.js index faabc24..5d806c9 100644 --- a/test/workflow-monitor_spec.js +++ b/test/workflow-monitor_spec.js @@ -414,6 +414,204 @@ describe("seqera-workflow-monitor Node", function () { monitorNode.receive({ workflowId: "wf-123" }); }); }); + + it("should send to BOTH output 1 and output 2 when status is succeeded", function (done) { + const flow = [ + createConfigNode(), + { + id: "monitor1", + type: "seqera-workflow-monitor", + name: "Test Monitor", + seqera: "config-node-1", + workflowId: "workflowId", + workflowIdType: "msg", + keepPolling: false, + wires: [["helper1"], ["helper2"], ["helper3"]], + }, + { id: "helper1", type: "helper" }, + { id: "helper2", type: "helper" }, + { id: "helper3", type: "helper" }, + ]; + + nock(DEFAULT_BASE_URL) + .get("/workflow/wf-123") + .query(true) + .reply(200, createWorkflowResponse({ status: "succeeded" })); + + helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () { + const monitorNode = helper.getNode("monitor1"); + const helper1 = helper.getNode("helper1"); + const helper2 = helper.getNode("helper2"); + const helper3 = helper.getNode("helper3"); + + let output1Received = false; + let output2Received = false; + + const checkDone = () => { + if (output1Received && output2Received) { + done(); + } + }; + + helper1.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("succeeded"); + output1Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + helper2.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("succeeded"); + output2Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + helper3.on("input", function () { + done(new Error("Should not receive on output 3 for succeeded status")); + }); + + monitorNode.receive({ workflowId: "wf-123" }); + }); + }); + + it("should send to BOTH output 1 and output 3 when status is failed", function (done) { + const flow = [ + createConfigNode(), + { + id: "monitor1", + type: "seqera-workflow-monitor", + name: "Test Monitor", + seqera: "config-node-1", + workflowId: "workflowId", + workflowIdType: "msg", + keepPolling: false, + wires: [["helper1"], ["helper2"], ["helper3"]], + }, + { id: "helper1", type: "helper" }, + { id: "helper2", type: "helper" }, + { id: "helper3", type: "helper" }, + ]; + + nock(DEFAULT_BASE_URL) + .get("/workflow/wf-123") + .query(true) + .reply(200, createWorkflowResponse({ status: "failed" })); + + helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () { + const monitorNode = helper.getNode("monitor1"); + const helper1 = helper.getNode("helper1"); + const helper2 = helper.getNode("helper2"); + const helper3 = helper.getNode("helper3"); + + let output1Received = false; + let output3Received = false; + + const checkDone = () => { + if (output1Received && output3Received) { + done(); + } + }; + + helper1.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("failed"); + output1Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + helper2.on("input", function () { + done(new Error("Should not receive on output 2 for failed status")); + }); + + helper3.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("failed"); + output3Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + monitorNode.receive({ workflowId: "wf-123" }); + }); + }); + + it("should send to BOTH output 1 and output 3 when status is cancelled", function (done) { + const flow = [ + createConfigNode(), + { + id: "monitor1", + type: "seqera-workflow-monitor", + name: "Test Monitor", + seqera: "config-node-1", + workflowId: "workflowId", + workflowIdType: "msg", + keepPolling: false, + wires: [["helper1"], ["helper2"], ["helper3"]], + }, + { id: "helper1", type: "helper" }, + { id: "helper2", type: "helper" }, + { id: "helper3", type: "helper" }, + ]; + + nock(DEFAULT_BASE_URL) + .get("/workflow/wf-123") + .query(true) + .reply(200, createWorkflowResponse({ status: "cancelled" })); + + helper.load([configNode, workflowMonitorNode], flow, createCredentials(), function () { + const monitorNode = helper.getNode("monitor1"); + const helper1 = helper.getNode("helper1"); + const helper2 = helper.getNode("helper2"); + const helper3 = helper.getNode("helper3"); + + let output1Received = false; + let output3Received = false; + + const checkDone = () => { + if (output1Received && output3Received) { + done(); + } + }; + + helper1.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("cancelled"); + output1Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + helper2.on("input", function () { + done(new Error("Should not receive on output 2 for cancelled status")); + }); + + helper3.on("input", function (msg) { + try { + expect(msg.payload.workflow.status).to.equal("cancelled"); + output3Received = true; + checkDone(); + } catch (err) { + done(err); + } + }); + + monitorNode.receive({ workflowId: "wf-123" }); + }); + }); }); describe("polling behavior", function () {