Skip to content

fix(evo-flow): route split node to the selected variant edge (EVO-1828)#74

Merged
dpaes merged 1 commit into
developfrom
fix/EVO-1828
Jun 18, 2026
Merged

fix(evo-flow): route split node to the selected variant edge (EVO-1828)#74
dpaes merged 1 commit into
developfrom
fix/EVO-1828

Conversation

@nickoliveira23

@nickoliveira23 nickoliveira23 commented Jun 18, 2026

Copy link
Copy Markdown

Summary

The Split (A/B test) node silently never branched: it computed the target handle (split-variant-<id>) but dropped it — the .then() called createSuccessResult(...) which does not carry nextNodeHandle, so the result's handle was undefined. 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 nextNodeHandle in the Split node's result (spread createSuccessResult + add the handle, mirroring ConditionalNode), and declare nextNodeHandle? on the NodeExecutionResult interface (it was already used informally by ConditionalNode and read by the executor).

Root cause (code-traced)

  • split.node.ts:72 computes nextNodeHandle = split-variant-<selectedVariant.id>, the inner fn returns it, but the .then() rebuilt the result via createSuccessResult (no nextNodeHandle).
  • base.node.ts NodeExecutionResult did not declare nextNodeHandle.
  • Editor contract verified OK: SplitNode.tsx emits handles id = 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:

node_transition: split-node-1781807883737 → add-label-node-1781807939020

add-label-node-1781807939020 is 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 → clean
  • evo-flow: npx prettier --check (split.node.ts + spec) → clean
  • Live smoke above

Changed Files

  • src/modules/temporal/activities/nodes/split.node.ts
  • src/modules/temporal/activities/nodes/base.node.ts
  • src/modules/temporal/activities/nodes/split.node.spec.ts (new)

Linked Issue

  • EVO-1828

🤖 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:

  • Fix Split node execution to include the selected variant handle so journeys branch according to configured variant weights instead of always taking the first edge.

Enhancements:

  • Extend the NodeExecutionResult contract to formally support nextNodeHandle for branch-routing nodes like Split and Conditional.

Tests:

  • Add unit tests for Split node execution to validate handle propagation, routing behavior, and deterministic variant selection.

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>
@sourcery-ai

sourcery-ai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes 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

Change Details Files
Ensure Split node execution result carries the chosen variant’s routing handle so the executor follows the correct edge instead of always the first.
  • Wrap the existing success result from createSuccessResult in an object spread and add nextNodeHandle from the split computation result.
  • Preserve existing variable outputs (selected variant, variant id, random value) while augmenting the result with routing information.
  • Document via an inline comment that nextNodeHandle corresponds to the selected variant edge handle (split-variant-).
src/modules/temporal/activities/nodes/split.node.ts
Extend the node execution contract to explicitly support handle-based branching used by conditional and split nodes.
  • Add an optional nextNodeHandle field to the NodeExecutionResult interface.
  • Annotate nextNodeHandle with a comment clarifying it is the edge.sourceHandle consumed by the executor for branching decisions.
src/modules/temporal/activities/nodes/base.node.ts
Introduce unit tests validating split node routing and determinism behavior.
  • Create a new Jest spec file for SplitNode covering that the handle is returned and 100% allocation to each variant routes correctly.
  • Verify deterministic behavior per contactId and alignment between selected variant and emitted handle.
src/modules/temporal/activities/nodes/split.node.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dpaes dpaes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 + adding nextNodeHandle: result.nextNodeHandle mirrors the ConditionalNode pattern, and is arguably cleaner — it preserves the base bookkeeping vars while keeping nextNodeId undefined (required so the executor uses handle-based edge routing).
  • Declaring nextNodeHandle?: string on NodeExecutionResult makes 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 sourceHandle edges — 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 that sourceHandle. Backend and frontend both read variant.id from the same persisted nodeData.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_transition to 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.

@dpaes dpaes merged commit 76fa427 into develop Jun 18, 2026
3 checks passed
@dpaes dpaes deleted the fix/EVO-1828 branch June 18, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants