From 8c1af668308ec9a80db819147bcf0e2f117d891e Mon Sep 17 00:00:00 2001 From: samarth70 Date: Fri, 7 Aug 2026 18:53:02 -0400 Subject: [PATCH] fix(tasks): join on the last task of each fork branch forkTaskJoin() built its JOIN via generateJoinTask() without passing joinOn, so the task shipped with joinOn: []. The server evaluates the join with joinOn.stream().allMatch(...), and allMatch() short-circuits to true on an empty stream, so the JOIN reached COMPLETED on its first evaluation without waiting for any fork branch. Downstream tasks then ran while the branches were still in progress. Derive joinOn from the generated fork branches, taking the last task reference of each branch, so the JOIN blocks until every branch has finished. Reading it back off the ForkJoinTaskDef keeps it correct if forkTask() later emits more than one branch. factory.test.ts asserted joinOn: [] and so locked in the broken behaviour; it now expects the branch's last task reference. Added cases for a multi-task branch and for an empty fork task list. Fixes #135 --- .../builders/tasks/__tests__/factory.test.ts | 14 +++++++++++++- src/sdk/builders/tasks/forkJoin.ts | 17 +++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/sdk/builders/tasks/__tests__/factory.test.ts b/src/sdk/builders/tasks/__tests__/factory.test.ts index fffbb6b4..3c02c34a 100644 --- a/src/sdk/builders/tasks/__tests__/factory.test.ts +++ b/src/sdk/builders/tasks/__tests__/factory.test.ts @@ -134,11 +134,23 @@ describe("forkTask", () => { name: "forkTaskJoin_join", taskReferenceName: "forkTaskJoin_join_ref", inputParameters: {}, - joinOn: [], + joinOn: ["forkTaskJoin"], optional: true, type: "JOIN", }); }); + it("Should join on the last task of each fork branch", () => { + const [, joinTask] = forkTaskJoin("multiStep", [ + eventTask("first", "prefix", "suffix"), + eventTask("second", "prefix", "suffix"), + eventTask("last", "prefix", "suffix"), + ]); + expect(joinTask.joinOn).toEqual(["last"]); + }); + it("Should produce an empty joinOn when there are no fork tasks", () => { + const [, joinTask] = forkTaskJoin("emptyFork", []); + expect(joinTask.joinOn).toEqual([]); + }); }); describe("httpTask", () => { diff --git a/src/sdk/builders/tasks/forkJoin.ts b/src/sdk/builders/tasks/forkJoin.ts index b150ea4f..41a174a3 100644 --- a/src/sdk/builders/tasks/forkJoin.ts +++ b/src/sdk/builders/tasks/forkJoin.ts @@ -20,7 +20,16 @@ export const forkTaskJoin = ( taskReferenceName: string, forkTasks: TaskDefTypes[], optional?: boolean -): [ForkJoinTaskDef, JoinTaskDef] => [ - forkTask(taskReferenceName, forkTasks), - generateJoinTask({ name: `${taskReferenceName}_join`, optional }), -]; +): [ForkJoinTaskDef, JoinTaskDef] => { + const fork = forkTask(taskReferenceName, forkTasks); + // The server checks joinOn with allMatch(), which short-circuits to true on an + // empty list — a JOIN with no joinOn completes without waiting for any branch. + // Join on the last task of every branch so the JOIN blocks until each finishes. + const joinOn = fork.forkTasks + .map((branch) => branch[branch.length - 1]?.taskReferenceName) + .filter((ref): ref is string => ref !== undefined); + return [ + fork, + generateJoinTask({ name: `${taskReferenceName}_join`, joinOn, optional }), + ]; +};