diff --git a/src/modules/temporal/activities/nodes/base.node.ts b/src/modules/temporal/activities/nodes/base.node.ts index 0359f6b..eb8abc7 100644 --- a/src/modules/temporal/activities/nodes/base.node.ts +++ b/src/modules/temporal/activities/nodes/base.node.ts @@ -8,6 +8,7 @@ import { export interface NodeExecutionResult { success: boolean; nextNodeId?: string; + nextNodeHandle?: string; // Branch routing: the edge.sourceHandle the executor should follow (conditional/split nodes) error?: string; variables?: Record; executionTime?: number; diff --git a/src/modules/temporal/activities/nodes/split.node.spec.ts b/src/modules/temporal/activities/nodes/split.node.spec.ts new file mode 100644 index 0000000..d8223bd --- /dev/null +++ b/src/modules/temporal/activities/nodes/split.node.spec.ts @@ -0,0 +1,55 @@ +import { SplitNode } from './split.node'; + +describe('SplitNode (EVO-1828)', () => { + let node: SplitNode; + + const make = ( + variants?: Array<{ id: string; name: string; percentage: number }>, + contactId = 'contact-1', + ) => ({ + nodeId: 'split-1', + contactId, + sessionId: 'session-1', + nodeData: { variants }, + }); + + beforeEach(() => { + node = new SplitNode(); + const logger = (node as any).logger; + for (const m of ['log', 'debug', 'warn', 'error']) { + jest.spyOn(logger, m).mockImplementation(() => undefined); + } + }); + + it('returns a nextNodeHandle pointing at the selected variant (was dropped before the fix)', async () => { + const res = await node.execute(make()); + expect(res.success).toBe(true); + expect(res.nextNodeHandle).toMatch(/^split-variant-(variant-a|variant-b)$/); + }); + + it('routes every contact to variant A when A is 100%', async () => { + const variants = [ + { id: 'variant-a', name: 'A', percentage: 100 }, + { id: 'variant-b', name: 'B', percentage: 0 }, + ]; + for (const c of ['c1', 'c2', 'whatever']) { + const res = await node.execute(make(variants, c)); + expect(res.nextNodeHandle).toBe('split-variant-variant-a'); + } + }); + + it('routes every contact to variant B when B is 100%', async () => { + const variants = [ + { id: 'variant-a', name: 'A', percentage: 0 }, + { id: 'variant-b', name: 'B', percentage: 100 }, + ]; + const res = await node.execute(make(variants, 'c1')); + expect(res.nextNodeHandle).toBe('split-variant-variant-b'); + }); + + it('selects deterministically per contactId', async () => { + const a = await node.execute(make(undefined, 'same-contact')); + const b = await node.execute(make(undefined, 'same-contact')); + expect(a.nextNodeHandle).toBe(b.nextNodeHandle); + }); +}); diff --git a/src/modules/temporal/activities/nodes/split.node.ts b/src/modules/temporal/activities/nodes/split.node.ts index 585fbec..67d162e 100644 --- a/src/modules/temporal/activities/nodes/split.node.ts +++ b/src/modules/temporal/activities/nodes/split.node.ts @@ -79,11 +79,17 @@ export class SplitNode extends BaseNode { }; }) .then(({ result, executionTime }) => { - return this.createSuccessResult(input, executionTime, { - [`node_${input.nodeId}_selected_variant`]: result.selectedVariant, - [`node_${input.nodeId}_variant_id`]: result.variantId, - [`node_${input.nodeId}_random_value`]: result.random, - }); + // EVO-1828: carry the routing decision so the executor branches to the + // selected variant's edge (sourceHandle = split-variant-) instead of + // always falling back to the first edge. + return { + ...this.createSuccessResult(input, executionTime, { + [`node_${input.nodeId}_selected_variant`]: result.selectedVariant, + [`node_${input.nodeId}_variant_id`]: result.variantId, + [`node_${input.nodeId}_random_value`]: result.random, + }), + nextNodeHandle: result.nextNodeHandle, + }; }) .catch((error) => { const executionTime = Date.now();