Initial push of Plunk Next

This commit is contained in:
Dries Augustyns
2025-12-01 09:56:56 +01:00
parent 07cea20262
commit ff1876d580
566 changed files with 89036 additions and 28423 deletions
@@ -0,0 +1,957 @@
import {describe, it, expect, beforeEach, vi} from 'vitest';
import {WorkflowStepType, StepExecutionStatus, WorkflowExecutionStatus, TemplateType, Prisma} from '@plunk/db';
import {WorkflowExecutionService} from '../WorkflowExecutionService';
import {factories, getPrismaClient} from '../../../../../test/helpers';
/**
* Integration Tests: Workflow Execution Engine
*
* These tests verify the actual execution logic of workflows,
* testing real step processing, conditional branching, event handling,
* and complex multi-step scenarios.
*/
// Mock QueueService to prevent actual job queueing
vi.mock('../QueueService', () => ({
QueueService: {
queueWorkflowStep: vi.fn(async () => ({id: 'mock-job-id'})),
queueEmail: vi.fn(async () => ({id: 'mock-email-job-id'})),
queueWorkflowTimeout: vi.fn(async () => ({id: 'mock-timeout-job-id'})),
cancelWorkflowTimeout: vi.fn(async () => true),
},
}));
// Mock SES for email sending
vi.mock('../../services/ses', () => ({
ses: {
sendEmail: vi.fn(async () => ({MessageId: 'mock-message-id'})),
},
}));
describe('WorkflowExecutionService - Integration Tests', () => {
let projectId: string;
const prisma = getPrismaClient();
beforeEach(async () => {
const {project} = await factories.createUserWithProject();
projectId = project.id;
});
// ========================================
// CONDITIONAL BRANCHING (CONDITION STEPS)
// ========================================
describe('Conditional Branching', () => {
it('should follow YES branch when condition evaluates to true', async () => {
const contact = await factories.createContact({
projectId,
data: {isPremium: true},
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
// Create condition step
const conditionStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Premium Status',
position: {x: 100, y: 0},
config: {
field: 'data.isPremium',
operator: 'equals',
value: true,
},
},
});
// Create YES and NO branches
const yesStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Premium Path',
position: {x: 200, y: -50},
config: {reason: 'Premium customer'},
},
});
const noStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Standard Path',
position: {x: 200, y: 50},
config: {reason: 'Standard customer'},
},
});
// Create transitions
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: conditionStep.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: conditionStep.id,
toStepId: yesStep.id,
condition: {branch: 'yes'},
priority: 1,
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: conditionStep.id,
toStepId: noStep.id,
condition: {branch: 'no'},
priority: 2,
},
});
// Create execution
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
// Process trigger step
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
// Process condition step
await WorkflowExecutionService.processStepExecution(execution.id, conditionStep.id);
// Verify YES branch was taken
const stepExecutions = await prisma.workflowStepExecution.findMany({
where: {executionId: execution.id},
include: {step: true},
orderBy: {createdAt: 'asc'},
});
// Should have: TRIGGER, CONDITION, and YES step
expect(stepExecutions.length).toBeGreaterThanOrEqual(2);
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditionExec).toBeDefined();
expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED);
expect((conditionExec?.output as any)?.branch).toBe('yes');
});
it('should follow NO branch when condition evaluates to false', async () => {
const contact = await factories.createContact({
projectId,
data: {isPremium: false},
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const conditionStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Premium',
position: {x: 100, y: 0},
config: {
field: 'data.isPremium',
operator: 'equals',
value: true,
},
},
});
const yesStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Premium',
position: {x: 200, y: -50},
config: {},
},
});
const noStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Standard',
position: {x: 200, y: 50},
config: {},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: conditionStep.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: conditionStep.id,
toStepId: yesStep.id,
condition: {branch: 'yes'},
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: conditionStep.id,
toStepId: noStep.id,
condition: {branch: 'no'},
},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, conditionStep.id);
const stepExecutions = await prisma.workflowStepExecution.findMany({
where: {executionId: execution.id},
include: {step: true},
orderBy: {createdAt: 'asc'},
});
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditionExec).toBeDefined();
expect((conditionExec?.output as any)?.branch).toBe('no');
});
it('should handle complex nested conditions', async () => {
const contact = await factories.createContact({
projectId,
data: {country: 'US', isPremium: true},
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
// First condition: Check country
const condition1 = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Country',
position: {x: 100, y: 0},
config: {field: 'data.country', operator: 'equals', value: 'US'},
},
});
// Second condition (nested): Check premium status (only for US)
const condition2 = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Premium (US)',
position: {x: 200, y: -50},
config: {field: 'data.isPremium', operator: 'equals', value: true},
},
});
const usPremiumExit = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'US Premium',
position: {x: 300, y: -75},
config: {},
},
});
const usStandardExit = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'US Standard',
position: {x: 300, y: -25},
config: {},
},
});
const nonUsExit = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Non-US',
position: {x: 200, y: 50},
config: {},
},
});
// Create transitions
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: condition1.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition1.id,
toStepId: condition2.id,
condition: {branch: 'yes'},
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition1.id,
toStepId: nonUsExit.id,
condition: {branch: 'no'},
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition2.id,
toStepId: usPremiumExit.id,
condition: {branch: 'yes'},
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition2.id,
toStepId: usStandardExit.id,
condition: {branch: 'no'},
},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, condition1.id);
await WorkflowExecutionService.processStepExecution(execution.id, condition2.id);
const stepExecutions = await prisma.workflowStepExecution.findMany({
where: {executionId: execution.id},
include: {step: true},
orderBy: {createdAt: 'asc'},
});
// Should have executed: TRIGGER → CONDITION (US) → CONDITION (Premium) → EXIT (US Premium)
expect(stepExecutions.length).toBeGreaterThanOrEqual(3);
const conditions = stepExecutions.filter(se => se.step.type === WorkflowStepType.CONDITION);
expect(conditions).toHaveLength(2);
expect((conditions[0].output as any)?.branch).toBe('yes'); // US = yes
expect((conditions[1].output as any)?.branch).toBe('yes'); // Premium = yes
});
});
// ========================================
// WAIT_FOR_EVENT STEPS
// ========================================
describe('Wait for Event', () => {
it('should pause workflow execution when waiting for event', async () => {
const contact = await factories.createContact({projectId});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const waitStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.WAIT_FOR_EVENT,
name: 'Wait for Purchase',
position: {x: 100, y: 0},
config: {
eventName: 'purchase.completed',
timeout: 3600, // 1 hour
},
},
});
const exitStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Complete',
position: {x: 200, y: 0},
config: {},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: waitStep.id},
});
await prisma.workflowTransition.create({
data: {fromStepId: waitStep.id, toStepId: exitStep.id},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, waitStep.id);
// Verify step is in WAITING status
const waitStepExecution = await prisma.workflowStepExecution.findFirst({
where: {
executionId: execution.id,
stepId: waitStep.id,
},
});
expect(waitStepExecution?.status).toBe(StepExecutionStatus.WAITING);
// Verify workflow execution is in WAITING status
const updatedExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(updatedExecution?.status).toBe(WorkflowExecutionStatus.WAITING);
});
it('should resume workflow when expected event arrives', async () => {
const contact = await factories.createContact({projectId});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const waitStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.WAIT_FOR_EVENT,
name: 'Wait for Event',
position: {x: 100, y: 0},
config: {
eventName: 'user.verified',
timeout: 3600,
},
},
});
const exitStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Done',
position: {x: 200, y: 0},
config: {},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: waitStep.id},
});
await prisma.workflowTransition.create({
data: {fromStepId: waitStep.id, toStepId: exitStep.id},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, waitStep.id);
// Verify waiting state
const waitingExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(waitingExecution?.status).toBe(WorkflowExecutionStatus.WAITING);
// Simulate event arrival by calling handleEvent
await WorkflowExecutionService.handleEvent(projectId, 'user.verified', contact.id, {verified: true});
// Verify step execution was resumed
const waitStepExecution = await prisma.workflowStepExecution.findFirst({
where: {
executionId: execution.id,
stepId: waitStep.id,
},
});
// Step should be completed after event arrives
expect(waitStepExecution?.status).toBe(StepExecutionStatus.COMPLETED);
});
});
// ========================================
// COMPLEX MULTI-STEP WORKFLOWS
// ========================================
describe('Complex Multi-Step Workflows', () => {
it('should execute linear workflow end-to-end', async () => {
const contact = await factories.createContact({projectId});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
// Build: TRIGGER → DELAY → CONDITION → EXIT
const delay = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.DELAY,
name: 'Wait 1 day',
position: {x: 100, y: 0},
config: {amount: 1, unit: 'days'},
},
});
const condition = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Status',
position: {x: 200, y: 0},
config: {field: 'contact.subscribed', operator: 'equals', value: true},
},
});
const exit = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Complete',
position: {x: 300, y: 0},
config: {},
},
});
// Create transitions
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: delay.id},
});
await prisma.workflowTransition.create({
data: {fromStepId: delay.id, toStepId: condition.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition.id,
toStepId: exit.id,
condition: {branch: 'yes'},
},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
// Execute through workflow
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
// Verify step executions were created
const stepExecutions = await prisma.workflowStepExecution.findMany({
where: {executionId: execution.id},
include: {step: true},
orderBy: {createdAt: 'asc'},
});
// Should have at least TRIGGER step
expect(stepExecutions.length).toBeGreaterThanOrEqual(1);
// Verify trigger step completed
const triggerExec = stepExecutions.find(se => se.step.type === WorkflowStepType.TRIGGER);
expect(triggerExec?.status).toBe(StepExecutionStatus.COMPLETED);
});
it('should handle workflows with multiple branches that converge', async () => {
const contact = await factories.createContact({
projectId,
data: {segment: 'A'},
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
// Split into A/B paths, then merge
const condition = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'A/B Split',
position: {x: 100, y: 0},
config: {field: 'data.segment', operator: 'equals', value: 'A'},
},
});
const pathA = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.DELAY,
name: 'Path A Delay',
position: {x: 200, y: -50},
config: {amount: 1, unit: 'hours'},
},
});
const pathB = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.DELAY,
name: 'Path B Delay',
position: {x: 200, y: 50},
config: {amount: 2, unit: 'hours'},
},
});
const merge = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Merge Point',
position: {x: 300, y: 0},
config: {},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: condition.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition.id,
toStepId: pathA.id,
condition: {branch: 'yes'},
},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition.id,
toStepId: pathB.id,
condition: {branch: 'no'},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: pathA.id, toStepId: merge.id},
});
await prisma.workflowTransition.create({
data: {fromStepId: pathB.id, toStepId: merge.id},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, condition.id);
const stepExecutions = await prisma.workflowStepExecution.findMany({
where: {executionId: execution.id},
include: {step: true},
orderBy: {createdAt: 'asc'},
});
// Should have: TRIGGER, CONDITION, and Path A (since segment = 'A')
expect(stepExecutions.length).toBeGreaterThanOrEqual(2);
const conditionExec = stepExecutions.find(se => se.step.type === WorkflowStepType.CONDITION);
expect((conditionExec?.output as any)?.branch).toBe('yes');
});
});
// ========================================
// ERROR HANDLING
// ========================================
describe('Error Handling', () => {
it('should mark workflow as FAILED when step execution fails', async () => {
const contact = await factories.createContact({projectId});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
// Create a CONDITION step with invalid config (will fail)
const badStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Bad Condition',
position: {x: 100, y: 0},
config: {}, // Invalid - missing required fields
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: badStep.id},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
// Processing the trigger step will automatically try to process the bad step
// due to the transition, which will throw an error
await expect(WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id)).rejects.toThrow();
// Verify workflow execution is marked as FAILED
const failedExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(failedExecution?.status).toBe(WorkflowExecutionStatus.FAILED);
});
it('should handle missing contact data gracefully in CONDITION steps', async () => {
const contact = await factories.createContact({
projectId,
data: {}, // No fields
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const condition = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.CONDITION,
name: 'Check Missing Field',
position: {x: 100, y: 0},
config: {
field: 'data.nonExistentField',
operator: 'equals',
value: 'something',
},
},
});
const noStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Exit',
position: {x: 200, y: 0},
config: {},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: condition.id},
});
await prisma.workflowTransition.create({
data: {
fromStepId: condition.id,
toStepId: noStep.id,
condition: {branch: 'no'},
},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, condition.id);
// Verify condition evaluated to 'no' when field doesn't exist
const conditionExec = await prisma.workflowStepExecution.findFirst({
where: {executionId: execution.id, stepId: condition.id},
});
expect(conditionExec?.status).toBe(StepExecutionStatus.COMPLETED);
expect((conditionExec?.output as any)?.branch).toBe('no');
});
});
// ========================================
// EXIT STEPS
// ========================================
describe('Exit Steps', () => {
it('should complete workflow when EXIT step is reached', async () => {
const contact = await factories.createContact({projectId});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await prisma.workflowStep.findFirstOrThrow({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
const exitStep = await prisma.workflowStep.create({
data: {
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'Early Exit',
position: {x: 100, y: 0},
config: {reason: 'User already converted'},
},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: exitStep.id},
});
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: {},
},
});
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, exitStep.id);
// Verify workflow completed
const completedExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(completedExecution?.status).toBe(WorkflowExecutionStatus.COMPLETED);
expect(completedExecution?.completedAt).toBeDefined();
});
});
describe('Non-Persistent Data in Workflow Context', () => {
it('should make non-persistent event data available throughout entire workflow execution', async () => {
const contact = await factories.createContact({
projectId,
data: {
firstName: 'Alice',
plan: 'enterprise',
},
});
const workflow = await factories.createWorkflow({projectId});
const triggerStep = await factories.createWorkflowStep({
workflowId: workflow.id,
type: WorkflowStepType.TRIGGER,
name: 'Start',
position: {x: 0, y: 0},
config: {},
});
const exitStep = await factories.createWorkflowStep({
workflowId: workflow.id,
type: WorkflowStepType.EXIT,
name: 'End',
position: {x: 100, y: 0},
config: {},
});
await prisma.workflowTransition.create({
data: {fromStepId: triggerStep.id, toStepId: exitStep.id},
});
// Create execution with context containing both persistent and non-persistent data
const contextData = {
totalSpent: 999.99, // Persistent
orderId: {value: 'ORD-789', persistent: false}, // Non-persistent
trackingUrl: {value: 'https://track.example.com/ORD-789', persistent: false}, // Non-persistent
};
const execution = await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep.id,
context: contextData as Prisma.InputJsonValue,
},
});
// Verify context persists in execution record
const savedExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(savedExecution?.context).toMatchObject({
totalSpent: 999.99,
orderId: {value: 'ORD-789', persistent: false},
trackingUrl: {value: 'https://track.example.com/ORD-789', persistent: false},
});
// Process workflow steps
await WorkflowExecutionService.processStepExecution(execution.id, triggerStep.id);
await WorkflowExecutionService.processStepExecution(execution.id, exitStep.id);
// Verify context still available after workflow completes
const completedExecution = await prisma.workflowExecution.findUnique({
where: {id: execution.id},
});
expect(completedExecution?.status).toBe(WorkflowExecutionStatus.COMPLETED);
expect(completedExecution?.context).toMatchObject({
totalSpent: 999.99,
orderId: {value: 'ORD-789', persistent: false},
trackingUrl: {value: 'https://track.example.com/ORD-789', persistent: false},
});
// Verify contact data was NOT polluted with non-persistent data
const savedContact = await prisma.contact.findUnique({
where: {id: contact.id},
});
expect(savedContact?.data).toMatchObject({
firstName: 'Alice',
plan: 'enterprise',
});
expect(savedContact?.data).not.toHaveProperty('orderId');
expect(savedContact?.data).not.toHaveProperty('trackingUrl');
});
});
});