@jagreehal/workflow 1.12.0 → 1.13.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 (42) hide show
  1. package/README.md +1197 -20
  2. package/dist/duration.cjs +2 -0
  3. package/dist/duration.cjs.map +1 -0
  4. package/dist/duration.d.cts +246 -0
  5. package/dist/duration.d.ts +246 -0
  6. package/dist/duration.js +2 -0
  7. package/dist/duration.js.map +1 -0
  8. package/dist/index.cjs +5 -5
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +3 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +5 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/match.cjs +2 -0
  15. package/dist/match.cjs.map +1 -0
  16. package/dist/match.d.cts +216 -0
  17. package/dist/match.d.ts +216 -0
  18. package/dist/match.js +2 -0
  19. package/dist/match.js.map +1 -0
  20. package/dist/schedule.cjs +2 -0
  21. package/dist/schedule.cjs.map +1 -0
  22. package/dist/schedule.d.cts +387 -0
  23. package/dist/schedule.d.ts +387 -0
  24. package/dist/schedule.js +2 -0
  25. package/dist/schedule.js.map +1 -0
  26. package/docs/api.md +30 -0
  27. package/docs/coming-from-neverthrow.md +103 -10
  28. package/docs/effect-features-to-port.md +210 -0
  29. package/docs/match-examples.test.ts +558 -0
  30. package/docs/match.md +417 -0
  31. package/docs/policies-examples.test.ts +750 -0
  32. package/docs/policies.md +508 -0
  33. package/docs/resource-management-examples.test.ts +729 -0
  34. package/docs/resource-management.md +509 -0
  35. package/docs/schedule-examples.test.ts +736 -0
  36. package/docs/schedule.md +467 -0
  37. package/docs/tagged-error-examples.test.ts +494 -0
  38. package/docs/tagged-error.md +730 -0
  39. package/docs/visualization-examples.test.ts +663 -0
  40. package/docs/visualization.md +395 -0
  41. package/docs/visualize-examples.md +1 -1
  42. package/package.json +17 -2
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Test file to verify all code examples in visualization.md actually work
3
+ * This file should compile and run without errors
4
+ */
5
+
6
+ import { describe, it, expect, } from "vitest";
7
+ import {
8
+ createWorkflow,
9
+ ok,
10
+ err,
11
+ type AsyncResult,
12
+ } from "../src/index";
13
+ import {
14
+ createVisualizer,
15
+ createEventCollector,
16
+ trackIf,
17
+ trackSwitch,
18
+ createTimeTravelController,
19
+ createPerformanceAnalyzer,
20
+ createLiveVisualizer,
21
+ } from "../src/visualize";
22
+
23
+ // ============================================================================
24
+ // Mock Dependencies
25
+ // ============================================================================
26
+
27
+ type User = { id: string; name: string; tier: string; region: string };
28
+ type Order = { id: string; items: string[] };
29
+ type Cart = { items: string[] };
30
+ type Payment = { amount: number };
31
+
32
+ async function fetchUser(id: string): AsyncResult<User, "USER_NOT_FOUND"> {
33
+ if (id === "404") return err("USER_NOT_FOUND");
34
+ return ok({ id, name: "Test User", tier: "premium", region: "US" });
35
+ }
36
+
37
+ async function fetchOrders(_id: string): AsyncResult<Order[], "ORDERS_ERROR"> {
38
+ return ok([{ id: "order-1", items: ["item-1"] }]);
39
+ }
40
+
41
+ async function fetchRecommendations(
42
+ _id: string
43
+ ): AsyncResult<string[], "REC_ERROR"> {
44
+ return ok(["rec-1", "rec-2"]);
45
+ }
46
+
47
+ async function fetchSettings(
48
+ _id: string
49
+ ): AsyncResult<{ theme: string }, "SETTINGS_ERROR"> {
50
+ return ok({ theme: "dark" });
51
+ }
52
+
53
+ async function validateCart(_cart: Cart): AsyncResult<boolean, "INVALID_CART"> {
54
+ return ok(true);
55
+ }
56
+
57
+ async function processPayment(
58
+ _payment: Payment
59
+ ): AsyncResult<string, "PAYMENT_FAILED"> {
60
+ return ok("payment-123");
61
+ }
62
+
63
+ async function applyDiscount(
64
+ percent: number
65
+ ): AsyncResult<number, "DISCOUNT_ERROR"> {
66
+ return ok(percent);
67
+ }
68
+
69
+ async function calculateTax(
70
+ _region: string
71
+ ): AsyncResult<number, "TAX_ERROR"> {
72
+ return ok(0.08);
73
+ }
74
+
75
+ // ============================================================================
76
+ // Basic Usage Examples
77
+ // ============================================================================
78
+
79
+ describe("visualization", () => {
80
+ describe("createVisualizer", () => {
81
+ it("creates visualizer and renders output", async () => {
82
+ const viz = createVisualizer({ workflowName: "checkout" });
83
+
84
+ const deps = {
85
+ validateCart,
86
+ processPayment,
87
+ };
88
+
89
+ const workflow = createWorkflow(deps, {
90
+ onEvent: viz.handleEvent,
91
+ });
92
+
93
+ await workflow(async (step, { validateCart, processPayment }) => {
94
+ await step(validateCart({ items: ["item-1"] }), {
95
+ name: "Validate cart",
96
+ });
97
+ await step(processPayment({ amount: 100 }), {
98
+ name: "Process payment",
99
+ });
100
+ return ok("done");
101
+ });
102
+
103
+ const output = viz.render();
104
+ expect(output).toContain("checkout");
105
+ expect(output).toContain("Validate cart");
106
+ expect(output).toContain("Process payment");
107
+ });
108
+
109
+ it("renders as mermaid", async () => {
110
+ const viz = createVisualizer({ workflowName: "test" });
111
+
112
+ const workflow = createWorkflow(
113
+ { fetchUser },
114
+ { onEvent: viz.handleEvent }
115
+ );
116
+
117
+ await workflow(async (step, { fetchUser }) => {
118
+ await step(fetchUser("123"), { name: "Fetch user" });
119
+ return ok("done");
120
+ });
121
+
122
+ const mermaid = viz.renderAs("mermaid");
123
+ expect(mermaid).toContain("flowchart");
124
+ });
125
+
126
+ it("renders as json", async () => {
127
+ const viz = createVisualizer({ workflowName: "test" });
128
+
129
+ const workflow = createWorkflow(
130
+ { fetchUser },
131
+ { onEvent: viz.handleEvent }
132
+ );
133
+
134
+ await workflow(async (step, { fetchUser }) => {
135
+ await step(fetchUser("123"), { name: "Fetch user" });
136
+ return ok("done");
137
+ });
138
+
139
+ const json = viz.renderAs("json");
140
+ const parsed = JSON.parse(json);
141
+ expect(parsed).toHaveProperty("root");
142
+ });
143
+ });
144
+
145
+ describe("createEventCollector", () => {
146
+ it("collects events for later visualization", async () => {
147
+ const collector = createEventCollector({ workflowName: "checkout" });
148
+
149
+ const workflow = createWorkflow(
150
+ { fetchUser, fetchOrders },
151
+ { onEvent: collector.handleEvent }
152
+ );
153
+
154
+ await workflow(async (step, { fetchUser, fetchOrders }) => {
155
+ await step(fetchUser("123"), { name: "Fetch user" });
156
+ await step(fetchOrders("123"), { name: "Fetch orders" });
157
+ return ok("done");
158
+ });
159
+
160
+ const events = collector.getEvents();
161
+ expect(events.length).toBeGreaterThan(0);
162
+
163
+ const workflowEvents = collector.getWorkflowEvents();
164
+ expect(workflowEvents.length).toBeGreaterThan(0);
165
+
166
+ const output = collector.visualize();
167
+ expect(output).toContain("Fetch user");
168
+ expect(output).toContain("Fetch orders");
169
+ });
170
+
171
+ it("can clear collected events", async () => {
172
+ const collector = createEventCollector();
173
+
174
+ const workflow = createWorkflow(
175
+ { fetchUser },
176
+ { onEvent: collector.handleEvent }
177
+ );
178
+
179
+ await workflow(async (step, { fetchUser }) => {
180
+ await step(fetchUser("123"));
181
+ return ok("done");
182
+ });
183
+
184
+ expect(collector.getEvents().length).toBeGreaterThan(0);
185
+
186
+ collector.clear();
187
+ expect(collector.getEvents().length).toBe(0);
188
+ });
189
+ });
190
+
191
+ describe("visualizer options", () => {
192
+ it("accepts configuration options", async () => {
193
+ const viz = createVisualizer({
194
+ workflowName: "checkout",
195
+ showTimings: true,
196
+ showKeys: true,
197
+ detectParallel: true,
198
+ });
199
+
200
+ expect(viz).toBeDefined();
201
+ expect(typeof viz.handleEvent).toBe("function");
202
+ expect(typeof viz.render).toBe("function");
203
+ });
204
+ });
205
+
206
+ describe("decision tracking", () => {
207
+ it("tracks if/else decisions", async () => {
208
+ const collector = createEventCollector();
209
+
210
+ const workflow = createWorkflow(
211
+ { fetchUser, applyDiscount },
212
+ { onEvent: collector.handleEvent }
213
+ );
214
+
215
+ await workflow(async (step, { fetchUser, applyDiscount }) => {
216
+ const user = await step(fetchUser("123"), { name: "Fetch user" });
217
+
218
+ const isPremium = trackIf("premium-check", user.tier === "premium", {
219
+ emit: collector.handleDecisionEvent,
220
+ });
221
+
222
+ if (user.tier === "premium") {
223
+ isPremium.takeBranch("premium");
224
+ await step(applyDiscount(20), { name: "Apply premium discount" });
225
+ } else {
226
+ isPremium.takeBranch("standard");
227
+ await step(applyDiscount(5), { name: "Apply standard discount" });
228
+ }
229
+ isPremium.end();
230
+
231
+ return ok("done");
232
+ });
233
+
234
+ const decisionEvents = collector.getDecisionEvents();
235
+ expect(decisionEvents.length).toBeGreaterThan(0);
236
+ });
237
+
238
+ it("tracks switch decisions", async () => {
239
+ const collector = createEventCollector();
240
+
241
+ const workflow = createWorkflow(
242
+ { fetchUser, calculateTax },
243
+ { onEvent: collector.handleEvent }
244
+ );
245
+
246
+ await workflow(async (step, { fetchUser, calculateTax }) => {
247
+ const user = await step(fetchUser("123"), { name: "Fetch user" });
248
+
249
+ const regionSwitch = trackSwitch("region", user.region, {
250
+ emit: collector.handleDecisionEvent,
251
+ });
252
+
253
+ switch (user.region) {
254
+ case "US":
255
+ regionSwitch.takeBranch("US");
256
+ await step(calculateTax("US"), { name: "Calculate US tax" });
257
+ break;
258
+ case "EU":
259
+ regionSwitch.takeBranch("EU");
260
+ await step(calculateTax("EU"), { name: "Calculate EU tax" });
261
+ break;
262
+ default:
263
+ regionSwitch.takeBranch("other");
264
+ }
265
+ regionSwitch.end();
266
+
267
+ return ok("done");
268
+ });
269
+
270
+ const output = collector.visualize();
271
+ expect(output).toBeDefined();
272
+ });
273
+ });
274
+
275
+ describe("time travel controller", () => {
276
+ it("navigates through workflow execution", async () => {
277
+ // Create time travel controller that receives events directly
278
+ const timeTravel = createTimeTravelController();
279
+
280
+ const workflow = createWorkflow(
281
+ { fetchUser, fetchOrders },
282
+ { onEvent: timeTravel.handleEvent }
283
+ );
284
+
285
+ await workflow(async (step, { fetchUser, fetchOrders }) => {
286
+ await step(fetchUser("123"), { name: "Fetch user" });
287
+ await step(fetchOrders("123"), { name: "Fetch orders" });
288
+ return ok("done");
289
+ });
290
+
291
+ // Navigate using the correct API
292
+ const ir0 = timeTravel.seek(0);
293
+ expect(ir0).toBeDefined();
294
+
295
+ const ir1 = timeTravel.stepForward();
296
+ expect(ir1).toBeDefined();
297
+
298
+ const ir2 = timeTravel.stepBackward();
299
+ expect(ir2).toBeDefined();
300
+
301
+ // Get state
302
+ const state = timeTravel.getState();
303
+ expect(state).toBeDefined();
304
+ });
305
+
306
+ it("supports state change subscription", async () => {
307
+ const timeTravel = createTimeTravelController();
308
+
309
+ const states: unknown[] = [];
310
+ const unsubscribe = timeTravel.onStateChange((state) => {
311
+ states.push(state);
312
+ });
313
+
314
+ const workflow = createWorkflow(
315
+ { fetchUser },
316
+ { onEvent: timeTravel.handleEvent }
317
+ );
318
+
319
+ await workflow(async (step, { fetchUser }) => {
320
+ await step(fetchUser("123"), { name: "Fetch user" });
321
+ return ok("done");
322
+ });
323
+
324
+ // Events trigger state changes during workflow execution
325
+ expect(states.length).toBeGreaterThan(0);
326
+
327
+ unsubscribe();
328
+ });
329
+ });
330
+
331
+ describe("performance analyzer", () => {
332
+ it("analyzes workflow execution patterns (exact markdown example)", async () => {
333
+ const analyzer = createPerformanceAnalyzer();
334
+
335
+ // Collect multiple runs
336
+ for (let i = 0; i < 3; i++) {
337
+ const collector = createEventCollector();
338
+ const workflow = createWorkflow(
339
+ { fetchUser, fetchOrders },
340
+ { onEvent: collector.handleEvent }
341
+ );
342
+
343
+ const startTime = Date.now();
344
+ await workflow(async (step, { fetchUser, fetchOrders }) => {
345
+ await step(() => fetchUser("123"), { name: "Fetch user" });
346
+ await step(() => fetchOrders("123"), { name: "Fetch orders" });
347
+ return ok("done");
348
+ });
349
+
350
+ analyzer.addRun({
351
+ id: `run-${i}`,
352
+ startTime,
353
+ events: collector.getEvents(),
354
+ });
355
+ }
356
+
357
+ // Analyze
358
+ const slowest = analyzer.getSlowestNodes(5);
359
+ const errorProne = analyzer.getErrorProneNodes(5);
360
+ const retryProne = analyzer.getRetryProneNodes(5);
361
+
362
+ expect(Array.isArray(slowest)).toBe(true);
363
+ expect(Array.isArray(errorProne)).toBe(true);
364
+ expect(Array.isArray(retryProne)).toBe(true);
365
+
366
+ // Get heatmap data for visualization
367
+ const collector = createEventCollector();
368
+ const workflow = createWorkflow(
369
+ { fetchUser },
370
+ { onEvent: collector.handleEvent }
371
+ );
372
+ await workflow(async (step, { fetchUser }) => {
373
+ await step(() => fetchUser("123"), { name: "Fetch user" });
374
+ return ok("done");
375
+ });
376
+
377
+ const viz = createVisualizer();
378
+ // Build IR from events for heatmap
379
+ collector.getEvents().forEach((e) => viz.handleEvent(e));
380
+ const ir = viz.getIR();
381
+
382
+ const heatmap = analyzer.getHeatmap(ir, "duration");
383
+ expect(heatmap).toHaveProperty("heat");
384
+ expect(heatmap).toHaveProperty("metric", "duration");
385
+ expect(heatmap).toHaveProperty("stats");
386
+ });
387
+ });
388
+
389
+ describe("parallel operations", () => {
390
+ it("visualizes parallel execution (using named object form)", async () => {
391
+ const viz = createVisualizer({ workflowName: "checkout" });
392
+
393
+ const deps = {
394
+ fetchUser,
395
+ fetchOrders,
396
+ fetchSettings,
397
+ };
398
+
399
+ const workflow = createWorkflow(deps, { onEvent: viz.handleEvent });
400
+
401
+ await workflow(
402
+ async (step, { fetchUser, fetchOrders, fetchSettings }) => {
403
+ // Use named object form - matches actual API
404
+ const { user, orders, settings } = await step.parallel(
405
+ {
406
+ user: () => fetchUser("123"),
407
+ orders: () => fetchOrders("123"),
408
+ settings: () => fetchSettings("123"),
409
+ },
410
+ { name: "Fetch all data" }
411
+ );
412
+
413
+ return ok({ user, orders, settings });
414
+ }
415
+ );
416
+
417
+ const output = viz.render();
418
+ expect(output).toContain("Fetch all data");
419
+ // Individual step names may not appear in parallel visualization
420
+ // but the parallel group should be visible
421
+ expect(output).toBeDefined();
422
+ });
423
+ });
424
+
425
+ describe("live visualization", () => {
426
+ it("updates terminal in real-time", async () => {
427
+ const live = createLiveVisualizer({
428
+ workflowName: "checkout",
429
+ clearOnUpdate: true, // Clear terminal between updates
430
+ });
431
+
432
+ const workflow = createWorkflow(
433
+ { fetchUser },
434
+ { onEvent: live.handleEvent }
435
+ );
436
+
437
+ live.start(); // Begin live updates
438
+
439
+ await workflow(async (step, { fetchUser }) => {
440
+ await step(() => fetchUser("123"), { name: "Processing..." });
441
+ return ok("done");
442
+ });
443
+
444
+ live.stop(); // Stop updates, show final state
445
+
446
+ const output = live.render();
447
+ expect(output).toContain("checkout");
448
+ expect(output).toContain("Processing...");
449
+ });
450
+ });
451
+
452
+ describe("visualizer reset", () => {
453
+ it("can reset for new workflow", async () => {
454
+ const viz = createVisualizer({ workflowName: "test" });
455
+
456
+ const workflow = createWorkflow(
457
+ { fetchUser },
458
+ { onEvent: viz.handleEvent }
459
+ );
460
+
461
+ await workflow(async (step, { fetchUser }) => {
462
+ await step(fetchUser("123"));
463
+ return ok("done");
464
+ });
465
+
466
+ const output1 = viz.render();
467
+ expect(output1).toBeDefined();
468
+
469
+ viz.reset();
470
+
471
+ const ir = viz.getIR();
472
+ expect(ir.root.children.length).toBe(0);
473
+ });
474
+ });
475
+
476
+ describe("update subscription", () => {
477
+ it("notifies on IR updates", async () => {
478
+ const viz = createVisualizer({ workflowName: "test" });
479
+ const updates: unknown[] = [];
480
+
481
+ const unsubscribe = viz.onUpdate((ir) => {
482
+ updates.push(ir);
483
+ });
484
+
485
+ const workflow = createWorkflow(
486
+ { fetchUser },
487
+ { onEvent: viz.handleEvent }
488
+ );
489
+
490
+ await workflow(async (step, { fetchUser }) => {
491
+ await step(fetchUser("123"), { name: "Fetch user" });
492
+ return ok("done");
493
+ });
494
+
495
+ expect(updates.length).toBeGreaterThan(0);
496
+
497
+ unsubscribe();
498
+ });
499
+
500
+ it("matches exact markdown time-travel example", async () => {
501
+ // Create controller that receives events directly
502
+ const timeTravel = createTimeTravelController();
503
+
504
+ const workflow = createWorkflow(
505
+ { fetchUser, fetchOrders },
506
+ { onEvent: timeTravel.handleEvent }
507
+ );
508
+
509
+ await workflow(async (step, { fetchUser, fetchOrders }) => {
510
+ await step(() => fetchUser("123"), { name: "Fetch user" });
511
+ await step(() => fetchOrders("123"), { name: "Fetch orders" });
512
+ return ok("done");
513
+ });
514
+
515
+ // Navigate
516
+ timeTravel.seek(0); // Jump to start
517
+ timeTravel.stepForward(); // Next event
518
+ timeTravel.stepBackward(); // Previous event
519
+ timeTravel.seek(5); // Jump to event 5
520
+
521
+ // Get current IR
522
+ const ir = timeTravel.getCurrentIR();
523
+ expect(ir).toBeDefined();
524
+
525
+ // Get state (includes position info)
526
+ const state = timeTravel.getState();
527
+ expect(state).toHaveProperty("currentIndex");
528
+ expect(state).toHaveProperty("snapshots");
529
+
530
+ // Subscribe to changes
531
+ const states: unknown[] = [];
532
+ const unsubscribe = timeTravel.onStateChange((s) => {
533
+ states.push(s);
534
+ });
535
+
536
+ // State changes happen during workflow execution, not during navigation
537
+ // The subscription was set up after workflow completed, so states may be empty
538
+ // This is expected behavior - state changes fire during event processing
539
+ unsubscribe();
540
+ });
541
+ });
542
+
543
+ describe("Best Practices", () => {
544
+ it("DO: Name your steps for readable output", async () => {
545
+ const collector = createEventCollector();
546
+ const workflow = createWorkflow(
547
+ { fetchUser, validateCart },
548
+ { onEvent: collector.handleEvent }
549
+ );
550
+
551
+ await workflow(async (step, { fetchUser, validateCart }) => {
552
+ // ✓ Clear names
553
+ await step(() => fetchUser("123"), {
554
+ name: "Fetch user profile",
555
+ });
556
+ await step(() => validateCart({ items: [] }), {
557
+ name: "Validate cart items",
558
+ });
559
+ return ok("done");
560
+ });
561
+
562
+ const events = collector.getWorkflowEvents();
563
+ // Check that events exist and have step information
564
+ expect(events.length).toBeGreaterThan(0);
565
+ // Events may have name in different properties depending on event type
566
+ const hasUserStep = events.some((e) => {
567
+ const event = e as { name?: string; stepName?: string };
568
+ return (
569
+ event.name === "Fetch user profile" ||
570
+ event.stepName === "Fetch user profile"
571
+ );
572
+ });
573
+ const hasCartStep = events.some((e) => {
574
+ const event = e as { name?: string; stepName?: string };
575
+ return (
576
+ event.name === "Validate cart items" ||
577
+ event.stepName === "Validate cart items"
578
+ );
579
+ });
580
+ expect(hasUserStep || hasCartStep).toBe(true);
581
+ });
582
+
583
+ it("DO: Use the event collector for tests", async () => {
584
+ const collector = createEventCollector();
585
+ const workflow = createWorkflow(
586
+ { fetchUser, fetchOrders, fetchSettings },
587
+ { onEvent: collector.handleEvent }
588
+ );
589
+
590
+ await workflow(
591
+ async (step, { fetchUser, fetchOrders, fetchSettings }) => {
592
+ await step(() => fetchUser("123"), { name: "Fetch user" });
593
+ await step(() => fetchOrders("123"), { name: "Validate input" });
594
+ await step(() => fetchSettings("123"), { name: "Process data" });
595
+ return ok("done");
596
+ }
597
+ );
598
+
599
+ const events = collector.getWorkflowEvents();
600
+ // Extract step names from events (may be in different properties)
601
+ const stepNames = events
602
+ .map((e) => {
603
+ const event = e as { name?: string; stepName?: string };
604
+ return event.name || event.stepName;
605
+ })
606
+ .filter((name): name is string => typeof name === "string");
607
+
608
+ // Verify we have the expected steps (order may vary)
609
+ expect(stepNames.length).toBeGreaterThanOrEqual(3);
610
+ expect(stepNames).toContain("Fetch user");
611
+ expect(stepNames).toContain("Validate input");
612
+ expect(stepNames).toContain("Process data");
613
+ });
614
+
615
+ it("DO: Track decisions that affect flow", async () => {
616
+ const collector = createEventCollector();
617
+ const workflow = createWorkflow(
618
+ { fetchUser, applyDiscount },
619
+ { onEvent: collector.handleEvent }
620
+ );
621
+
622
+ await workflow(async (step, { fetchUser, applyDiscount }) => {
623
+ const user = await step(() => fetchUser("123"), { name: "Fetch user" });
624
+
625
+ // Decisions show WHY a path was taken
626
+ const decision = trackIf(
627
+ "can-refund",
628
+ user.tier === "premium",
629
+ {
630
+ emit: collector.handleDecisionEvent,
631
+ }
632
+ );
633
+
634
+ if (user.tier === "premium") {
635
+ decision.takeBranch("premium");
636
+ await step(() => applyDiscount(20), { name: "Apply premium" });
637
+ } else {
638
+ decision.takeBranch("standard");
639
+ await step(() => applyDiscount(5), { name: "Apply standard" });
640
+ }
641
+ decision.end();
642
+
643
+ return ok("done");
644
+ });
645
+
646
+ const decisionEvents = collector.getDecisionEvents();
647
+ expect(decisionEvents.length).toBeGreaterThan(0);
648
+ // Visualization shows: ◇ can-refund [true] → Refund branch taken
649
+ });
650
+ });
651
+ });
652
+
653
+ // ============================================================================
654
+ // Export to avoid unused variable warnings
655
+ // ============================================================================
656
+
657
+ export {
658
+ fetchUser,
659
+ fetchOrders,
660
+ fetchRecommendations,
661
+ validateCart,
662
+ processPayment,
663
+ };