fix(evo-flow): route split node to the selected variant edge (EVO-1828)#74
Conversation
The Split (A/B) node computed the target handle but dropped it: its `.then()` called createSuccessResult without nextNodeHandle, so the executor fell back to the first edge and every contact took variant A regardless of percentages. Carry nextNodeHandle in the result (mirroring ConditionalNode) and declare it on NodeExecutionResult. Adds unit specs for the handle + per-contact selection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer's guide (collapsed on small PRs)Reviewer's GuideFixes Split (A/B test) node routing so that it correctly propagates the selected variant’s handle to the workflow executor, and formalizes the nextNodeHandle field on NodeExecutionResult while adding tests for split-node behavior. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
dpaes
left a comment
There was a problem hiding this comment.
✅ Approved
Reviewed against the EVO-1828 acceptance criteria — code-traced all three seams firsthand.
Root cause confirmed: split.node.ts computed nextNodeHandle but the .then() returned createSuccessResult(...), which never carried it, so the executor (journey-execution.workflow.ts) saw nextNodeHandle === undefined and fell back to outgoingEdges[0].target — every contact took the first edge.
Fix is correct and minimal:
- Spreading
createSuccessResult+ addingnextNodeHandle: result.nextNodeHandlemirrors theConditionalNodepattern, and is arguably cleaner — it preserves the base bookkeeping vars while keepingnextNodeIdundefined (required so the executor uses handle-based edge routing). - Declaring
nextNodeHandle?: stringonNodeExecutionResultmakes explicit a contract the executor was already reading — good hygiene.
AC validation (4/4):
- ✅ Result includes
nextNodeHandle = split-variant-<selectedVariantId>. - ✅ Routes per percentage given matching
sourceHandleedges — verified the cross-repo contract: the Flow Builder renders<Handle type="source" id={split-variant-${variant.id}}>(SplitNode.tsx), so user-drawn edges carry exactly thatsourceHandle. Backend and frontend both readvariant.idfrom the same persistednodeData.variants→ they match by construction. No companion frontend fix needed; your PR note holds. - ✅ Deterministic per
contactId(hashString). - ✅ Unit test covers the handle + selection. Live full-stack smoke (A=0%/B=100% → worker
node_transitionto the variant-B branch) independently proves real routing.
Non-blocking note (out of scope, pre-existing): in base.node.ts, nodeTypesToForceNextNode uses type strings ('transfer-journey-node', 'conditional-node', 'wait-node') that don't match the actual nodeType values ('TransferJourney', 'Conditional', 'Wait') — only 'exit-journey-node' matches. Doesn't affect this fix (Split correctly wants nextNodeId undefined). Worth a separate look at whether Wait/Transfer rely on the never-forced nextNodeId.
Merging to develop via squash.
Summary
The Split (A/B test) node silently never branched: it computed the target handle (
split-variant-<id>) but dropped it — the.then()calledcreateSuccessResult(...)which does not carrynextNodeHandle, so the result's handle wasundefined. The executor (journey-execution.workflow.ts:1121) then fell back to the first outgoing edge, sending every contact to variant A regardless of the configured percentages.Fix: carry
nextNodeHandlein the Split node's result (spreadcreateSuccessResult+ add the handle, mirroringConditionalNode), and declarenextNodeHandle?on theNodeExecutionResultinterface (it was already used informally by ConditionalNode and read by the executor).Root cause (code-traced)
split.node.ts:72computesnextNodeHandle = split-variant-<selectedVariant.id>, the inner fn returns it, but the.then()rebuilt the result viacreateSuccessResult(nonextNodeHandle).base.node.tsNodeExecutionResultdid not declarenextNodeHandle.SplitNode.tsxemits handlesid = split-variant-<variant.id>→ matches what the runtime now returns. Backend-only fix.Live smoke (full stack, fix branch via dev:single)
Built a journey
Split (A=0% / B=100%) → A: add-label test_label / B: add-label test_label_2, triggered it for a contact. The worker log shows the routing decision explicitly:add-label-node-1781807939020is the variant B (100%) branch (test_label_2) — not variant A / the first edge. Before the fix it routed to variant A. AC proven.Validation
evo-flow: npx jest split.node.spec.ts→ 4/4 (handle returned; 100%→A; 100%→B; deterministic per contactId)evo-flow: npm run build→ cleanevo-flow: npx prettier --check(split.node.ts + spec) → cleanChanged Files
src/modules/temporal/activities/nodes/split.node.tssrc/modules/temporal/activities/nodes/base.node.tssrc/modules/temporal/activities/nodes/split.node.spec.ts(new)Linked Issue
🤖 Generated with Claude Code
Summary by Sourcery
Ensure Split (A/B test) nodes route contacts to the correct variant edge by propagating the selected handle through node execution results.
Bug Fixes:
Enhancements:
Tests: