@bratsos/workflow-engine 0.1.0 → 0.2.0

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.
Files changed (37) hide show
  1. package/README.md +270 -513
  2. package/dist/chunk-HL3OJG7W.js +1033 -0
  3. package/dist/chunk-HL3OJG7W.js.map +1 -0
  4. package/dist/{chunk-7IITBLFY.js → chunk-NYKMT46J.js} +268 -25
  5. package/dist/chunk-NYKMT46J.js.map +1 -0
  6. package/dist/chunk-SPXBCZLB.js +17 -0
  7. package/dist/chunk-SPXBCZLB.js.map +1 -0
  8. package/dist/{client-5vz5Vv4A.d.ts → client-D4PoxADF.d.ts} +3 -143
  9. package/dist/client.d.ts +3 -2
  10. package/dist/{index-DmR3E8D7.d.ts → index-DAzCfO1R.d.ts} +20 -1
  11. package/dist/index.d.ts +234 -601
  12. package/dist/index.js +46 -2034
  13. package/dist/index.js.map +1 -1
  14. package/dist/{interface-Cv22wvLG.d.ts → interface-MMqhfQQK.d.ts} +69 -2
  15. package/dist/kernel/index.d.ts +26 -0
  16. package/dist/kernel/index.js +3 -0
  17. package/dist/kernel/index.js.map +1 -0
  18. package/dist/kernel/testing/index.d.ts +44 -0
  19. package/dist/kernel/testing/index.js +85 -0
  20. package/dist/kernel/testing/index.js.map +1 -0
  21. package/dist/persistence/index.d.ts +2 -2
  22. package/dist/persistence/index.js +2 -1
  23. package/dist/persistence/prisma/index.d.ts +2 -2
  24. package/dist/persistence/prisma/index.js +2 -1
  25. package/dist/plugins-BCnDUwIc.d.ts +415 -0
  26. package/dist/ports-tU3rzPXJ.d.ts +245 -0
  27. package/dist/stage-BPw7m9Wx.d.ts +144 -0
  28. package/dist/testing/index.d.ts +23 -1
  29. package/dist/testing/index.js +156 -13
  30. package/dist/testing/index.js.map +1 -1
  31. package/package.json +11 -1
  32. package/skills/workflow-engine/SKILL.md +234 -348
  33. package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
  34. package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
  35. package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
  36. package/skills/workflow-engine/references/08-common-patterns.md +118 -431
  37. package/dist/chunk-7IITBLFY.js.map +0 -1
@@ -1,546 +1,213 @@
1
1
  # Testing Patterns
2
2
 
3
- Complete guide for testing workflow stages and complete workflows.
3
+ Complete guide for testing with the command kernel using in-memory adapters.
4
4
 
5
- ## Test Utilities
5
+ ## In-Memory Adapters
6
6
 
7
- The workflow engine provides testing utilities:
7
+ The engine provides in-memory implementations for all ports:
8
8
 
9
9
  ```typescript
10
+ // Persistence and job queue
10
11
  import {
11
- TestWorkflowPersistence,
12
- TestJobQueue,
13
- MockAIHelper,
12
+ InMemoryWorkflowPersistence,
13
+ InMemoryJobQueue,
14
+ InMemoryAICallLogger,
14
15
  } from "@bratsos/workflow-engine/testing";
15
- ```
16
-
17
- ## TestWorkflowPersistence
18
-
19
- In-memory implementation of `WorkflowPersistence` for testing:
20
-
21
- ```typescript
22
- const persistence = new TestWorkflowPersistence();
23
-
24
- // Create a run
25
- const run = await persistence.createRun({
26
- workflowId: "test-workflow",
27
- workflowName: "Test Workflow",
28
- workflowType: "test",
29
- input: { data: "test" },
30
- });
31
16
 
32
- // Update and query
33
- await persistence.updateRun(run.id, { status: "RUNNING" });
34
- const retrieved = await persistence.getRun(run.id);
35
-
36
- // Reset between tests
37
- persistence.reset();
17
+ // Kernel-specific test adapters
18
+ import {
19
+ FakeClock,
20
+ InMemoryBlobStore,
21
+ CollectingEventSink,
22
+ NoopScheduler,
23
+ } from "@bratsos/workflow-engine/kernel/testing";
38
24
  ```
39
25
 
40
- ## TestJobQueue
26
+ ## Test Kernel Setup
41
27
 
42
- In-memory implementation of `JobQueue`:
28
+ Create a fully in-memory kernel for testing:
43
29
 
44
30
  ```typescript
45
- const jobQueue = new TestJobQueue();
31
+ import { createKernel } from "@bratsos/workflow-engine/kernel";
46
32
 
47
- // Enqueue a job
48
- const jobId = await jobQueue.enqueue({
49
- workflowRunId: "run-123",
50
- stageId: "stage-1",
51
- priority: 5,
52
- });
53
-
54
- // Dequeue
55
- const job = await jobQueue.dequeue();
33
+ function createTestKernel(workflows: Map<string, Workflow>) {
34
+ const persistence = new InMemoryWorkflowPersistence();
35
+ const jobQueue = new InMemoryJobQueue();
36
+ const blobStore = new InMemoryBlobStore();
37
+ const eventSink = new CollectingEventSink();
38
+ const scheduler = new NoopScheduler();
39
+ const clock = new FakeClock();
56
40
 
57
- // Complete or fail
58
- await jobQueue.complete(jobId);
59
- await jobQueue.fail(jobId, "Error message", true); // retry=true
41
+ const kernel = createKernel({
42
+ persistence,
43
+ blobStore,
44
+ jobTransport: jobQueue,
45
+ eventSink,
46
+ scheduler,
47
+ clock,
48
+ registry: { getWorkflow: (id) => workflows.get(id) },
49
+ });
60
50
 
61
- // Reset between tests
62
- jobQueue.reset();
51
+ return { kernel, persistence, jobQueue, blobStore, eventSink, scheduler, clock };
52
+ }
63
53
  ```
64
54
 
65
- ## MockAIHelper
66
-
67
- Mock implementation of `AIHelper` for predictable test results:
55
+ ## Full Workflow Lifecycle Test
68
56
 
69
57
  ```typescript
70
- const mockAI = new MockAIHelper();
71
-
72
- // Mock generateText responses
73
- mockAI.mockGenerateText("Expected text response");
74
- mockAI.mockGenerateText("Second response"); // Queue multiple
75
-
76
- // Mock generateObject responses
77
- mockAI.mockGenerateObject({
78
- title: "Test Title",
79
- tags: ["test", "mock"],
58
+ import { describe, it, expect, beforeEach } from "vitest";
59
+ import { defineStage, WorkflowBuilder } from "@bratsos/workflow-engine";
60
+ import { z } from "zod";
61
+
62
+ const echoStage = defineStage({
63
+ id: "echo",
64
+ name: "Echo",
65
+ schemas: {
66
+ input: z.object({ message: z.string() }),
67
+ output: z.object({ echoed: z.string() }),
68
+ config: z.object({}),
69
+ },
70
+ async execute(ctx) {
71
+ return { output: { echoed: ctx.input.message } };
72
+ },
80
73
  });
81
74
 
82
- // Mock embeddings
83
- mockAI.mockEmbed([0.1, 0.2, 0.3, /* ... */]);
75
+ const workflow = new WorkflowBuilder(
76
+ "echo-wf", "Echo WF", "Test",
77
+ z.object({ message: z.string() }),
78
+ z.object({ echoed: z.string() }),
79
+ )
80
+ .pipe(echoStage)
81
+ .build();
84
82
 
85
- // Mock errors
86
- mockAI.mockError(new Error("API rate limit"));
87
-
88
- // Use in tests
89
- const result = await mockAI.generateText("gemini-2.5-flash", "Any prompt");
90
- // Returns "Expected text response"
91
-
92
- // Check calls
93
- console.log(mockAI.calls.length); // Number of calls
94
- console.log(mockAI.calls[0].modelKey); // Model used
95
- console.log(mockAI.calls[0].prompt); // Prompt sent
96
-
97
- // Reset between tests
98
- mockAI.reset();
99
- ```
100
-
101
- ## Testing Individual Stages
102
-
103
- ### Unit Testing a Sync Stage
104
-
105
- ```typescript
106
- import { describe, it, expect, beforeEach } from "vitest";
107
- import { TestWorkflowPersistence, MockAIHelper } from "@bratsos/workflow-engine/testing";
108
- import { myStage } from "./my-stage";
109
-
110
- describe("myStage", () => {
111
- let persistence: TestWorkflowPersistence;
112
- let mockAI: MockAIHelper;
83
+ describe("echo workflow", () => {
84
+ let kernel, persistence, jobQueue;
113
85
 
114
86
  beforeEach(() => {
115
- persistence = new TestWorkflowPersistence();
116
- mockAI = new MockAIHelper();
87
+ const t = createTestKernel(new Map([["echo-wf", workflow]]));
88
+ kernel = t.kernel;
89
+ persistence = t.persistence;
90
+ jobQueue = t.jobQueue;
117
91
  });
118
92
 
119
- it("should process input correctly", async () => {
120
- // Arrange
121
- const input = { data: "test input" };
122
- const config = { verbose: true };
123
-
124
- // Create context
125
- const ctx = {
126
- input,
127
- config,
128
- workflowContext: {},
129
- workflowRunId: "test-run",
130
- stageId: "my-stage",
131
- log: async () => {},
132
- storage: persistence.createStorage("test-run"),
133
- require: (id: string) => {
134
- throw new Error(`Stage ${id} not found`);
135
- },
136
- optional: (id: string) => undefined,
137
- };
138
-
139
- // Act
140
- const result = await myStage.execute(ctx);
141
-
142
- // Assert
143
- expect(result.output).toBeDefined();
144
- expect(result.output.processedData).toBe("TEST INPUT");
145
- });
93
+ it("completes a single-stage workflow", async () => {
94
+ // 1. Create the run
95
+ const { workflowRunId } = await kernel.dispatch({
96
+ type: "run.create",
97
+ idempotencyKey: "test-1",
98
+ workflowId: "echo-wf",
99
+ input: { message: "hello" },
100
+ });
146
101
 
147
- it("should use AI helper correctly", async () => {
148
- // Mock AI response
149
- mockAI.mockGenerateObject({ classification: "tech" });
102
+ // 2. Claim pending runs (enqueues first-stage job)
103
+ await kernel.dispatch({
104
+ type: "run.claimPending",
105
+ workerId: "test-worker",
106
+ });
150
107
 
151
- const ctx = {
152
- input: { text: "AI content" },
153
- config: {},
154
- workflowContext: {},
155
- workflowRunId: "test-run",
156
- stageId: "ai-stage",
157
- log: async () => {},
158
- storage: persistence.createStorage("test-run"),
159
- require: () => ({}),
160
- optional: () => undefined,
161
- // Inject mock AI helper
162
- ai: mockAI,
163
- };
164
-
165
- const result = await aiStage.execute(ctx);
166
-
167
- // Verify AI was called
168
- expect(mockAI.calls.length).toBe(1);
169
- expect(mockAI.calls[0].modelKey).toBe("gemini-2.5-flash");
170
-
171
- // Verify result
172
- expect(result.output.classification).toBe("tech");
173
- });
108
+ // 3. Dequeue and execute the job
109
+ const job = await jobQueue.dequeue();
110
+ expect(job).not.toBeNull();
174
111
 
175
- it("should handle errors gracefully", async () => {
176
- mockAI.mockError(new Error("API error"));
112
+ await kernel.dispatch({
113
+ type: "job.execute",
114
+ workflowRunId: job.workflowRunId,
115
+ workflowId: job.workflowId,
116
+ stageId: job.stageId,
117
+ config: {},
118
+ });
119
+ await jobQueue.complete(job.jobId);
177
120
 
178
- const ctx = createTestContext({ ai: mockAI });
121
+ // 4. Transition the workflow
122
+ const { action } = await kernel.dispatch({
123
+ type: "run.transition",
124
+ workflowRunId,
125
+ });
126
+ expect(action).toBe("completed");
179
127
 
180
- await expect(myStage.execute(ctx)).rejects.toThrow("API error");
128
+ // 5. Verify final state
129
+ const run = await persistence.getRun(workflowRunId);
130
+ expect(run.status).toBe("COMPLETED");
181
131
  });
182
132
  });
183
133
  ```
184
134
 
185
- ### Testing Stage with Dependencies
135
+ ## FakeClock
186
136
 
187
- ```typescript
188
- it("should access previous stage output", async () => {
189
- // Setup workflow context with previous stage output
190
- const workflowContext = {
191
- "data-extraction": {
192
- title: "Test Document",
193
- sections: [{ heading: "Intro", content: "..." }],
194
- },
195
- };
196
-
197
- const ctx = {
198
- input: {},
199
- config: {},
200
- workflowContext,
201
- workflowRunId: "test-run",
202
- stageId: "processing-stage",
203
- log: async () => {},
204
- storage: persistence.createStorage("test-run"),
205
- require: (id: string) => {
206
- const output = workflowContext[id];
207
- if (!output) throw new Error(`Missing ${id}`);
208
- return output;
209
- },
210
- optional: (id: string) => workflowContext[id],
211
- };
212
-
213
- const result = await processingStage.execute(ctx);
214
-
215
- expect(result.output.processedTitle).toBe("Test Document");
216
- });
217
- ```
218
-
219
- ## Testing Async Batch Stages
220
-
221
- ### Testing Execute (Suspension)
137
+ Injectable time source for deterministic testing:
222
138
 
223
139
  ```typescript
224
- it("should suspend and return batch info", async () => {
225
- const ctx = createTestContext({
226
- workflowContext: {
227
- "data-extraction": { items: ["a", "b", "c"] },
228
- },
229
- });
140
+ const clock = new FakeClock();
141
+ clock.now(); // Returns frozen time
230
142
 
231
- const result = await batchStage.execute(ctx);
232
-
233
- // Verify suspended result
234
- expect(result.suspended).toBe(true);
235
- expect(result.state.batchId).toBeDefined();
236
- expect(result.state.pollInterval).toBe(60000);
237
- expect(result.pollConfig.nextPollAt).toBeInstanceOf(Date);
238
- });
143
+ // Advance time for testing stale leases, poll intervals, etc.
144
+ clock.advance(60_000); // Advance 60 seconds
239
145
  ```
240
146
 
241
- ### Testing Execute (Resume)
242
-
243
- ```typescript
244
- it("should return cached results on resume", async () => {
245
- // Setup storage with cached results
246
- await persistence.saveArtifact({
247
- workflowRunId: "test-run",
248
- key: "batch-result",
249
- type: "ARTIFACT",
250
- data: { items: ["processed-a", "processed-b"] },
251
- size: 100,
252
- });
253
-
254
- const ctx = createTestContext({
255
- resumeState: {
256
- batchId: "batch-123",
257
- submittedAt: new Date().toISOString(),
258
- pollInterval: 60000,
259
- maxWaitTime: 3600000,
260
- },
261
- });
147
+ ## CollectingEventSink
262
148
 
263
- const result = await batchStage.execute(ctx);
264
-
265
- expect(result.suspended).toBeUndefined();
266
- expect(result.output.items).toEqual(["processed-a", "processed-b"]);
267
- });
268
- ```
269
-
270
- ### Testing checkCompletion
149
+ Captures events for assertions:
271
150
 
272
151
  ```typescript
273
- import { vi } from "vitest";
152
+ const eventSink = new CollectingEventSink();
274
153
 
275
- describe("checkCompletion", () => {
276
- it("should return not ready when batch is processing", async () => {
277
- // Mock batch API
278
- vi.spyOn(batchProvider, "getStatus").mockResolvedValue({
279
- status: "processing",
280
- });
281
-
282
- const result = await batchStage.checkCompletion(
283
- { batchId: "batch-123", submittedAt: "...", pollInterval: 60000, maxWaitTime: 3600000 },
284
- { workflowRunId: "test-run", stageId: "batch-stage", config: {}, log: async () => {}, storage: persistence.createStorage("test-run") }
285
- );
286
-
287
- expect(result.ready).toBe(false);
288
- expect(result.nextCheckIn).toBe(60000);
289
- });
154
+ // ... run workflow ...
290
155
 
291
- it("should return results when batch is completed", async () => {
292
- vi.spyOn(batchProvider, "getStatus").mockResolvedValue({ status: "completed" });
293
- vi.spyOn(batchProvider, "getResults").mockResolvedValue([
294
- { id: "req-1", result: "processed", inputTokens: 100, outputTokens: 50, status: "succeeded" },
295
- ]);
156
+ // Flush outbox to publish events
157
+ await kernel.dispatch({ type: "outbox.flush" });
296
158
 
297
- const result = await batchStage.checkCompletion(
298
- { batchId: "batch-123", submittedAt: "...", pollInterval: 60000, maxWaitTime: 3600000 },
299
- createCheckContext()
300
- );
301
-
302
- expect(result.ready).toBe(true);
303
- expect(result.output.items).toHaveLength(1);
304
- });
305
-
306
- it("should return error when batch fails", async () => {
307
- vi.spyOn(batchProvider, "getStatus").mockResolvedValue({ status: "failed" });
308
-
309
- const result = await batchStage.checkCompletion(
310
- { batchId: "batch-123", submittedAt: "...", pollInterval: 60000, maxWaitTime: 3600000 },
311
- createCheckContext()
312
- );
313
-
314
- expect(result.ready).toBe(false);
315
- expect(result.error).toBeDefined();
316
- });
317
- });
159
+ // Assert on collected events
160
+ expect(eventSink.events).toContainEqual(
161
+ expect.objectContaining({ type: "workflow:completed" })
162
+ );
318
163
  ```
319
164
 
320
- ## Integration Testing Workflows
321
-
322
- ### Testing Complete Workflow
165
+ ## Testing Idempotency
323
166
 
324
167
  ```typescript
325
- import { WorkflowExecutor } from "@bratsos/workflow-engine";
326
-
327
- describe("documentWorkflow integration", () => {
328
- let persistence: TestWorkflowPersistence;
329
- let mockAI: MockAIHelper;
330
-
331
- beforeEach(() => {
332
- persistence = new TestWorkflowPersistence();
333
- mockAI = new MockAIHelper();
334
-
335
- // Setup mock responses for all AI calls
336
- mockAI.mockGenerateObject({ title: "Test", sections: [] });
337
- mockAI.mockGenerateObject({ categories: ["tech"] });
338
- mockAI.mockGenerateText("This is a summary.");
339
- });
168
+ it("deduplicates run.create with same key", async () => {
169
+ const cmd = {
170
+ type: "run.create" as const,
171
+ idempotencyKey: "same-key",
172
+ workflowId: "echo-wf",
173
+ input: { message: "hello" },
174
+ };
340
175
 
341
- it("should execute complete workflow", async () => {
342
- // Create run record
343
- const run = await persistence.createRun({
344
- workflowId: "document-analysis",
345
- workflowName: "Document Analysis",
346
- workflowType: "document-analysis",
347
- input: { documentUrl: "https://example.com/doc.pdf" },
348
- });
176
+ const first = await kernel.dispatch(cmd);
177
+ const second = await kernel.dispatch(cmd);
349
178
 
350
- // Create executor
351
- const executor = new WorkflowExecutor(
352
- documentWorkflow,
353
- run.id,
354
- "document-analysis",
355
- {
356
- persistence,
357
- aiLogger: createMockAILogger(),
358
- }
359
- );
360
-
361
- // Execute
362
- const result = await executor.execute(
363
- { documentUrl: "https://example.com/doc.pdf" },
364
- documentWorkflow.getDefaultConfig()
365
- );
366
-
367
- // Verify
368
- expect(result.status).toBe("COMPLETED");
369
-
370
- // Check stages were created
371
- const stages = await persistence.getStagesByRun(run.id);
372
- expect(stages.length).toBe(4);
373
- expect(stages.every(s => s.status === "COMPLETED")).toBe(true);
374
- });
179
+ expect(first.workflowRunId).toBe(second.workflowRunId);
375
180
  });
376
181
  ```
377
182
 
378
- ### Testing with Runtime
183
+ ## Testing Cancellation
379
184
 
380
185
  ```typescript
381
- describe("workflow with runtime", () => {
382
- let runtime: WorkflowRuntime;
383
- let persistence: TestWorkflowPersistence;
384
- let jobQueue: TestJobQueue;
385
-
386
- beforeEach(() => {
387
- persistence = new TestWorkflowPersistence();
388
- jobQueue = new TestJobQueue();
389
-
390
- runtime = createWorkflowRuntime({
391
- persistence,
392
- jobQueue,
393
- registry: { getWorkflow: (id) => workflows[id] ?? null },
394
- pollIntervalMs: 100,
395
- jobPollIntervalMs: 50,
396
- });
186
+ it("cancels a running workflow", async () => {
187
+ const { workflowRunId } = await kernel.dispatch({
188
+ type: "run.create",
189
+ idempotencyKey: "cancel-test",
190
+ workflowId: "echo-wf",
191
+ input: { message: "hello" },
397
192
  });
398
193
 
399
- afterEach(() => {
400
- runtime.stop();
401
- });
402
-
403
- it("should process workflow through runtime", async () => {
404
- // Create run
405
- const { workflowRunId } = await runtime.createRun({
406
- workflowId: "simple-workflow",
407
- input: { data: "test" },
408
- });
409
-
410
- // Start runtime (in test mode)
411
- await runtime.start();
194
+ await kernel.dispatch({ type: "run.claimPending", workerId: "w1" });
412
195
 
413
- // Wait for completion
414
- await waitFor(async () => {
415
- const run = await persistence.getRun(workflowRunId);
416
- return run?.status === "COMPLETED";
417
- }, { timeout: 5000 });
418
-
419
- // Verify
420
- const run = await persistence.getRun(workflowRunId);
421
- expect(run?.status).toBe("COMPLETED");
196
+ const { cancelled } = await kernel.dispatch({
197
+ type: "run.cancel",
198
+ workflowRunId,
199
+ reason: "User cancelled",
422
200
  });
201
+ expect(cancelled).toBe(true);
423
202
  });
424
203
  ```
425
204
 
426
- ## Test Helpers
427
-
428
- ### Creating Test Context
429
-
430
- ```typescript
431
- function createTestContext<TInput, TConfig>(
432
- overrides: Partial<{
433
- input: TInput;
434
- config: TConfig;
435
- workflowContext: Record<string, unknown>;
436
- resumeState: unknown;
437
- ai: MockAIHelper;
438
- }> = {}
439
- ) {
440
- const persistence = new TestWorkflowPersistence();
441
-
442
- return {
443
- input: overrides.input ?? {},
444
- config: overrides.config ?? {},
445
- workflowContext: overrides.workflowContext ?? {},
446
- workflowRunId: "test-run",
447
- stageId: "test-stage",
448
- resumeState: overrides.resumeState,
449
- log: async (level: string, message: string) => {
450
- console.log(`[${level}] ${message}`);
451
- },
452
- storage: persistence.createStorage("test-run"),
453
- require: (id: string) => {
454
- const output = overrides.workflowContext?.[id];
455
- if (!output) throw new Error(`Missing required stage: ${id}`);
456
- return output;
457
- },
458
- optional: (id: string) => overrides.workflowContext?.[id],
459
- };
460
- }
461
- ```
462
-
463
- ### Wait Helper
464
-
465
- ```typescript
466
- async function waitFor(
467
- condition: () => Promise<boolean>,
468
- options: { timeout?: number; interval?: number } = {}
469
- ): Promise<void> {
470
- const { timeout = 5000, interval = 100 } = options;
471
- const start = Date.now();
472
-
473
- while (Date.now() - start < timeout) {
474
- if (await condition()) return;
475
- await new Promise(r => setTimeout(r, interval));
476
- }
477
-
478
- throw new Error(`Condition not met within ${timeout}ms`);
479
- }
480
- ```
481
-
482
- ### Mock AI Logger
483
-
484
- ```typescript
485
- function createMockAILogger(): AICallLogger {
486
- const calls: CreateAICallInput[] = [];
487
-
488
- return {
489
- logCall: (call) => { calls.push(call); },
490
- logBatchResults: async (batchId, results) => { calls.push(...results); },
491
- getStats: async (topic) => ({
492
- totalCalls: calls.filter(c => c.topic.startsWith(topic)).length,
493
- totalInputTokens: 0,
494
- totalOutputTokens: 0,
495
- totalCost: 0,
496
- perModel: {},
497
- }),
498
- isRecorded: async (batchId) => calls.some(c => c.metadata?.batchId === batchId),
499
- };
500
- }
501
- ```
502
-
503
- ## Best Practices
504
-
505
- ### 1. Isolate Tests
205
+ ## Reset Between Tests
506
206
 
507
207
  ```typescript
508
208
  beforeEach(() => {
509
- persistence.reset();
510
- jobQueue.reset();
511
- mockAI.reset();
512
- });
513
- ```
514
-
515
- ### 2. Test Edge Cases
516
-
517
- ```typescript
518
- it("should handle empty input", async () => { ... });
519
- it("should handle missing dependencies", async () => { ... });
520
- it("should handle AI errors", async () => { ... });
521
- it("should handle timeout", async () => { ... });
522
- ```
523
-
524
- ### 3. Use Type-Safe Mocks
525
-
526
- ```typescript
527
- // Bad: loosely typed
528
- const ctx = { input: {}, config: {} } as any;
529
-
530
- // Good: properly typed
531
- const ctx = createTestContext<InputType, ConfigType>({
532
- input: { data: "test" },
533
- config: { verbose: true },
534
- });
535
- ```
536
-
537
- ### 4. Test Metrics and Artifacts
538
-
539
- ```typescript
540
- it("should record metrics", async () => {
541
- const result = await stage.execute(ctx);
542
-
543
- expect(result.customMetrics?.itemsProcessed).toBe(10);
544
- expect(result.artifacts?.rawData).toBeDefined();
209
+ persistence.clear();
210
+ jobQueue.clear();
211
+ eventSink.events = [];
545
212
  });
546
213
  ```