Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { normalizeConditionalPathHandle } from './conditional-path-handle.util';

describe('normalizeConditionalPathHandle (EVO-1922)', () => {
it('strips a single leading `path-` prefix (legacy conditional edge handle)', () => {
expect(normalizeConditionalPathHandle('path-abc123')).toBe('abc123');
});

it('returns a non-prefixed handle unchanged (new conditional handle)', () => {
expect(normalizeConditionalPathHandle('abc123')).toBe('abc123');
});

it('matches legacy edge handle against the raw node handle', () => {
// Edge saved before EVO-1902 -> `path-<id>`; node emits raw `<id>`.
expect(normalizeConditionalPathHandle('path-branch-1')).toBe(
normalizeConditionalPathHandle('branch-1'),
);
});

it('matches new edge handle against the raw node handle', () => {
expect(normalizeConditionalPathHandle('branch-1')).toBe(
normalizeConditionalPathHandle('branch-1'),
);
});

it('leaves `else` (default path handle) untouched', () => {
expect(normalizeConditionalPathHandle('else')).toBe('else');
});

it('does NOT regress Split: `split-variant-<id>` is returned unchanged', () => {
// Split prefixes both sides with `split-variant-`; it must NOT be stripped.
const handle = 'split-variant-42';
expect(normalizeConditionalPathHandle(handle)).toBe(handle);
});

it('strips only the first `path-` occurrence, not nested ones', () => {
expect(normalizeConditionalPathHandle('path-path-x')).toBe('path-x');
});

it('does not strip `path` without the trailing hyphen', () => {
expect(normalizeConditionalPathHandle('pathway-1')).toBe('pathway-1');
});

it('coerces non-string handles to an empty string', () => {
expect(normalizeConditionalPathHandle(undefined)).toBe('');
expect(normalizeConditionalPathHandle(null)).toBe('');
expect(normalizeConditionalPathHandle(123 as unknown)).toBe('');
});
});
19 changes: 19 additions & 0 deletions src/modules/temporal/workflows/conditional-path-handle.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Legacy conditional edges were saved with a `path-` prefixed sourceHandle
// (`path-<id>`), while the Conditional node emits the raw `path.id` (EVO-1902).
// To keep journeys authored before that fix routing correctly, strip a single
// optional leading `path-` from both the edge handle and the node handle before
// comparing — so legacy (`path-<id>`) and new (`<id>`) both match.
//
// This normalization is scoped to the conditional path handle: a Split handle
// (`split-variant-<id>`) does not start with `path-`, so it is returned
// unchanged and Split keeps matching on its full prefixed handle on both sides.
// `else` and any other non-prefixed handle are likewise returned as-is.
// (EVO-1922)
export const CONDITIONAL_PATH_HANDLE_PREFIX = 'path-';

export function normalizeConditionalPathHandle(handle: unknown): string {
if (typeof handle !== 'string') return '';
return handle.startsWith(CONDITIONAL_PATH_HANDLE_PREFIX)
? handle.slice(CONDITIONAL_PATH_HANDLE_PREFIX.length)
: handle;
}
14 changes: 12 additions & 2 deletions src/modules/temporal/workflows/journey-execution.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { JourneyTrackingActivities } from '../activities/journey-tracking.a
import type { JourneyTrackingContext } from '../services/journey-tracking.service';
import type { WaitActivities } from '../activities/wait.activities';
import { resolveInitialWorkflowStatus } from './initial-workflow-status.util';
import { normalizeConditionalPathHandle } from './conditional-path-handle.util';

// Interfaces for workflow input and state
export interface JourneyExecutionInput {
Expand Down Expand Up @@ -1127,9 +1128,18 @@ export async function JourneyExecutionWorkflow(
if (outgoingEdges && outgoingEdges.length > 0) {
// Check if this is a multi-output node (like split or wait with fallback)
if (nodeResult.nextNodeHandle) {
// Find edge matching the specific handle
// Find edge matching the specific handle. Normalize an optional
// legacy `path-` prefix on both sides so conditional journeys
// saved before EVO-1902 (sourceHandle `path-<id>`) still route to
// the matched branch instead of falling through to else/first.
// Split (`split-variant-<id>`) is untouched — see helper. (EVO-1922)
const normalizedHandle = normalizeConditionalPathHandle(
nodeResult.nextNodeHandle,
);
const targetEdge = outgoingEdges.find(
(edge: any) => edge.sourceHandle === nodeResult.nextNodeHandle,
(edge: any) =>
normalizeConditionalPathHandle(edge.sourceHandle) ===
normalizedHandle,
);

// log.info('🔍 DEBUG: Handle-based edge matching', {
Expand Down
Loading