@sprqvntrs/workflows 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Testing utilities for workflows.
3
+ *
4
+ * This module provides utilities for testing workflow templates and operations
5
+ * in isolation, without requiring a full database or pg-boss setup.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createMockOrchestrator, createMockContext } from '@sprqvntrs/workflows/testing';
10
+ *
11
+ * // Test an operation handler
12
+ * const context = createMockContext({
13
+ * workflowId: 'test-workflow',
14
+ * operationType: 'gather.data',
15
+ * previousResults: { url: 'https://example.com' },
16
+ * });
17
+ *
18
+ * const result = await myHandler(context);
19
+ * expect(result.status).toBe('completed');
20
+ * ```
21
+ */
22
+
23
+ import type {
24
+ OperationContext,
25
+ OperationResult,
26
+ WorkflowTemplate,
27
+ OperationHandler,
28
+ WorkflowContext,
29
+ WorkflowRecord,
30
+ OperationRecord,
31
+ WorkflowStatus,
32
+ OperationStatus,
33
+ } from '../types';
34
+
35
+ // =============================================================================
36
+ // Mock Context
37
+ // =============================================================================
38
+
39
+ /**
40
+ * Options for creating a mock operation context.
41
+ */
42
+ export interface MockContextOptions {
43
+ /**
44
+ * Workflow ID.
45
+ * @default 'test-workflow-id'
46
+ */
47
+ workflowId?: string;
48
+
49
+ /**
50
+ * Operation ID.
51
+ * @default 'test-operation-id'
52
+ */
53
+ operationId?: string;
54
+
55
+ /**
56
+ * Operation type.
57
+ * @default 'test.operation'
58
+ */
59
+ operationType?: string;
60
+
61
+ /**
62
+ * Stage name.
63
+ * @default 'test-stage'
64
+ */
65
+ stageName?: string;
66
+
67
+ /**
68
+ * Current attempt number.
69
+ * @default 1
70
+ */
71
+ attempt?: number;
72
+
73
+ /**
74
+ * Maximum attempts.
75
+ * @default 3
76
+ */
77
+ maxAttempts?: number;
78
+
79
+ /**
80
+ * Previous operation results.
81
+ * @default {}
82
+ */
83
+ previousResults?: Record<string, unknown>;
84
+
85
+ /**
86
+ * Initial workflow context.
87
+ * @default {}
88
+ */
89
+ initialContext?: Record<string, unknown>;
90
+
91
+ /**
92
+ * Workflow type.
93
+ * @default 'test-workflow'
94
+ */
95
+ workflowType?: string;
96
+
97
+ /**
98
+ * Abort signal for cancellation testing.
99
+ */
100
+ signal?: AbortSignal;
101
+ }
102
+
103
+ /**
104
+ * Creates a mock operation context for testing handlers.
105
+ *
106
+ * @param options - Context options
107
+ * @returns Mock operation context
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * // Basic usage
112
+ * const context = createMockContext();
113
+ *
114
+ * // With custom values
115
+ * const context = createMockContext({
116
+ * operationType: 'gather.data',
117
+ * previousResults: { url: 'https://example.com' },
118
+ * attempt: 2,
119
+ * });
120
+ *
121
+ * // Test handler
122
+ * const result = await myHandler(context);
123
+ * ```
124
+ */
125
+ export function createMockContext<T = Record<string, unknown>>(
126
+ options?: MockContextOptions,
127
+ ): OperationContext<T> {
128
+ return {
129
+ workflowId: options?.workflowId ?? 'test-workflow-id',
130
+ operationId: options?.operationId ?? 'test-operation-id',
131
+ operationType: options?.operationType ?? 'test.operation',
132
+ stageName: options?.stageName ?? 'test-stage',
133
+ attempt: options?.attempt ?? 1,
134
+ maxAttempts: options?.maxAttempts ?? 3,
135
+ previousResults: (options?.previousResults ?? {}) as T,
136
+ initialContext: options?.initialContext ?? {},
137
+ workflowType: options?.workflowType ?? 'test-workflow',
138
+ signal: options?.signal,
139
+ };
140
+ }
141
+
142
+ // =============================================================================
143
+ // Mock Results
144
+ // =============================================================================
145
+
146
+ /**
147
+ * Creates a successful operation result.
148
+ *
149
+ * @param data - Result data
150
+ * @returns Successful operation result
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const result = createSuccessResult({ processedCount: 10 });
155
+ * // { status: 'completed', data: { processedCount: 10 } }
156
+ * ```
157
+ */
158
+ export function createSuccessResult<T = Record<string, unknown>>(
159
+ data?: T,
160
+ ): OperationResult<T> {
161
+ return {
162
+ status: 'completed',
163
+ data,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Creates a failed operation result.
169
+ *
170
+ * @param reason - Failure reason
171
+ * @returns Failed operation result
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const result = createFailureResult('Network timeout');
176
+ * // { status: 'failed', reason: 'Network timeout' }
177
+ * ```
178
+ */
179
+ export function createFailureResult(reason: string): OperationResult {
180
+ return {
181
+ status: 'failed',
182
+ reason,
183
+ };
184
+ }
185
+
186
+ // =============================================================================
187
+ // Mock Workflow Records
188
+ // =============================================================================
189
+
190
+ /**
191
+ * Options for creating a mock workflow record.
192
+ */
193
+ export interface MockWorkflowOptions {
194
+ id?: string;
195
+ type?: string;
196
+ status?: WorkflowStatus;
197
+ context?: WorkflowContext;
198
+ currentStage?: string | null;
199
+ checkpointStatus?: string | null;
200
+ errorMessage?: string | null;
201
+ templateVersion?: string;
202
+ createdAt?: Date;
203
+ startedAt?: Date | null;
204
+ completedAt?: Date | null;
205
+ }
206
+
207
+ /**
208
+ * Creates a mock workflow record for testing.
209
+ *
210
+ * @param options - Workflow options
211
+ * @returns Mock workflow record
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const workflow = createMockWorkflow({
216
+ * status: 'active',
217
+ * currentStage: 'analyze',
218
+ * });
219
+ * ```
220
+ */
221
+ export function createMockWorkflow(options?: MockWorkflowOptions): WorkflowRecord {
222
+ return {
223
+ id: options?.id ?? 'test-workflow-id',
224
+ type: options?.type ?? 'test-workflow',
225
+ status: options?.status ?? 'pending',
226
+ context: options?.context ?? {},
227
+ currentStage: options?.currentStage ?? null,
228
+ checkpointStatus: options?.checkpointStatus ?? null,
229
+ errorMessage: options?.errorMessage ?? null,
230
+ templateVersion: options?.templateVersion ?? '1.0.0',
231
+ createdAt: options?.createdAt ?? new Date(),
232
+ startedAt: options?.startedAt ?? null,
233
+ completedAt: options?.completedAt ?? null,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Options for creating a mock operation record.
239
+ */
240
+ export interface MockOperationOptions {
241
+ id?: string;
242
+ workflowId?: string;
243
+ type?: string;
244
+ stage?: string;
245
+ status?: OperationStatus;
246
+ result?: Record<string, unknown> | null;
247
+ errorMessage?: string | null;
248
+ attempts?: number;
249
+ maxAttempts?: number;
250
+ createdAt?: Date;
251
+ startedAt?: Date | null;
252
+ completedAt?: Date | null;
253
+ }
254
+
255
+ /**
256
+ * Creates a mock operation record for testing.
257
+ *
258
+ * @param options - Operation options
259
+ * @returns Mock operation record
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * const operation = createMockOperation({
264
+ * type: 'gather.data',
265
+ * status: 'completed',
266
+ * result: { data: {} },
267
+ * });
268
+ * ```
269
+ */
270
+ export function createMockOperation(options?: MockOperationOptions): OperationRecord {
271
+ return {
272
+ id: options?.id ?? 'test-operation-id',
273
+ workflowId: options?.workflowId ?? 'test-workflow-id',
274
+ type: options?.type ?? 'test.operation',
275
+ stage: options?.stage ?? 'test-stage',
276
+ status: options?.status ?? 'pending',
277
+ result: options?.result ?? null,
278
+ errorMessage: options?.errorMessage ?? null,
279
+ attempts: options?.attempts ?? 0,
280
+ maxAttempts: options?.maxAttempts ?? 3,
281
+ createdAt: options?.createdAt ?? new Date(),
282
+ startedAt: options?.startedAt ?? null,
283
+ completedAt: options?.completedAt ?? null,
284
+ };
285
+ }
286
+
287
+ // =============================================================================
288
+ // Handler Testing
289
+ // =============================================================================
290
+
291
+ /**
292
+ * Tests an operation handler with the given context.
293
+ *
294
+ * Convenience wrapper that creates a mock context and runs the handler.
295
+ *
296
+ * @param handler - Handler to test
297
+ * @param options - Context options
298
+ * @returns Operation result
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * const result = await testHandler(myHandler, {
303
+ * previousResults: { url: 'https://example.com' },
304
+ * });
305
+ *
306
+ * expect(result.status).toBe('completed');
307
+ * expect(result.data?.fetchedData).toBeDefined();
308
+ * ```
309
+ */
310
+ export async function testHandler<TContext = Record<string, unknown>, TResult = Record<string, unknown>>(
311
+ handler: OperationHandler<TContext, TResult>,
312
+ options?: MockContextOptions,
313
+ ): Promise<OperationResult<TResult>> {
314
+ const context = createMockContext<TContext>(options);
315
+ return handler(context);
316
+ }
317
+
318
+ /**
319
+ * Creates a mock handler that returns a fixed result.
320
+ *
321
+ * Useful for testing workflows without running actual operations.
322
+ *
323
+ * @param result - Result to return
324
+ * @returns Mock handler
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const mockHandler = createMockHandler(createSuccessResult({ data: 'test' }));
329
+ * registry.register('test.operation', mockHandler);
330
+ * ```
331
+ */
332
+ export function createMockHandler<T = Record<string, unknown>>(
333
+ result: OperationResult<T>,
334
+ ): OperationHandler<Record<string, unknown>, T> {
335
+ return async () => result;
336
+ }
337
+
338
+ /**
339
+ * Creates a mock handler that calls a spy function.
340
+ *
341
+ * Useful for verifying handler calls in tests.
342
+ *
343
+ * @param spy - Spy function to call
344
+ * @param result - Result to return
345
+ * @returns Mock handler
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * const calls: OperationContext[] = [];
350
+ * const mockHandler = createSpyHandler(
351
+ * (ctx) => calls.push(ctx),
352
+ * createSuccessResult()
353
+ * );
354
+ *
355
+ * await testHandler(mockHandler);
356
+ * expect(calls.length).toBe(1);
357
+ * ```
358
+ */
359
+ export function createSpyHandler<T = Record<string, unknown>>(
360
+ spy: (context: OperationContext) => void,
361
+ result: OperationResult<T>,
362
+ ): OperationHandler<Record<string, unknown>, T> {
363
+ return async (context) => {
364
+ spy(context);
365
+ return result;
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Creates a mock handler that throws an error.
371
+ *
372
+ * Useful for testing error handling.
373
+ *
374
+ * @param error - Error to throw
375
+ * @returns Mock handler that throws
376
+ *
377
+ * @example
378
+ * ```typescript
379
+ * const errorHandler = createThrowingHandler(new Error('Network failure'));
380
+ *
381
+ * await expect(testHandler(errorHandler)).rejects.toThrow('Network failure');
382
+ * ```
383
+ */
384
+ export function createThrowingHandler(error: Error): OperationHandler {
385
+ return async () => {
386
+ throw error;
387
+ };
388
+ }
389
+
390
+ // =============================================================================
391
+ // Template Testing
392
+ // =============================================================================
393
+
394
+ /**
395
+ * Creates a minimal valid workflow template for testing.
396
+ *
397
+ * @param overrides - Template overrides
398
+ * @returns Valid workflow template
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * const template = createMockTemplate({
403
+ * type: 'my-workflow',
404
+ * stages: [
405
+ * { name: 'gather', operations: [{ type: 'gather.data' }] },
406
+ * ],
407
+ * });
408
+ * ```
409
+ */
410
+ export function createMockTemplate(
411
+ overrides?: Partial<WorkflowTemplate>,
412
+ ): WorkflowTemplate {
413
+ return {
414
+ type: overrides?.type ?? 'test-workflow',
415
+ queue: overrides?.queue ?? 'test-queue',
416
+ version: overrides?.version ?? '1.0.0',
417
+ description: overrides?.description ?? 'Test workflow',
418
+ stages: overrides?.stages ?? [
419
+ {
420
+ name: 'test-stage',
421
+ operations: [{ type: 'test.operation' }],
422
+ },
423
+ ],
424
+ checkpoints: overrides?.checkpoints,
425
+ nextWorkflow: overrides?.nextWorkflow,
426
+ coordination: overrides?.coordination,
427
+ queueConfig: overrides?.queueConfig,
428
+ estimatedDurationSeconds: overrides?.estimatedDurationSeconds,
429
+ };
430
+ }
431
+
432
+ // =============================================================================
433
+ // Assertions
434
+ // =============================================================================
435
+
436
+ /**
437
+ * Asserts that an operation result is successful.
438
+ *
439
+ * @param result - Result to check
440
+ * @throws Error if result is not successful
441
+ *
442
+ * @example
443
+ * ```typescript
444
+ * const result = await testHandler(myHandler);
445
+ * assertSuccess(result);
446
+ * // Type narrows to { status: 'completed', data: ... }
447
+ * ```
448
+ */
449
+ export function assertSuccess<T>(
450
+ result: OperationResult<T>,
451
+ ): asserts result is OperationResult<T> & { status: 'completed' } {
452
+ if (result.status !== 'completed') {
453
+ throw new Error(`Expected success but got failure: ${result.reason}`);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Asserts that an operation result is a failure.
459
+ *
460
+ * @param result - Result to check
461
+ * @throws Error if result is not a failure
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * const result = await testHandler(myHandler);
466
+ * assertFailure(result);
467
+ * // Type narrows to { status: 'failed', reason: ... }
468
+ * ```
469
+ */
470
+ export function assertFailure(
471
+ result: OperationResult,
472
+ ): asserts result is OperationResult & { status: 'failed' } {
473
+ if (result.status !== 'failed') {
474
+ throw new Error(`Expected failure but got success`);
475
+ }
476
+ }