From 2c4dbb90a542a8bd21c90ced5388c426f1936d8a Mon Sep 17 00:00:00 2001 From: Nickolas Oliveira Date: Wed, 10 Jun 2026 17:04:05 -0300 Subject: [PATCH] fix(journey): map session-store status into the workflow loop vocabulary (EVO-1690) The session store speaks 'active'/'waiting'/'completed'; the execution loop only advances on 'running'. Copying the pre-created session's status verbatim (the norm since EVO-1644) made the loop condition false on the first iteration: every journey 'completed successfully' with zero nodes executed and no error anywhere. Only 'paused' survives the mapping - a paused session must not resume by itself. Extracted as a pure util with regression specs; validated live: the same manual trigger that produced completedNodes: 0 now executes trigger -> send-message and creates the message in the CRM conversation. Co-Authored-By: Claude Fable 5 --- .../initial-workflow-status.util.spec.ts | 23 +++++++++++++++++++ .../workflows/initial-workflow-status.util.ts | 20 ++++++++++++++++ .../workflows/journey-execution.workflow.ts | 9 ++++---- 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 src/modules/temporal/workflows/initial-workflow-status.util.spec.ts create mode 100644 src/modules/temporal/workflows/initial-workflow-status.util.ts diff --git a/src/modules/temporal/workflows/initial-workflow-status.util.spec.ts b/src/modules/temporal/workflows/initial-workflow-status.util.spec.ts new file mode 100644 index 0000000..8c757f9 --- /dev/null +++ b/src/modules/temporal/workflows/initial-workflow-status.util.spec.ts @@ -0,0 +1,23 @@ +import { resolveInitialWorkflowStatus } from './initial-workflow-status.util'; + +describe('resolveInitialWorkflowStatus (EVO-1690 regression)', () => { + it("maps the session-store 'active' status into the loop's 'running'", () => { + expect(resolveInitialWorkflowStatus('active')).toBe('running'); + }); + + it("maps 'waiting' into 'running' so signal-driven resumes re-enter the loop", () => { + expect(resolveInitialWorkflowStatus('waiting')).toBe('running'); + }); + + it("restarts a 'completed' session as 'running' (pre-existing resume semantics)", () => { + expect(resolveInitialWorkflowStatus('completed')).toBe('running'); + }); + + it("defaults to 'running' when the session has no status", () => { + expect(resolveInitialWorkflowStatus(undefined)).toBe('running'); + }); + + it("preserves 'paused' — a paused session must not resume by itself", () => { + expect(resolveInitialWorkflowStatus('paused')).toBe('paused'); + }); +}); diff --git a/src/modules/temporal/workflows/initial-workflow-status.util.ts b/src/modules/temporal/workflows/initial-workflow-status.util.ts new file mode 100644 index 0000000..7ba2742 --- /dev/null +++ b/src/modules/temporal/workflows/initial-workflow-status.util.ts @@ -0,0 +1,20 @@ +export type WorkflowExecutionStatus = + | 'running' + | 'completed' + | 'failed' + | 'paused'; + +/** + * Maps a persisted journey-session status into the workflow loop's vocabulary + * (EVO-1690). The session store speaks 'active'/'waiting'/'completed'; the + * execution loop only advances on 'running' — copying the session status + * verbatim made `while (state.status === 'running')` false on the first + * iteration and every pre-created session (the norm since EVO-1644) completed + * with zero nodes executed. Only 'paused' survives the mapping: a paused + * session must not resume by itself. + */ +export function resolveInitialWorkflowStatus( + sessionStatus: string | undefined, +): WorkflowExecutionStatus { + return sessionStatus === 'paused' ? 'paused' : 'running'; +} diff --git a/src/modules/temporal/workflows/journey-execution.workflow.ts b/src/modules/temporal/workflows/journey-execution.workflow.ts index 02a0f48..748c17b 100644 --- a/src/modules/temporal/workflows/journey-execution.workflow.ts +++ b/src/modules/temporal/workflows/journey-execution.workflow.ts @@ -13,6 +13,7 @@ import type { ActionNodeActivities } from '../activities/action-nodes.activities import type { JourneyTrackingActivities } from '../activities/journey-tracking.activities'; import type { JourneyTrackingContext } from '../services/journey-tracking.service'; import type { WaitActivities } from '../activities/wait.activities'; +import { resolveInitialWorkflowStatus } from './initial-workflow-status.util'; // Interfaces for workflow input and state export interface JourneyExecutionInput { @@ -141,10 +142,10 @@ export async function JourneyExecutionWorkflow( triggerEvent: input.triggerEvent, // Always use current trigger }, completedNodes: existingSession.completedNodes || [], - status: - existingSession.status === 'completed' - ? 'running' - : existingSession.status || 'running', // Resume if not completed + // Session-store statuses ('active'/'waiting') are a different + // vocabulary from the workflow loop's ('running'/'paused') — copying + // them verbatim kept the loop from ever running (EVO-1690). + status: resolveInitialWorkflowStatus(existingSession.status), } : { currentNodeId: undefined,