diff --git a/src/modules/temporal/workflows/conditional-path-handle.util.spec.ts b/src/modules/temporal/workflows/conditional-path-handle.util.spec.ts new file mode 100644 index 0000000..3e34a0c --- /dev/null +++ b/src/modules/temporal/workflows/conditional-path-handle.util.spec.ts @@ -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-`; node emits raw ``. + 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-` 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(''); + }); +}); diff --git a/src/modules/temporal/workflows/conditional-path-handle.util.ts b/src/modules/temporal/workflows/conditional-path-handle.util.ts new file mode 100644 index 0000000..633324e --- /dev/null +++ b/src/modules/temporal/workflows/conditional-path-handle.util.ts @@ -0,0 +1,19 @@ +// Legacy conditional edges were saved with a `path-` prefixed sourceHandle +// (`path-`), 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-`) and new (``) both match. +// +// This normalization is scoped to the conditional path handle: a Split handle +// (`split-variant-`) 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; +} diff --git a/src/modules/temporal/workflows/journey-execution.workflow.ts b/src/modules/temporal/workflows/journey-execution.workflow.ts index 126877f..1280b47 100644 --- a/src/modules/temporal/workflows/journey-execution.workflow.ts +++ b/src/modules/temporal/workflows/journey-execution.workflow.ts @@ -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 { @@ -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-`) still route to + // the matched branch instead of falling through to else/first. + // Split (`split-variant-`) 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', {