@multiplier-labs/stepflow 0.3.1 → 0.3.3

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 (84) hide show
  1. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  2. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  3. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  4. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  5. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  6. package/.github/CODEOWNERS +15 -0
  7. package/.github/dependabot.yml +23 -0
  8. package/.github/workflows/ci.yml +38 -0
  9. package/.github/workflows/publish.yml +11 -5
  10. package/.github/workflows/security.yml +29 -0
  11. package/CHANGELOG.md +34 -0
  12. package/README.md +24 -0
  13. package/SECURITY.md +40 -0
  14. package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
  15. package/dist/chunk-4VOKMGP6.js.map +1 -0
  16. package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
  17. package/dist/chunk-GYPNJBDP.js.map +1 -0
  18. package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
  19. package/dist/chunk-KHTUWLVZ.js.map +1 -0
  20. package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
  21. package/dist/chunk-ML35PIHX.js.map +1 -0
  22. package/dist/events/index.d.ts +4 -4
  23. package/dist/events/index.js +1 -1
  24. package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
  25. package/dist/index.d.ts +14 -12
  26. package/dist/index.js +40 -82
  27. package/dist/index.js.map +1 -1
  28. package/dist/scheduler/index.d.ts +4 -4
  29. package/dist/scheduler/index.js +2 -2
  30. package/dist/storage/index.d.ts +13 -13
  31. package/dist/storage/index.js +2 -2
  32. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  33. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  34. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  35. package/docs/stepflow-api-reference.md +2 -3
  36. package/package.json +7 -6
  37. package/src/core/engine.test.ts +466 -522
  38. package/src/core/engine.ts +42 -67
  39. package/src/core/index.ts +3 -3
  40. package/src/core/orchestrator.ts +64 -83
  41. package/src/core/types.ts +7 -29
  42. package/src/events/index.ts +4 -4
  43. package/src/events/memory.test.ts +39 -41
  44. package/src/events/memory.ts +7 -10
  45. package/src/events/socketio.test.ts +112 -162
  46. package/src/events/socketio.ts +20 -27
  47. package/src/events/types.ts +16 -19
  48. package/src/events/webhook.test.ts +197 -235
  49. package/src/events/webhook.ts +44 -71
  50. package/src/index.ts +19 -19
  51. package/src/planning/index.ts +6 -3
  52. package/src/planning/planner.ts +66 -91
  53. package/src/planning/planning.test.ts +303 -366
  54. package/src/planning/registry.test.ts +204 -251
  55. package/src/planning/registry.ts +39 -43
  56. package/src/planning/types.ts +23 -27
  57. package/src/scheduler/cron.test.ts +141 -150
  58. package/src/scheduler/cron.ts +32 -67
  59. package/src/scheduler/index.ts +4 -14
  60. package/src/scheduler/postgres-persistence.ts +71 -99
  61. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  62. package/src/scheduler/sqlite-persistence.ts +12 -19
  63. package/src/scheduler/types.ts +4 -9
  64. package/src/storage/index.ts +5 -5
  65. package/src/storage/memory.test.ts +133 -183
  66. package/src/storage/memory.ts +21 -42
  67. package/src/storage/postgres.ts +197 -393
  68. package/src/storage/sqlite.test.ts +154 -281
  69. package/src/storage/sqlite.ts +50 -85
  70. package/src/storage/types.ts +33 -58
  71. package/src/utils/errors.test.ts +78 -80
  72. package/src/utils/errors.ts +27 -57
  73. package/src/utils/id.ts +3 -3
  74. package/src/utils/index.ts +4 -4
  75. package/src/utils/logger.test.ts +52 -58
  76. package/src/utils/logger.ts +4 -4
  77. package/src/utils/postgres-deps.ts +4 -4
  78. package/src/utils/retry.test.ts +47 -67
  79. package/src/utils/retry.ts +9 -9
  80. package/dist/chunk-FQCT3I2A.js.map +0 -1
  81. package/dist/chunk-GXY66V7Q.js.map +0 -1
  82. package/dist/chunk-U5QGN3RT.js.map +0 -1
  83. package/dist/chunk-XCF4PLN7.js.map +0 -1
  84. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,96 +1,87 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import {
3
- MemoryStepHandlerRegistry,
4
- MemoryRecipeRegistry,
5
- createRegistry,
6
- } from "./registry";
7
- import type { Recipe, RegisteredStepHandler } from "./types";
8
-
9
- describe("MemoryStepHandlerRegistry", () => {
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { MemoryStepHandlerRegistry, MemoryRecipeRegistry, createRegistry } from './registry';
3
+ import type { Recipe, RegisteredStepHandler } from './types';
4
+
5
+ describe('MemoryStepHandlerRegistry', () => {
10
6
  let registry: MemoryStepHandlerRegistry;
11
7
 
12
8
  beforeEach(() => {
13
9
  registry = new MemoryStepHandlerRegistry();
14
10
  });
15
11
 
16
- it("should register and retrieve a handler", () => {
12
+ it('should register and retrieve a handler', () => {
17
13
  const handler: RegisteredStepHandler = {
18
- id: "handler.a",
19
- description: "Handler A",
20
- handler: async () => "result",
14
+ id: 'handler.a',
15
+ description: 'Handler A',
16
+ handler: async () => 'result',
21
17
  };
22
18
 
23
19
  registry.register(handler);
24
20
 
25
- expect(registry.get("handler.a")).toBeDefined();
26
- expect(registry.get("handler.a")?.id).toBe("handler.a");
27
- expect(registry.has("handler.a")).toBe(true);
21
+ expect(registry.get('handler.a')).toBeDefined();
22
+ expect(registry.get('handler.a')?.id).toBe('handler.a');
23
+ expect(registry.has('handler.a')).toBe(true);
28
24
  });
29
25
 
30
- it("should throw on duplicate registration", () => {
31
- registry.register({ id: "h1", handler: async () => {} });
26
+ it('should throw on duplicate registration', () => {
27
+ registry.register({ id: 'h1', handler: async () => {} });
32
28
 
33
- expect(() =>
34
- registry.register({ id: "h1", handler: async () => {} }),
35
- ).toThrow("Step handler 'h1' is already registered");
29
+ expect(() => registry.register({ id: 'h1', handler: async () => {} }))
30
+ .toThrow("Step handler 'h1' is already registered");
36
31
  });
37
32
 
38
- it("should return undefined for non-existent handler", () => {
39
- expect(registry.get("nonexistent")).toBeUndefined();
40
- expect(registry.has("nonexistent")).toBe(false);
33
+ it('should return undefined for non-existent handler', () => {
34
+ expect(registry.get('nonexistent')).toBeUndefined();
35
+ expect(registry.has('nonexistent')).toBe(false);
41
36
  });
42
37
 
43
- it("should list all handlers", () => {
44
- registry.register({ id: "h1", handler: async () => {} });
45
- registry.register({ id: "h2", handler: async () => {} });
38
+ it('should list all handlers', () => {
39
+ registry.register({ id: 'h1', handler: async () => {} });
40
+ registry.register({ id: 'h2', handler: async () => {} });
46
41
 
47
42
  const list = registry.list();
48
43
  expect(list).toHaveLength(2);
49
- expect(list.map((h) => h.id).sort()).toEqual(["h1", "h2"]);
44
+ expect(list.map(h => h.id).sort()).toEqual(['h1', 'h2']);
50
45
  });
51
46
 
52
- it("should list handlers by tag", () => {
53
- registry.register({
54
- id: "h1",
55
- handler: async () => {},
56
- tags: ["ai", "text"],
57
- });
58
- registry.register({ id: "h2", handler: async () => {}, tags: ["ai"] });
59
- registry.register({ id: "h3", handler: async () => {}, tags: ["io"] });
47
+ it('should list handlers by tag', () => {
48
+ registry.register({ id: 'h1', handler: async () => {}, tags: ['ai', 'text'] });
49
+ registry.register({ id: 'h2', handler: async () => {}, tags: ['ai'] });
50
+ registry.register({ id: 'h3', handler: async () => {}, tags: ['io'] });
60
51
 
61
- const aiHandlers = registry.listByTag("ai");
52
+ const aiHandlers = registry.listByTag('ai');
62
53
  expect(aiHandlers).toHaveLength(2);
63
- expect(aiHandlers.map((h) => h.id).sort()).toEqual(["h1", "h2"]);
54
+ expect(aiHandlers.map(h => h.id).sort()).toEqual(['h1', 'h2']);
64
55
 
65
- expect(registry.listByTag("nonexistent")).toEqual([]);
56
+ expect(registry.listByTag('nonexistent')).toEqual([]);
66
57
  });
67
58
 
68
- it("should resolve handler reference to handler function", () => {
69
- const fn = async () => "resolved";
70
- registry.register({ id: "h1", handler: fn });
59
+ it('should resolve handler reference to handler function', () => {
60
+ const fn = async () => 'resolved';
61
+ registry.register({ id: 'h1', handler: fn });
71
62
 
72
- expect(registry.resolve("h1")).toBe(fn);
73
- expect(registry.resolve("nonexistent")).toBeUndefined();
63
+ expect(registry.resolve('h1')).toBe(fn);
64
+ expect(registry.resolve('nonexistent')).toBeUndefined();
74
65
  });
75
66
 
76
- it("should clear all handlers and tag indexes", () => {
77
- registry.register({ id: "h1", handler: async () => {}, tags: ["t1"] });
67
+ it('should clear all handlers and tag indexes', () => {
68
+ registry.register({ id: 'h1', handler: async () => {}, tags: ['t1'] });
78
69
  registry.clear();
79
70
 
80
71
  expect(registry.list()).toHaveLength(0);
81
- expect(registry.listByTag("t1")).toEqual([]);
72
+ expect(registry.listByTag('t1')).toEqual([]);
82
73
  });
83
74
  });
84
75
 
85
- describe("MemoryRecipeRegistry", () => {
76
+ describe('MemoryRecipeRegistry', () => {
86
77
  let registry: MemoryRecipeRegistry;
87
78
 
88
79
  const makeRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
89
- id: "recipe-1",
90
- name: "Recipe 1",
91
- workflowKind: "test.workflow",
92
- variant: "default",
93
- steps: [{ key: "s1", name: "S1", handlerRef: "h1" }],
80
+ id: 'recipe-1',
81
+ name: 'Recipe 1',
82
+ workflowKind: 'test.workflow',
83
+ variant: 'default',
84
+ steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
94
85
  ...overrides,
95
86
  });
96
87
 
@@ -98,346 +89,308 @@ describe("MemoryRecipeRegistry", () => {
98
89
  registry = new MemoryRecipeRegistry();
99
90
  });
100
91
 
101
- describe("register / get / has", () => {
102
- it("should register and retrieve a recipe", () => {
92
+ describe('register / get / has', () => {
93
+ it('should register and retrieve a recipe', () => {
103
94
  const recipe = makeRecipe();
104
95
  registry.register(recipe);
105
96
 
106
- expect(registry.get("recipe-1")).toEqual(recipe);
107
- expect(registry.has("recipe-1")).toBe(true);
97
+ expect(registry.get('recipe-1')).toEqual(recipe);
98
+ expect(registry.has('recipe-1')).toBe(true);
108
99
  });
109
100
 
110
- it("should throw on duplicate recipe id", () => {
101
+ it('should throw on duplicate recipe id', () => {
111
102
  registry.register(makeRecipe());
112
103
 
113
- expect(() => registry.register(makeRecipe())).toThrow(
114
- "Recipe 'recipe-1' is already registered",
115
- );
104
+ expect(() => registry.register(makeRecipe()))
105
+ .toThrow("Recipe 'recipe-1' is already registered");
116
106
  });
117
107
 
118
- it("should throw on duplicate kind:variant", () => {
119
- registry.register(makeRecipe({ id: "r1" }));
108
+ it('should throw on duplicate kind:variant', () => {
109
+ registry.register(makeRecipe({ id: 'r1' }));
120
110
 
121
- expect(() => registry.register(makeRecipe({ id: "r2" }))).toThrow(
122
- "Recipe variant 'default' for 'test.workflow' is already registered",
123
- );
111
+ expect(() => registry.register(makeRecipe({ id: 'r2' })))
112
+ .toThrow("Recipe variant 'default' for 'test.workflow' is already registered");
124
113
  });
125
114
 
126
- it("should return undefined for non-existent recipe", () => {
127
- expect(registry.get("nonexistent")).toBeUndefined();
128
- expect(registry.has("nonexistent")).toBe(false);
115
+ it('should return undefined for non-existent recipe', () => {
116
+ expect(registry.get('nonexistent')).toBeUndefined();
117
+ expect(registry.has('nonexistent')).toBe(false);
129
118
  });
130
119
  });
131
120
 
132
- describe("registerAll", () => {
133
- it("should register multiple recipes", () => {
121
+ describe('registerAll', () => {
122
+ it('should register multiple recipes', () => {
134
123
  registry.registerAll([
135
- makeRecipe({ id: "r1", variant: "fast" }),
136
- makeRecipe({ id: "r2", variant: "slow" }),
124
+ makeRecipe({ id: 'r1', variant: 'fast' }),
125
+ makeRecipe({ id: 'r2', variant: 'slow' }),
137
126
  ]);
138
127
 
139
128
  expect(registry.list()).toHaveLength(2);
140
129
  });
141
130
  });
142
131
 
143
- describe("getByKind", () => {
144
- it("should return recipes matching a workflow kind", () => {
145
- registry.register(makeRecipe({ id: "r1", variant: "v1" }));
146
- registry.register(makeRecipe({ id: "r2", variant: "v2" }));
147
- registry.register(
148
- makeRecipe({ id: "r3", workflowKind: "other", variant: "default" }),
149
- );
132
+ describe('getByKind', () => {
133
+ it('should return recipes matching a workflow kind', () => {
134
+ registry.register(makeRecipe({ id: 'r1', variant: 'v1' }));
135
+ registry.register(makeRecipe({ id: 'r2', variant: 'v2' }));
136
+ registry.register(makeRecipe({ id: 'r3', workflowKind: 'other', variant: 'default' }));
150
137
 
151
- const results = registry.getByKind("test.workflow");
138
+ const results = registry.getByKind('test.workflow');
152
139
  expect(results).toHaveLength(2);
153
140
  });
154
141
 
155
- it("should return empty array for unknown kind", () => {
156
- expect(registry.getByKind("unknown")).toEqual([]);
142
+ it('should return empty array for unknown kind', () => {
143
+ expect(registry.getByKind('unknown')).toEqual([]);
157
144
  });
158
145
  });
159
146
 
160
- describe("getVariant", () => {
161
- it("should return recipe by kind and variant", () => {
162
- registry.register(makeRecipe({ id: "r1", variant: "fast" }));
163
- registry.register(
164
- makeRecipe({ id: "r2", workflowKind: "other", variant: "default" }),
165
- );
147
+ describe('getVariant', () => {
148
+ it('should return recipe by kind and variant', () => {
149
+ registry.register(makeRecipe({ id: 'r1', variant: 'fast' }));
150
+ registry.register(makeRecipe({ id: 'r2', workflowKind: 'other', variant: 'default' }));
166
151
 
167
- expect(registry.getVariant("test.workflow", "fast")?.id).toBe("r1");
168
- expect(
169
- registry.getVariant("test.workflow", "nonexistent"),
170
- ).toBeUndefined();
152
+ expect(registry.getVariant('test.workflow', 'fast')?.id).toBe('r1');
153
+ expect(registry.getVariant('test.workflow', 'nonexistent')).toBeUndefined();
171
154
  });
172
155
  });
173
156
 
174
- describe("getDefault", () => {
157
+ describe('getDefault', () => {
175
158
  it('should return the "default" variant', () => {
176
- registry.register(makeRecipe({ id: "r1", variant: "default" }));
177
- registry.register(makeRecipe({ id: "r2", variant: "fast" }));
159
+ registry.register(makeRecipe({ id: 'r1', variant: 'default' }));
160
+ registry.register(makeRecipe({ id: 'r2', variant: 'fast' }));
178
161
 
179
- expect(registry.getDefault("test.workflow")?.id).toBe("r1");
162
+ expect(registry.getDefault('test.workflow')?.id).toBe('r1');
180
163
  });
181
164
 
182
- it("should fall back to lowest priority recipe", () => {
183
- registry.register(
184
- makeRecipe({ id: "r1", variant: "fast", priority: 10 }),
185
- );
186
- registry.register(makeRecipe({ id: "r2", variant: "slow", priority: 1 }));
165
+ it('should fall back to lowest priority recipe', () => {
166
+ registry.register(makeRecipe({ id: 'r1', variant: 'fast', priority: 10 }));
167
+ registry.register(makeRecipe({ id: 'r2', variant: 'slow', priority: 1 }));
187
168
 
188
- expect(registry.getDefault("test.workflow")?.id).toBe("r2");
169
+ expect(registry.getDefault('test.workflow')?.id).toBe('r2');
189
170
  });
190
171
 
191
- it("should return undefined for unknown kind", () => {
192
- expect(registry.getDefault("unknown")).toBeUndefined();
172
+ it('should return undefined for unknown kind', () => {
173
+ expect(registry.getDefault('unknown')).toBeUndefined();
193
174
  });
194
175
 
195
- it("should handle recipes without explicit priority", () => {
196
- registry.register(makeRecipe({ id: "r1", variant: "v1" }));
176
+ it('should handle recipes without explicit priority', () => {
177
+ registry.register(makeRecipe({ id: 'r1', variant: 'v1' }));
197
178
 
198
- expect(registry.getDefault("test.workflow")?.id).toBe("r1");
179
+ expect(registry.getDefault('test.workflow')?.id).toBe('r1');
199
180
  });
200
181
  });
201
182
 
202
- describe("listVariants", () => {
203
- it("should list all variants for a kind", () => {
204
- registry.register(makeRecipe({ id: "r1", variant: "fast" }));
205
- registry.register(makeRecipe({ id: "r2", variant: "slow" }));
183
+ describe('listVariants', () => {
184
+ it('should list all variants for a kind', () => {
185
+ registry.register(makeRecipe({ id: 'r1', variant: 'fast' }));
186
+ registry.register(makeRecipe({ id: 'r2', variant: 'slow' }));
206
187
 
207
- expect(registry.listVariants("test.workflow").sort()).toEqual([
208
- "fast",
209
- "slow",
210
- ]);
188
+ expect(registry.listVariants('test.workflow').sort()).toEqual(['fast', 'slow']);
211
189
  });
212
190
  });
213
191
 
214
- describe("query", () => {
192
+ describe('query', () => {
215
193
  beforeEach(() => {
216
- registry.register(
217
- makeRecipe({ id: "r1", variant: "fast", tags: ["ai"] }),
218
- );
219
- registry.register(
220
- makeRecipe({
221
- id: "r2",
222
- variant: "slow",
223
- tags: ["legacy"],
224
- workflowKind: "other",
225
- }),
226
- );
227
- registry.register(
228
- makeRecipe({
229
- id: "r3",
230
- variant: "conditional",
231
- workflowKind: "other",
232
- conditions: [{ field: "size", operator: "gt", value: 100 }],
233
- }),
234
- );
235
- });
236
-
237
- it("should filter by workflowKind", () => {
238
- const results = registry.query({ workflowKind: "other" });
194
+ registry.register(makeRecipe({ id: 'r1', variant: 'fast', tags: ['ai'] }));
195
+ registry.register(makeRecipe({ id: 'r2', variant: 'slow', tags: ['legacy'], workflowKind: 'other' }));
196
+ registry.register(makeRecipe({
197
+ id: 'r3',
198
+ variant: 'conditional',
199
+ workflowKind: 'other',
200
+ conditions: [{ field: 'size', operator: 'gt', value: 100 }],
201
+ }));
202
+ });
203
+
204
+ it('should filter by workflowKind', () => {
205
+ const results = registry.query({ workflowKind: 'other' });
239
206
  expect(results).toHaveLength(2);
240
207
  });
241
208
 
242
- it("should filter by variant", () => {
243
- const results = registry.query({ variant: "fast" });
209
+ it('should filter by variant', () => {
210
+ const results = registry.query({ variant: 'fast' });
244
211
  expect(results).toHaveLength(1);
245
- expect(results[0].id).toBe("r1");
212
+ expect(results[0].id).toBe('r1');
246
213
  });
247
214
 
248
- it("should filter by tags", () => {
249
- const results = registry.query({ tags: ["ai"] });
215
+ it('should filter by tags', () => {
216
+ const results = registry.query({ tags: ['ai'] });
250
217
  expect(results).toHaveLength(1);
251
- expect(results[0].id).toBe("r1");
218
+ expect(results[0].id).toBe('r1');
252
219
  });
253
220
 
254
- it("should filter by matchConditions", () => {
221
+ it('should filter by matchConditions', () => {
255
222
  const results = registry.query({ matchConditions: { size: 200 } });
256
223
  // r1 has no conditions (passes), r2 has no conditions (passes), r3 has size > 100 (passes)
257
224
  expect(results).toHaveLength(3);
258
225
  });
259
226
 
260
- it("should exclude recipes where conditions fail", () => {
227
+ it('should exclude recipes where conditions fail', () => {
261
228
  const results = registry.query({ matchConditions: { size: 50 } });
262
229
  // r3 has size > 100 which fails for 50
263
230
  expect(results).toHaveLength(2);
264
- expect(results.find((r) => r.id === "r3")).toBeUndefined();
231
+ expect(results.find(r => r.id === 'r3')).toBeUndefined();
265
232
  });
266
233
  });
267
234
 
268
- describe("condition evaluation", () => {
269
- const registerConditional = (
270
- field: string,
271
- operator: string,
272
- value?: unknown,
273
- ) => {
235
+ describe('condition evaluation', () => {
236
+ const registerConditional = (field: string, operator: string, value?: unknown) => {
274
237
  const id = `cond-${Math.random()}`;
275
- registry.register(
276
- makeRecipe({
277
- id,
278
- variant: id,
279
- conditions: [{ field, operator: operator as any, value }],
280
- }),
281
- );
238
+ registry.register(makeRecipe({
239
+ id,
240
+ variant: id,
241
+ conditions: [{ field, operator: operator as any, value }],
242
+ }));
282
243
  return id;
283
244
  };
284
245
 
285
- it("should evaluate eq/neq", () => {
286
- const eqId = registerConditional("x", "eq", 5);
287
- const neqId = registerConditional("x", "neq", 5);
246
+ it('should evaluate eq/neq', () => {
247
+ const eqId = registerConditional('x', 'eq', 5);
248
+ const neqId = registerConditional('x', 'neq', 5);
288
249
 
289
250
  let results = registry.query({ matchConditions: { x: 5 } });
290
- expect(results.find((r) => r.id === eqId)).toBeDefined();
251
+ expect(results.find(r => r.id === eqId)).toBeDefined();
291
252
 
292
253
  results = registry.query({ matchConditions: { x: 3 } });
293
- expect(results.find((r) => r.id === eqId)).toBeUndefined();
294
- expect(results.find((r) => r.id === neqId)).toBeDefined();
254
+ expect(results.find(r => r.id === eqId)).toBeUndefined();
255
+ expect(results.find(r => r.id === neqId)).toBeDefined();
295
256
  });
296
257
 
297
- it("should evaluate gt/gte/lt/lte", () => {
298
- const gtId = registerConditional("n", "gt", 10);
299
- const gteId = registerConditional("n", "gte", 10);
300
- const ltId = registerConditional("n", "lt", 10);
301
- const lteId = registerConditional("n", "lte", 10);
258
+ it('should evaluate gt/gte/lt/lte', () => {
259
+ const gtId = registerConditional('n', 'gt', 10);
260
+ const gteId = registerConditional('n', 'gte', 10);
261
+ const ltId = registerConditional('n', 'lt', 10);
262
+ const lteId = registerConditional('n', 'lte', 10);
302
263
 
303
264
  let results = registry.query({ matchConditions: { n: 15 } });
304
- expect(results.find((r) => r.id === gtId)).toBeDefined();
305
- expect(results.find((r) => r.id === gteId)).toBeDefined();
306
- expect(results.find((r) => r.id === ltId)).toBeUndefined();
265
+ expect(results.find(r => r.id === gtId)).toBeDefined();
266
+ expect(results.find(r => r.id === gteId)).toBeDefined();
267
+ expect(results.find(r => r.id === ltId)).toBeUndefined();
307
268
 
308
269
  results = registry.query({ matchConditions: { n: 10 } });
309
- expect(results.find((r) => r.id === gtId)).toBeUndefined();
310
- expect(results.find((r) => r.id === gteId)).toBeDefined();
311
- expect(results.find((r) => r.id === lteId)).toBeDefined();
270
+ expect(results.find(r => r.id === gtId)).toBeUndefined();
271
+ expect(results.find(r => r.id === gteId)).toBeDefined();
272
+ expect(results.find(r => r.id === lteId)).toBeDefined();
312
273
 
313
274
  results = registry.query({ matchConditions: { n: 5 } });
314
- expect(results.find((r) => r.id === ltId)).toBeDefined();
315
- expect(results.find((r) => r.id === lteId)).toBeDefined();
275
+ expect(results.find(r => r.id === ltId)).toBeDefined();
276
+ expect(results.find(r => r.id === lteId)).toBeDefined();
316
277
  });
317
278
 
318
- it("should return false for gt/lt with non-number values", () => {
319
- const gtId = registerConditional("x", "gt", 10);
279
+ it('should return false for gt/lt with non-number values', () => {
280
+ const gtId = registerConditional('x', 'gt', 10);
320
281
 
321
- const results = registry.query({
322
- matchConditions: { x: "not a number" },
323
- });
324
- expect(results.find((r) => r.id === gtId)).toBeUndefined();
282
+ const results = registry.query({ matchConditions: { x: 'not a number' } });
283
+ expect(results.find(r => r.id === gtId)).toBeUndefined();
325
284
  });
326
285
 
327
- it("should evaluate contains for strings", () => {
328
- const id = registerConditional("text", "contains", "hello");
286
+ it('should evaluate contains for strings', () => {
287
+ const id = registerConditional('text', 'contains', 'hello');
329
288
 
330
- let results = registry.query({
331
- matchConditions: { text: "say hello world" },
332
- });
333
- expect(results.find((r) => r.id === id)).toBeDefined();
289
+ let results = registry.query({ matchConditions: { text: 'say hello world' } });
290
+ expect(results.find(r => r.id === id)).toBeDefined();
334
291
 
335
- results = registry.query({ matchConditions: { text: "no match" } });
336
- expect(results.find((r) => r.id === id)).toBeUndefined();
292
+ results = registry.query({ matchConditions: { text: 'no match' } });
293
+ expect(results.find(r => r.id === id)).toBeUndefined();
337
294
  });
338
295
 
339
- it("should evaluate contains for arrays", () => {
340
- const id = registerConditional("tags", "contains", "important");
296
+ it('should evaluate contains for arrays', () => {
297
+ const id = registerConditional('tags', 'contains', 'important');
341
298
 
342
- const results = registry.query({
343
- matchConditions: { tags: ["important", "other"] },
344
- });
345
- expect(results.find((r) => r.id === id)).toBeDefined();
299
+ const results = registry.query({ matchConditions: { tags: ['important', 'other'] } });
300
+ expect(results.find(r => r.id === id)).toBeDefined();
346
301
  });
347
302
 
348
- it("should return false for contains with non-string/non-array", () => {
349
- const id = registerConditional("x", "contains", "test");
303
+ it('should return false for contains with non-string/non-array', () => {
304
+ const id = registerConditional('x', 'contains', 'test');
350
305
 
351
306
  const results = registry.query({ matchConditions: { x: 42 } });
352
- expect(results.find((r) => r.id === id)).toBeUndefined();
307
+ expect(results.find(r => r.id === id)).toBeUndefined();
353
308
  });
354
309
 
355
- it("should evaluate matches (regex)", () => {
356
- const id = registerConditional("name", "matches", "^test-\\d+$");
310
+ it('should evaluate matches (regex)', () => {
311
+ const id = registerConditional('name', 'matches', '^test-\\d+$');
357
312
 
358
- let results = registry.query({ matchConditions: { name: "test-123" } });
359
- expect(results.find((r) => r.id === id)).toBeDefined();
313
+ let results = registry.query({ matchConditions: { name: 'test-123' } });
314
+ expect(results.find(r => r.id === id)).toBeDefined();
360
315
 
361
- results = registry.query({ matchConditions: { name: "not-matching" } });
362
- expect(results.find((r) => r.id === id)).toBeUndefined();
316
+ results = registry.query({ matchConditions: { name: 'not-matching' } });
317
+ expect(results.find(r => r.id === id)).toBeUndefined();
363
318
  });
364
319
 
365
- it("should return false for matches with invalid regex", () => {
366
- const id = registerConditional("x", "matches", "[invalid");
320
+ it('should return false for matches with invalid regex', () => {
321
+ const id = registerConditional('x', 'matches', '[invalid');
367
322
 
368
- const results = registry.query({ matchConditions: { x: "anything" } });
369
- expect(results.find((r) => r.id === id)).toBeUndefined();
323
+ const results = registry.query({ matchConditions: { x: 'anything' } });
324
+ expect(results.find(r => r.id === id)).toBeUndefined();
370
325
  });
371
326
 
372
- it("should return false for matches with non-string field", () => {
373
- const id = registerConditional("x", "matches", ".*");
327
+ it('should return false for matches with non-string field', () => {
328
+ const id = registerConditional('x', 'matches', '.*');
374
329
 
375
330
  const results = registry.query({ matchConditions: { x: 42 } });
376
- expect(results.find((r) => r.id === id)).toBeUndefined();
331
+ expect(results.find(r => r.id === id)).toBeUndefined();
377
332
  });
378
333
 
379
- it("should evaluate exists/notExists", () => {
380
- const existsId = registerConditional("field", "exists");
381
- const notExistsId = registerConditional("field", "notExists");
334
+ it('should evaluate exists/notExists', () => {
335
+ const existsId = registerConditional('field', 'exists');
336
+ const notExistsId = registerConditional('field', 'notExists');
382
337
 
383
- let results = registry.query({ matchConditions: { field: "present" } });
384
- expect(results.find((r) => r.id === existsId)).toBeDefined();
385
- expect(results.find((r) => r.id === notExistsId)).toBeUndefined();
338
+ let results = registry.query({ matchConditions: { field: 'present' } });
339
+ expect(results.find(r => r.id === existsId)).toBeDefined();
340
+ expect(results.find(r => r.id === notExistsId)).toBeUndefined();
386
341
 
387
- results = registry.query({ matchConditions: { field: "" } });
388
- expect(results.find((r) => r.id === existsId)).toBeUndefined();
389
- expect(results.find((r) => r.id === notExistsId)).toBeDefined();
342
+ results = registry.query({ matchConditions: { field: '' } });
343
+ expect(results.find(r => r.id === existsId)).toBeUndefined();
344
+ expect(results.find(r => r.id === notExistsId)).toBeDefined();
390
345
 
391
346
  results = registry.query({ matchConditions: { field: null } });
392
- expect(results.find((r) => r.id === existsId)).toBeUndefined();
393
- expect(results.find((r) => r.id === notExistsId)).toBeDefined();
347
+ expect(results.find(r => r.id === existsId)).toBeUndefined();
348
+ expect(results.find(r => r.id === notExistsId)).toBeDefined();
394
349
 
395
350
  results = registry.query({ matchConditions: {} });
396
- expect(results.find((r) => r.id === existsId)).toBeUndefined();
397
- expect(results.find((r) => r.id === notExistsId)).toBeDefined();
351
+ expect(results.find(r => r.id === existsId)).toBeUndefined();
352
+ expect(results.find(r => r.id === notExistsId)).toBeDefined();
398
353
  });
399
354
 
400
- it("should handle nested field paths", () => {
401
- const id = registerConditional("a.b.c", "eq", 42);
355
+ it('should handle nested field paths', () => {
356
+ const id = registerConditional('a.b.c', 'eq', 42);
402
357
 
403
- let results = registry.query({
404
- matchConditions: { a: { b: { c: 42 } } },
405
- });
406
- expect(results.find((r) => r.id === id)).toBeDefined();
358
+ let results = registry.query({ matchConditions: { a: { b: { c: 42 } } } });
359
+ expect(results.find(r => r.id === id)).toBeDefined();
407
360
 
408
361
  results = registry.query({ matchConditions: { a: { b: { c: 99 } } } });
409
- expect(results.find((r) => r.id === id)).toBeUndefined();
362
+ expect(results.find(r => r.id === id)).toBeUndefined();
410
363
  });
411
364
 
412
- it("should return undefined for missing nested path segments", () => {
413
- const id = registerConditional("a.b.c", "exists");
365
+ it('should return undefined for missing nested path segments', () => {
366
+ const id = registerConditional('a.b.c', 'exists');
414
367
 
415
- const results = registry.query({ matchConditions: { a: "not-object" } });
416
- expect(results.find((r) => r.id === id)).toBeUndefined();
368
+ const results = registry.query({ matchConditions: { a: 'not-object' } });
369
+ expect(results.find(r => r.id === id)).toBeUndefined();
417
370
  });
418
371
 
419
- it("should return false for unknown operator", () => {
420
- const id = registerConditional("x", "unknownOp" as any, 1);
372
+ it('should return false for unknown operator', () => {
373
+ const id = registerConditional('x', 'unknownOp' as any, 1);
421
374
 
422
375
  const results = registry.query({ matchConditions: { x: 1 } });
423
- expect(results.find((r) => r.id === id)).toBeUndefined();
376
+ expect(results.find(r => r.id === id)).toBeUndefined();
424
377
  });
425
378
  });
426
379
 
427
- describe("clear", () => {
428
- it("should clear all registries", () => {
429
- registry.register(makeRecipe({ id: "r1", variant: "v1", tags: ["t1"] }));
380
+ describe('clear', () => {
381
+ it('should clear all registries', () => {
382
+ registry.register(makeRecipe({ id: 'r1', variant: 'v1', tags: ['t1'] }));
430
383
  registry.clear();
431
384
 
432
385
  expect(registry.list()).toHaveLength(0);
433
- expect(registry.getByKind("test.workflow")).toEqual([]);
434
- expect(registry.listVariants("test.workflow")).toEqual([]);
386
+ expect(registry.getByKind('test.workflow')).toEqual([]);
387
+ expect(registry.listVariants('test.workflow')).toEqual([]);
435
388
  });
436
389
  });
437
390
  });
438
391
 
439
- describe("createRegistry", () => {
440
- it("should return a combined registry", () => {
392
+ describe('createRegistry', () => {
393
+ it('should return a combined registry', () => {
441
394
  const reg = createRegistry();
442
395
  expect(reg.recipes).toBeInstanceOf(MemoryRecipeRegistry);
443
396
  expect(reg.handlers).toBeInstanceOf(MemoryStepHandlerRegistry);