@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.
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +11 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
- package/dist/chunk-4VOKMGP6.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
- package/dist/chunk-GYPNJBDP.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
- package/dist/chunk-KHTUWLVZ.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
- package/dist/chunk-ML35PIHX.js.map +1 -0
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
- package/dist/index.d.ts +14 -12
- package/dist/index.js +40 -82
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +13 -13
- package/dist/storage/index.js +2 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +2 -3
- package/package.json +7 -6
- package/src/core/engine.test.ts +466 -522
- package/src/core/engine.ts +42 -67
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator.ts +64 -83
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +197 -235
- package/src/events/webhook.ts +44 -71
- package/src/index.ts +19 -19
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +66 -91
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +39 -43
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +141 -150
- package/src/scheduler/cron.ts +32 -67
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +71 -99
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +12 -19
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres.ts +197 -393
- package/src/storage/sqlite.test.ts +154 -281
- package/src/storage/sqlite.ts +50 -85
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +4 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/postgres-deps.ts +4 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { RuleBasedPlanner } from "./planner";
|
|
8
|
-
import type { Recipe, RecipeCondition, PlanningContext } from "./types";
|
|
9
|
-
|
|
10
|
-
describe("MemoryRecipeRegistry", () => {
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryRecipeRegistry, MemoryStepHandlerRegistry, createRegistry } from './registry';
|
|
3
|
+
import { RuleBasedPlanner } from './planner';
|
|
4
|
+
import type { Recipe, RecipeCondition, PlanningContext } from './types';
|
|
5
|
+
|
|
6
|
+
describe('MemoryRecipeRegistry', () => {
|
|
11
7
|
let registry: MemoryRecipeRegistry;
|
|
12
8
|
|
|
13
9
|
beforeEach(() => {
|
|
@@ -15,225 +11,195 @@ describe("MemoryRecipeRegistry", () => {
|
|
|
15
11
|
});
|
|
16
12
|
|
|
17
13
|
const createTestRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
|
|
18
|
-
id:
|
|
19
|
-
name:
|
|
20
|
-
workflowKind:
|
|
21
|
-
variant:
|
|
22
|
-
steps: [
|
|
14
|
+
id: 'test.recipe',
|
|
15
|
+
name: 'Test Recipe',
|
|
16
|
+
workflowKind: 'test.workflow',
|
|
17
|
+
variant: 'default',
|
|
18
|
+
steps: [
|
|
19
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'handlers.step1' },
|
|
20
|
+
],
|
|
23
21
|
...overrides,
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
describe(
|
|
27
|
-
it(
|
|
24
|
+
describe('register', () => {
|
|
25
|
+
it('should register a recipe', () => {
|
|
28
26
|
const recipe = createTestRecipe();
|
|
29
27
|
registry.register(recipe);
|
|
30
28
|
|
|
31
|
-
expect(registry.has(
|
|
32
|
-
expect(registry.get(
|
|
29
|
+
expect(registry.has('test.recipe')).toBe(true);
|
|
30
|
+
expect(registry.get('test.recipe')).toEqual(recipe);
|
|
33
31
|
});
|
|
34
32
|
|
|
35
|
-
it(
|
|
33
|
+
it('should throw if recipe already registered', () => {
|
|
36
34
|
const recipe = createTestRecipe();
|
|
37
35
|
registry.register(recipe);
|
|
38
36
|
|
|
39
37
|
expect(() => registry.register(recipe)).toThrow(
|
|
40
|
-
"Recipe 'test.recipe' is already registered"
|
|
38
|
+
"Recipe 'test.recipe' is already registered"
|
|
41
39
|
);
|
|
42
40
|
});
|
|
43
41
|
|
|
44
|
-
it(
|
|
45
|
-
const recipe1 = createTestRecipe({ id:
|
|
46
|
-
const recipe2 = createTestRecipe({ id:
|
|
42
|
+
it('should throw if variant already exists for workflow kind', () => {
|
|
43
|
+
const recipe1 = createTestRecipe({ id: 'recipe1' });
|
|
44
|
+
const recipe2 = createTestRecipe({ id: 'recipe2' });
|
|
47
45
|
|
|
48
46
|
registry.register(recipe1);
|
|
49
47
|
|
|
50
48
|
expect(() => registry.register(recipe2)).toThrow(
|
|
51
|
-
"Recipe variant 'default' for 'test.workflow' is already registered"
|
|
49
|
+
"Recipe variant 'default' for 'test.workflow' is already registered"
|
|
52
50
|
);
|
|
53
51
|
});
|
|
54
52
|
});
|
|
55
53
|
|
|
56
|
-
describe(
|
|
57
|
-
it(
|
|
58
|
-
registry.register(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
workflowKind: "other.workflow",
|
|
66
|
-
variant: "default",
|
|
67
|
-
}),
|
|
68
|
-
);
|
|
54
|
+
describe('getByKind', () => {
|
|
55
|
+
it('should return all recipes for a workflow kind', () => {
|
|
56
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
|
|
57
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
|
|
58
|
+
registry.register(createTestRecipe({
|
|
59
|
+
id: 'recipe3',
|
|
60
|
+
workflowKind: 'other.workflow',
|
|
61
|
+
variant: 'default',
|
|
62
|
+
}));
|
|
69
63
|
|
|
70
|
-
const recipes = registry.getByKind(
|
|
64
|
+
const recipes = registry.getByKind('test.workflow');
|
|
71
65
|
expect(recipes).toHaveLength(2);
|
|
72
|
-
expect(recipes.map(
|
|
73
|
-
expect(recipes.map(
|
|
66
|
+
expect(recipes.map(r => r.id)).toContain('recipe1');
|
|
67
|
+
expect(recipes.map(r => r.id)).toContain('recipe2');
|
|
74
68
|
});
|
|
75
69
|
|
|
76
|
-
it(
|
|
77
|
-
expect(registry.getByKind(
|
|
70
|
+
it('should return empty array if no recipes found', () => {
|
|
71
|
+
expect(registry.getByKind('nonexistent')).toEqual([]);
|
|
78
72
|
});
|
|
79
73
|
});
|
|
80
74
|
|
|
81
|
-
describe(
|
|
82
|
-
it(
|
|
83
|
-
registry.register(
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
|
|
75
|
+
describe('getVariant', () => {
|
|
76
|
+
it('should return specific variant', () => {
|
|
77
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
|
|
78
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
|
|
87
79
|
|
|
88
|
-
const recipe = registry.getVariant(
|
|
89
|
-
expect(recipe?.id).toBe(
|
|
80
|
+
const recipe = registry.getVariant('test.workflow', 'fast');
|
|
81
|
+
expect(recipe?.id).toBe('recipe2');
|
|
90
82
|
});
|
|
91
83
|
|
|
92
|
-
it(
|
|
84
|
+
it('should return undefined if variant not found', () => {
|
|
93
85
|
registry.register(createTestRecipe());
|
|
94
86
|
|
|
95
|
-
expect(
|
|
96
|
-
registry.getVariant("test.workflow", "nonexistent"),
|
|
97
|
-
).toBeUndefined();
|
|
87
|
+
expect(registry.getVariant('test.workflow', 'nonexistent')).toBeUndefined();
|
|
98
88
|
});
|
|
99
89
|
});
|
|
100
90
|
|
|
101
|
-
describe(
|
|
102
|
-
it(
|
|
103
|
-
registry.register(
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
|
|
91
|
+
describe('getDefault', () => {
|
|
92
|
+
it('should return default variant if exists', () => {
|
|
93
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
|
|
94
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
|
|
107
95
|
|
|
108
|
-
const recipe = registry.getDefault(
|
|
109
|
-
expect(recipe?.variant).toBe(
|
|
96
|
+
const recipe = registry.getDefault('test.workflow');
|
|
97
|
+
expect(recipe?.variant).toBe('default');
|
|
110
98
|
});
|
|
111
99
|
|
|
112
|
-
it(
|
|
113
|
-
registry.register(
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
registry.register(
|
|
117
|
-
createTestRecipe({ id: "recipe2", variant: "slow", priority: 5 }),
|
|
118
|
-
);
|
|
100
|
+
it('should return lowest priority recipe if no default variant', () => {
|
|
101
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'fast', priority: 10 }));
|
|
102
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'slow', priority: 5 }));
|
|
119
103
|
|
|
120
|
-
const recipe = registry.getDefault(
|
|
121
|
-
expect(recipe?.id).toBe(
|
|
104
|
+
const recipe = registry.getDefault('test.workflow');
|
|
105
|
+
expect(recipe?.id).toBe('recipe2');
|
|
122
106
|
});
|
|
123
107
|
});
|
|
124
108
|
|
|
125
|
-
describe(
|
|
126
|
-
it(
|
|
127
|
-
registry.register(
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
registry.register(createTestRecipe({ id: "recipe2", variant: "fast" }));
|
|
131
|
-
registry.register(
|
|
132
|
-
createTestRecipe({ id: "recipe3", variant: "thorough" }),
|
|
133
|
-
);
|
|
109
|
+
describe('listVariants', () => {
|
|
110
|
+
it('should list all variants for a workflow kind', () => {
|
|
111
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'default' }));
|
|
112
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'fast' }));
|
|
113
|
+
registry.register(createTestRecipe({ id: 'recipe3', variant: 'thorough' }));
|
|
134
114
|
|
|
135
|
-
const variants = registry.listVariants(
|
|
115
|
+
const variants = registry.listVariants('test.workflow');
|
|
136
116
|
expect(variants).toHaveLength(3);
|
|
137
|
-
expect(variants).toContain(
|
|
138
|
-
expect(variants).toContain(
|
|
139
|
-
expect(variants).toContain(
|
|
117
|
+
expect(variants).toContain('default');
|
|
118
|
+
expect(variants).toContain('fast');
|
|
119
|
+
expect(variants).toContain('thorough');
|
|
140
120
|
});
|
|
141
121
|
});
|
|
142
122
|
|
|
143
|
-
describe(
|
|
144
|
-
it(
|
|
145
|
-
registry.register(createTestRecipe({ id:
|
|
146
|
-
registry.register(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}),
|
|
152
|
-
);
|
|
123
|
+
describe('query', () => {
|
|
124
|
+
it('should filter by workflow kind', () => {
|
|
125
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'v1' }));
|
|
126
|
+
registry.register(createTestRecipe({
|
|
127
|
+
id: 'recipe2',
|
|
128
|
+
workflowKind: 'other',
|
|
129
|
+
variant: 'v1',
|
|
130
|
+
}));
|
|
153
131
|
|
|
154
|
-
const results = registry.query({ workflowKind:
|
|
132
|
+
const results = registry.query({ workflowKind: 'test.workflow' });
|
|
155
133
|
expect(results).toHaveLength(1);
|
|
156
|
-
expect(results[0].id).toBe(
|
|
134
|
+
expect(results[0].id).toBe('recipe1');
|
|
157
135
|
});
|
|
158
136
|
|
|
159
|
-
it(
|
|
160
|
-
registry.register(
|
|
161
|
-
|
|
162
|
-
id: "recipe1",
|
|
163
|
-
variant: "v1",
|
|
164
|
-
tags: ["fast", "simple"],
|
|
165
|
-
}),
|
|
166
|
-
);
|
|
167
|
-
registry.register(
|
|
168
|
-
createTestRecipe({
|
|
169
|
-
id: "recipe2",
|
|
170
|
-
variant: "v2",
|
|
171
|
-
tags: ["slow", "complex"],
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
137
|
+
it('should filter by tags', () => {
|
|
138
|
+
registry.register(createTestRecipe({ id: 'recipe1', variant: 'v1', tags: ['fast', 'simple'] }));
|
|
139
|
+
registry.register(createTestRecipe({ id: 'recipe2', variant: 'v2', tags: ['slow', 'complex'] }));
|
|
174
140
|
|
|
175
|
-
const results = registry.query({ tags: [
|
|
141
|
+
const results = registry.query({ tags: ['fast'] });
|
|
176
142
|
expect(results).toHaveLength(1);
|
|
177
|
-
expect(results[0].id).toBe(
|
|
143
|
+
expect(results[0].id).toBe('recipe1');
|
|
178
144
|
});
|
|
179
145
|
});
|
|
180
146
|
});
|
|
181
147
|
|
|
182
|
-
describe(
|
|
148
|
+
describe('MemoryStepHandlerRegistry', () => {
|
|
183
149
|
let registry: MemoryStepHandlerRegistry;
|
|
184
150
|
|
|
185
151
|
beforeEach(() => {
|
|
186
152
|
registry = new MemoryStepHandlerRegistry();
|
|
187
153
|
});
|
|
188
154
|
|
|
189
|
-
it(
|
|
155
|
+
it('should register and retrieve handlers', () => {
|
|
190
156
|
const handler = {
|
|
191
|
-
id:
|
|
192
|
-
description:
|
|
193
|
-
handler: async () => ({ result:
|
|
157
|
+
id: 'test.handler',
|
|
158
|
+
description: 'Test handler',
|
|
159
|
+
handler: async () => ({ result: 'test' }),
|
|
194
160
|
};
|
|
195
161
|
|
|
196
162
|
registry.register(handler);
|
|
197
163
|
|
|
198
|
-
expect(registry.has(
|
|
199
|
-
expect(registry.get(
|
|
164
|
+
expect(registry.has('test.handler')).toBe(true);
|
|
165
|
+
expect(registry.get('test.handler')).toEqual(handler);
|
|
200
166
|
});
|
|
201
167
|
|
|
202
|
-
it(
|
|
168
|
+
it('should throw if handler already registered', () => {
|
|
203
169
|
const handler = {
|
|
204
|
-
id:
|
|
170
|
+
id: 'test.handler',
|
|
205
171
|
handler: async () => ({}),
|
|
206
172
|
};
|
|
207
173
|
|
|
208
174
|
registry.register(handler);
|
|
209
175
|
|
|
210
176
|
expect(() => registry.register(handler)).toThrow(
|
|
211
|
-
"Step handler 'test.handler' is already registered"
|
|
177
|
+
"Step handler 'test.handler' is already registered"
|
|
212
178
|
);
|
|
213
179
|
});
|
|
214
180
|
|
|
215
|
-
it(
|
|
181
|
+
it('should list handlers by tag', () => {
|
|
216
182
|
registry.register({
|
|
217
|
-
id:
|
|
183
|
+
id: 'handler1',
|
|
218
184
|
handler: async () => ({}),
|
|
219
|
-
tags: [
|
|
185
|
+
tags: ['llm', 'text'],
|
|
220
186
|
});
|
|
221
187
|
registry.register({
|
|
222
|
-
id:
|
|
188
|
+
id: 'handler2',
|
|
223
189
|
handler: async () => ({}),
|
|
224
|
-
tags: [
|
|
190
|
+
tags: ['data', 'text'],
|
|
225
191
|
});
|
|
226
192
|
|
|
227
|
-
const llmHandlers = registry.listByTag(
|
|
193
|
+
const llmHandlers = registry.listByTag('llm');
|
|
228
194
|
expect(llmHandlers).toHaveLength(1);
|
|
229
|
-
expect(llmHandlers[0].id).toBe(
|
|
195
|
+
expect(llmHandlers[0].id).toBe('handler1');
|
|
230
196
|
|
|
231
|
-
const textHandlers = registry.listByTag(
|
|
197
|
+
const textHandlers = registry.listByTag('text');
|
|
232
198
|
expect(textHandlers).toHaveLength(2);
|
|
233
199
|
});
|
|
234
200
|
});
|
|
235
201
|
|
|
236
|
-
describe(
|
|
202
|
+
describe('RuleBasedPlanner', () => {
|
|
237
203
|
let registry: MemoryRecipeRegistry;
|
|
238
204
|
let planner: RuleBasedPlanner;
|
|
239
205
|
|
|
@@ -243,156 +209,132 @@ describe("RuleBasedPlanner", () => {
|
|
|
243
209
|
});
|
|
244
210
|
|
|
245
211
|
const createTestRecipe = (overrides: Partial<Recipe> = {}): Recipe => ({
|
|
246
|
-
id:
|
|
247
|
-
name:
|
|
248
|
-
workflowKind:
|
|
249
|
-
variant:
|
|
212
|
+
id: 'test.recipe',
|
|
213
|
+
name: 'Test Recipe',
|
|
214
|
+
workflowKind: 'test.workflow',
|
|
215
|
+
variant: 'default',
|
|
250
216
|
steps: [
|
|
251
|
-
{ key:
|
|
252
|
-
{ key:
|
|
217
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'handlers.step1' },
|
|
218
|
+
{ key: 'step2', name: 'Step 2', handlerRef: 'handlers.step2' },
|
|
253
219
|
],
|
|
254
220
|
...overrides,
|
|
255
221
|
});
|
|
256
222
|
|
|
257
|
-
describe(
|
|
258
|
-
it(
|
|
223
|
+
describe('selectRecipe', () => {
|
|
224
|
+
it('should select recipe with matching conditions', async () => {
|
|
259
225
|
const conditions: RecipeCondition[] = [
|
|
260
|
-
{ field:
|
|
226
|
+
{ field: 'priority', operator: 'eq', value: 'speed' },
|
|
261
227
|
];
|
|
262
228
|
|
|
263
|
-
registry.register(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
conditions,
|
|
271
|
-
priority: 10,
|
|
272
|
-
}),
|
|
273
|
-
);
|
|
229
|
+
registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
|
|
230
|
+
registry.register(createTestRecipe({
|
|
231
|
+
id: 'fast',
|
|
232
|
+
variant: 'fast',
|
|
233
|
+
conditions,
|
|
234
|
+
priority: 10,
|
|
235
|
+
}));
|
|
274
236
|
|
|
275
|
-
const result = await planner.selectRecipe(
|
|
276
|
-
priority: "speed",
|
|
277
|
-
});
|
|
237
|
+
const result = await planner.selectRecipe('test.workflow', { priority: 'speed' });
|
|
278
238
|
|
|
279
|
-
expect(result.recipe.id).toBe(
|
|
239
|
+
expect(result.recipe.id).toBe('fast');
|
|
280
240
|
expect(result.score).toBeGreaterThan(0);
|
|
281
241
|
});
|
|
282
242
|
|
|
283
|
-
it(
|
|
284
|
-
registry.register(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
variant: "fast",
|
|
291
|
-
conditions: [{ field: "priority", operator: "eq", value: "speed" }],
|
|
292
|
-
}),
|
|
293
|
-
);
|
|
243
|
+
it('should fall back to default if no conditions match', async () => {
|
|
244
|
+
registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
|
|
245
|
+
registry.register(createTestRecipe({
|
|
246
|
+
id: 'fast',
|
|
247
|
+
variant: 'fast',
|
|
248
|
+
conditions: [{ field: 'priority', operator: 'eq', value: 'speed' }],
|
|
249
|
+
}));
|
|
294
250
|
|
|
295
|
-
const result = await planner.selectRecipe(
|
|
296
|
-
priority: "quality",
|
|
297
|
-
});
|
|
251
|
+
const result = await planner.selectRecipe('test.workflow', { priority: 'quality' });
|
|
298
252
|
|
|
299
|
-
expect(result.recipe.id).toBe(
|
|
253
|
+
expect(result.recipe.id).toBe('default');
|
|
300
254
|
});
|
|
301
255
|
|
|
302
|
-
it(
|
|
303
|
-
registry.register(
|
|
304
|
-
|
|
305
|
-
);
|
|
306
|
-
registry.register(
|
|
307
|
-
createTestRecipe({ id: "specific", variant: "specific" }),
|
|
308
|
-
);
|
|
256
|
+
it('should use forced recipe from hints', async () => {
|
|
257
|
+
registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
|
|
258
|
+
registry.register(createTestRecipe({ id: 'specific', variant: 'specific' }));
|
|
309
259
|
|
|
310
260
|
const context: PlanningContext = {
|
|
311
|
-
hints: { forceRecipeId:
|
|
261
|
+
hints: { forceRecipeId: 'specific' },
|
|
312
262
|
};
|
|
313
263
|
|
|
314
|
-
const result = await planner.selectRecipe(
|
|
264
|
+
const result = await planner.selectRecipe('test.workflow', {}, context);
|
|
315
265
|
|
|
316
|
-
expect(result.recipe.id).toBe(
|
|
266
|
+
expect(result.recipe.id).toBe('specific');
|
|
317
267
|
expect(result.score).toBe(100);
|
|
318
268
|
});
|
|
319
269
|
|
|
320
|
-
it(
|
|
321
|
-
registry.register(
|
|
322
|
-
|
|
323
|
-
);
|
|
324
|
-
registry.register(createTestRecipe({ id: "fast", variant: "fast" }));
|
|
270
|
+
it('should use preferred variant from hints', async () => {
|
|
271
|
+
registry.register(createTestRecipe({ id: 'default', variant: 'default' }));
|
|
272
|
+
registry.register(createTestRecipe({ id: 'fast', variant: 'fast' }));
|
|
325
273
|
|
|
326
274
|
const context: PlanningContext = {
|
|
327
|
-
hints: { preferredVariant:
|
|
275
|
+
hints: { preferredVariant: 'fast' },
|
|
328
276
|
};
|
|
329
277
|
|
|
330
|
-
const result = await planner.selectRecipe(
|
|
278
|
+
const result = await planner.selectRecipe('test.workflow', {}, context);
|
|
331
279
|
|
|
332
|
-
expect(result.recipe.id).toBe(
|
|
280
|
+
expect(result.recipe.id).toBe('fast');
|
|
333
281
|
});
|
|
334
282
|
|
|
335
|
-
it(
|
|
336
|
-
await expect(
|
|
337
|
-
|
|
338
|
-
);
|
|
283
|
+
it('should throw if no recipes found', async () => {
|
|
284
|
+
await expect(
|
|
285
|
+
planner.selectRecipe('nonexistent', {})
|
|
286
|
+
).rejects.toThrow('No recipes found for workflow kind: nonexistent');
|
|
339
287
|
});
|
|
340
288
|
});
|
|
341
289
|
|
|
342
|
-
describe(
|
|
343
|
-
it(
|
|
290
|
+
describe('generatePlan', () => {
|
|
291
|
+
it('should generate a plan from a recipe', async () => {
|
|
344
292
|
const recipe = createTestRecipe();
|
|
345
293
|
|
|
346
294
|
const plan = await planner.generatePlan(recipe, {});
|
|
347
295
|
|
|
348
|
-
expect(plan.recipeId).toBe(
|
|
349
|
-
expect(plan.variant).toBe(
|
|
296
|
+
expect(plan.recipeId).toBe('test.recipe');
|
|
297
|
+
expect(plan.variant).toBe('default');
|
|
350
298
|
expect(plan.steps).toHaveLength(2);
|
|
351
|
-
expect(plan.steps[0].key).toBe(
|
|
352
|
-
expect(plan.steps[1].key).toBe(
|
|
299
|
+
expect(plan.steps[0].key).toBe('step1');
|
|
300
|
+
expect(plan.steps[1].key).toBe('step2');
|
|
353
301
|
});
|
|
354
302
|
|
|
355
|
-
it(
|
|
303
|
+
it('should apply skip hints', async () => {
|
|
356
304
|
const recipe = createTestRecipe();
|
|
357
305
|
const context: PlanningContext = {
|
|
358
|
-
hints: { skipSteps: [
|
|
306
|
+
hints: { skipSteps: ['step1'] },
|
|
359
307
|
};
|
|
360
308
|
|
|
361
309
|
const plan = await planner.generatePlan(recipe, {}, context);
|
|
362
310
|
|
|
363
311
|
expect(plan.steps).toHaveLength(1);
|
|
364
|
-
expect(plan.steps[0].key).toBe(
|
|
312
|
+
expect(plan.steps[0].key).toBe('step2');
|
|
365
313
|
expect(plan.modifications).toContainEqual(
|
|
366
|
-
expect.objectContaining({ type:
|
|
314
|
+
expect.objectContaining({ type: 'remove_step', stepKey: 'step1' })
|
|
367
315
|
);
|
|
368
316
|
});
|
|
369
317
|
|
|
370
|
-
it(
|
|
318
|
+
it('should apply additional config from hints', async () => {
|
|
371
319
|
const recipe = createTestRecipe();
|
|
372
320
|
const context: PlanningContext = {
|
|
373
|
-
hints: { additionalConfig: { depth:
|
|
321
|
+
hints: { additionalConfig: { depth: 'comprehensive' } },
|
|
374
322
|
};
|
|
375
323
|
|
|
376
324
|
const plan = await planner.generatePlan(recipe, {}, context);
|
|
377
325
|
|
|
378
|
-
expect(plan.steps[0].config).toEqual({ depth:
|
|
379
|
-
expect(plan.steps[1].config).toEqual({ depth:
|
|
326
|
+
expect(plan.steps[0].config).toEqual({ depth: 'comprehensive' });
|
|
327
|
+
expect(plan.steps[1].config).toEqual({ depth: 'comprehensive' });
|
|
380
328
|
});
|
|
381
329
|
|
|
382
|
-
it(
|
|
330
|
+
it('should apply speed constraints', async () => {
|
|
383
331
|
const recipe = createTestRecipe({
|
|
384
332
|
steps: [
|
|
385
|
-
{
|
|
386
|
-
key: "step1",
|
|
387
|
-
name: "Step 1",
|
|
388
|
-
handlerRef: "h1",
|
|
389
|
-
timeout: 60000,
|
|
390
|
-
maxRetries: 5,
|
|
391
|
-
},
|
|
333
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'h1', timeout: 60000, maxRetries: 5 },
|
|
392
334
|
],
|
|
393
335
|
});
|
|
394
336
|
const context: PlanningContext = {
|
|
395
|
-
constraints: { priority:
|
|
337
|
+
constraints: { priority: 'speed' },
|
|
396
338
|
};
|
|
397
339
|
|
|
398
340
|
const plan = await planner.generatePlan(recipe, {}, context);
|
|
@@ -401,7 +343,7 @@ describe("RuleBasedPlanner", () => {
|
|
|
401
343
|
expect(plan.steps[0].maxRetries).toBeLessThanOrEqual(1);
|
|
402
344
|
});
|
|
403
345
|
|
|
404
|
-
it(
|
|
346
|
+
it('should apply duration constraints', async () => {
|
|
405
347
|
const recipe = createTestRecipe();
|
|
406
348
|
const context: PlanningContext = {
|
|
407
349
|
constraints: { maxDuration: 10000 },
|
|
@@ -414,15 +356,15 @@ describe("RuleBasedPlanner", () => {
|
|
|
414
356
|
expect(plan.steps[1].timeout).toBeLessThanOrEqual(5000);
|
|
415
357
|
});
|
|
416
358
|
|
|
417
|
-
it(
|
|
359
|
+
it('should apply cost constraints (maxRetries=0)', async () => {
|
|
418
360
|
const recipe = createTestRecipe({
|
|
419
361
|
steps: [
|
|
420
|
-
{ key:
|
|
421
|
-
{ key:
|
|
362
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'h1', maxRetries: 5 },
|
|
363
|
+
{ key: 'step2', name: 'Step 2', handlerRef: 'h2', maxRetries: 3 },
|
|
422
364
|
],
|
|
423
365
|
});
|
|
424
366
|
const context: PlanningContext = {
|
|
425
|
-
constraints: { priority:
|
|
367
|
+
constraints: { priority: 'cost' },
|
|
426
368
|
};
|
|
427
369
|
|
|
428
370
|
const plan = await planner.generatePlan(recipe, {}, context);
|
|
@@ -431,19 +373,19 @@ describe("RuleBasedPlanner", () => {
|
|
|
431
373
|
expect(plan.steps[1].maxRetries).toBe(0);
|
|
432
374
|
expect(plan.modifications).toContainEqual(
|
|
433
375
|
expect.objectContaining({
|
|
434
|
-
type:
|
|
435
|
-
value: { priority:
|
|
436
|
-
})
|
|
376
|
+
type: 'set_default',
|
|
377
|
+
value: { priority: 'cost' },
|
|
378
|
+
})
|
|
437
379
|
);
|
|
438
380
|
});
|
|
439
381
|
|
|
440
|
-
it(
|
|
382
|
+
it('should apply maxDuration to per-step timeouts evenly', async () => {
|
|
441
383
|
const recipe = createTestRecipe({
|
|
442
384
|
steps: [
|
|
443
|
-
{ key:
|
|
444
|
-
{ key:
|
|
445
|
-
{ key:
|
|
446
|
-
{ key:
|
|
385
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'h1' },
|
|
386
|
+
{ key: 'step2', name: 'Step 2', handlerRef: 'h2' },
|
|
387
|
+
{ key: 'step3', name: 'Step 3', handlerRef: 'h3' },
|
|
388
|
+
{ key: 'step4', name: 'Step 4', handlerRef: 'h4' },
|
|
447
389
|
],
|
|
448
390
|
});
|
|
449
391
|
const context: PlanningContext = {
|
|
@@ -458,17 +400,17 @@ describe("RuleBasedPlanner", () => {
|
|
|
458
400
|
}
|
|
459
401
|
expect(plan.modifications).toContainEqual(
|
|
460
402
|
expect.objectContaining({
|
|
461
|
-
type:
|
|
403
|
+
type: 'set_default',
|
|
462
404
|
value: { maxDuration: 20000 },
|
|
463
|
-
})
|
|
405
|
+
})
|
|
464
406
|
);
|
|
465
407
|
});
|
|
466
408
|
|
|
467
|
-
it(
|
|
409
|
+
it('should cap existing step timeout to per-step budget when maxDuration is set', async () => {
|
|
468
410
|
const recipe = createTestRecipe({
|
|
469
411
|
steps: [
|
|
470
|
-
{ key:
|
|
471
|
-
{ key:
|
|
412
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'h1', timeout: 30000 },
|
|
413
|
+
{ key: 'step2', name: 'Step 2', handlerRef: 'h2', timeout: 2000 },
|
|
472
414
|
],
|
|
473
415
|
});
|
|
474
416
|
const context: PlanningContext = {
|
|
@@ -483,22 +425,22 @@ describe("RuleBasedPlanner", () => {
|
|
|
483
425
|
});
|
|
484
426
|
});
|
|
485
427
|
|
|
486
|
-
describe(
|
|
487
|
-
it(
|
|
428
|
+
describe('plan', () => {
|
|
429
|
+
it('should select recipe and generate plan in one call', async () => {
|
|
488
430
|
registry.register(createTestRecipe());
|
|
489
431
|
|
|
490
|
-
const plan = await planner.plan(
|
|
432
|
+
const plan = await planner.plan('test.workflow', {});
|
|
491
433
|
|
|
492
|
-
expect(plan.recipeId).toBe(
|
|
434
|
+
expect(plan.recipeId).toBe('test.recipe');
|
|
493
435
|
expect(plan.steps).toHaveLength(2);
|
|
494
|
-
expect(plan.reasoning).toContain(
|
|
436
|
+
expect(plan.reasoning).toContain('test.recipe');
|
|
495
437
|
});
|
|
496
438
|
});
|
|
497
439
|
|
|
498
|
-
describe(
|
|
499
|
-
it(
|
|
440
|
+
describe('validatePlan', () => {
|
|
441
|
+
it('should validate a correct plan', async () => {
|
|
500
442
|
registry.register(createTestRecipe());
|
|
501
|
-
const plan = await planner.plan(
|
|
443
|
+
const plan = await planner.plan('test.workflow', {});
|
|
502
444
|
|
|
503
445
|
const result = planner.validatePlan(plan);
|
|
504
446
|
|
|
@@ -506,11 +448,11 @@ describe("RuleBasedPlanner", () => {
|
|
|
506
448
|
expect(result.errors).toHaveLength(0);
|
|
507
449
|
});
|
|
508
450
|
|
|
509
|
-
it(
|
|
451
|
+
it('should detect missing steps', () => {
|
|
510
452
|
const result = planner.validatePlan({
|
|
511
|
-
id:
|
|
512
|
-
recipeId:
|
|
513
|
-
variant:
|
|
453
|
+
id: 'plan-1',
|
|
454
|
+
recipeId: 'test',
|
|
455
|
+
variant: 'default',
|
|
514
456
|
modifications: [],
|
|
515
457
|
steps: [],
|
|
516
458
|
defaults: {},
|
|
@@ -518,37 +460,32 @@ describe("RuleBasedPlanner", () => {
|
|
|
518
460
|
});
|
|
519
461
|
|
|
520
462
|
expect(result.valid).toBe(false);
|
|
521
|
-
expect(result.errors).toContain(
|
|
463
|
+
expect(result.errors).toContain('Plan has no steps');
|
|
522
464
|
});
|
|
523
465
|
|
|
524
|
-
it(
|
|
466
|
+
it('should detect duplicate step keys', () => {
|
|
525
467
|
const result = planner.validatePlan({
|
|
526
|
-
id:
|
|
527
|
-
recipeId:
|
|
528
|
-
variant:
|
|
468
|
+
id: 'plan-1',
|
|
469
|
+
recipeId: 'test',
|
|
470
|
+
variant: 'default',
|
|
529
471
|
modifications: [],
|
|
530
472
|
steps: [
|
|
531
|
-
{ key:
|
|
532
|
-
{
|
|
533
|
-
key: "step1",
|
|
534
|
-
name: "Step 1 Duplicate",
|
|
535
|
-
handlerRef: "h2",
|
|
536
|
-
config: {},
|
|
537
|
-
},
|
|
473
|
+
{ key: 'step1', name: 'Step 1', handlerRef: 'h1', config: {} },
|
|
474
|
+
{ key: 'step1', name: 'Step 1 Duplicate', handlerRef: 'h2', config: {} },
|
|
538
475
|
],
|
|
539
476
|
defaults: {},
|
|
540
477
|
createdAt: new Date(),
|
|
541
478
|
});
|
|
542
479
|
|
|
543
480
|
expect(result.valid).toBe(false);
|
|
544
|
-
expect(result.errors).toContain(
|
|
481
|
+
expect(result.errors).toContain('Duplicate step key: step1');
|
|
545
482
|
});
|
|
546
483
|
});
|
|
547
484
|
|
|
548
|
-
describe(
|
|
549
|
-
it(
|
|
485
|
+
describe('estimateResources', () => {
|
|
486
|
+
it('should estimate resources for a plan', async () => {
|
|
550
487
|
registry.register(createTestRecipe());
|
|
551
|
-
const plan = await planner.plan(
|
|
488
|
+
const plan = await planner.plan('test.workflow', {});
|
|
552
489
|
|
|
553
490
|
const estimate = planner.estimateResources(plan);
|
|
554
491
|
|
|
@@ -559,8 +496,8 @@ describe("RuleBasedPlanner", () => {
|
|
|
559
496
|
});
|
|
560
497
|
});
|
|
561
498
|
|
|
562
|
-
describe(
|
|
563
|
-
it(
|
|
499
|
+
describe('createRegistry', () => {
|
|
500
|
+
it('should create a combined registry', () => {
|
|
564
501
|
const { recipes, handlers } = createRegistry();
|
|
565
502
|
|
|
566
503
|
expect(recipes).toBeInstanceOf(MemoryRecipeRegistry);
|
|
@@ -568,7 +505,7 @@ describe("createRegistry", () => {
|
|
|
568
505
|
});
|
|
569
506
|
});
|
|
570
507
|
|
|
571
|
-
describe(
|
|
508
|
+
describe('Condition Evaluation', () => {
|
|
572
509
|
let registry: MemoryRecipeRegistry;
|
|
573
510
|
let planner: RuleBasedPlanner;
|
|
574
511
|
|
|
@@ -580,211 +517,211 @@ describe("Condition Evaluation", () => {
|
|
|
580
517
|
const testConditions = async (
|
|
581
518
|
conditions: RecipeCondition[],
|
|
582
519
|
input: Record<string, unknown>,
|
|
583
|
-
shouldMatch: boolean
|
|
520
|
+
shouldMatch: boolean
|
|
584
521
|
) => {
|
|
585
522
|
registry.clear();
|
|
586
523
|
registry.register({
|
|
587
|
-
id:
|
|
588
|
-
name:
|
|
589
|
-
workflowKind:
|
|
590
|
-
variant:
|
|
591
|
-
steps: [{ key:
|
|
524
|
+
id: 'conditional',
|
|
525
|
+
name: 'Conditional Recipe',
|
|
526
|
+
workflowKind: 'test.workflow',
|
|
527
|
+
variant: 'conditional',
|
|
528
|
+
steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
|
|
592
529
|
conditions,
|
|
593
530
|
priority: 10,
|
|
594
531
|
});
|
|
595
532
|
registry.register({
|
|
596
|
-
id:
|
|
597
|
-
name:
|
|
598
|
-
workflowKind:
|
|
599
|
-
variant:
|
|
600
|
-
steps: [{ key:
|
|
533
|
+
id: 'default',
|
|
534
|
+
name: 'Default Recipe',
|
|
535
|
+
workflowKind: 'test.workflow',
|
|
536
|
+
variant: 'default',
|
|
537
|
+
steps: [{ key: 's1', name: 'S1', handlerRef: 'h1' }],
|
|
601
538
|
});
|
|
602
539
|
|
|
603
|
-
const result = await planner.selectRecipe(
|
|
540
|
+
const result = await planner.selectRecipe('test.workflow', input);
|
|
604
541
|
if (shouldMatch) {
|
|
605
|
-
expect(result.recipe.id).toBe(
|
|
542
|
+
expect(result.recipe.id).toBe('conditional');
|
|
606
543
|
} else {
|
|
607
|
-
expect(result.recipe.id).toBe(
|
|
544
|
+
expect(result.recipe.id).toBe('default');
|
|
608
545
|
}
|
|
609
546
|
};
|
|
610
547
|
|
|
611
|
-
it(
|
|
548
|
+
it('should evaluate eq operator', async () => {
|
|
612
549
|
await testConditions(
|
|
613
|
-
[{ field:
|
|
614
|
-
{ type:
|
|
615
|
-
true
|
|
550
|
+
[{ field: 'type', operator: 'eq', value: 'fast' }],
|
|
551
|
+
{ type: 'fast' },
|
|
552
|
+
true
|
|
616
553
|
);
|
|
617
554
|
await testConditions(
|
|
618
|
-
[{ field:
|
|
619
|
-
{ type:
|
|
620
|
-
false
|
|
555
|
+
[{ field: 'type', operator: 'eq', value: 'fast' }],
|
|
556
|
+
{ type: 'slow' },
|
|
557
|
+
false
|
|
621
558
|
);
|
|
622
559
|
});
|
|
623
560
|
|
|
624
|
-
it(
|
|
561
|
+
it('should evaluate neq operator', async () => {
|
|
625
562
|
await testConditions(
|
|
626
|
-
[{ field:
|
|
627
|
-
{ type:
|
|
628
|
-
true
|
|
563
|
+
[{ field: 'type', operator: 'neq', value: 'slow' }],
|
|
564
|
+
{ type: 'fast' },
|
|
565
|
+
true
|
|
629
566
|
);
|
|
630
567
|
await testConditions(
|
|
631
|
-
[{ field:
|
|
632
|
-
{ type:
|
|
633
|
-
false
|
|
568
|
+
[{ field: 'type', operator: 'neq', value: 'slow' }],
|
|
569
|
+
{ type: 'slow' },
|
|
570
|
+
false
|
|
634
571
|
);
|
|
635
572
|
});
|
|
636
573
|
|
|
637
|
-
it(
|
|
574
|
+
it('should evaluate gt operator', async () => {
|
|
638
575
|
await testConditions(
|
|
639
|
-
[{ field:
|
|
576
|
+
[{ field: 'count', operator: 'gt', value: 10 }],
|
|
640
577
|
{ count: 15 },
|
|
641
|
-
true
|
|
578
|
+
true
|
|
642
579
|
);
|
|
643
580
|
await testConditions(
|
|
644
|
-
[{ field:
|
|
581
|
+
[{ field: 'count', operator: 'gt', value: 10 }],
|
|
645
582
|
{ count: 5 },
|
|
646
|
-
false
|
|
583
|
+
false
|
|
647
584
|
);
|
|
648
585
|
});
|
|
649
586
|
|
|
650
|
-
it(
|
|
587
|
+
it('should evaluate gte operator', async () => {
|
|
651
588
|
await testConditions(
|
|
652
|
-
[{ field:
|
|
589
|
+
[{ field: 'count', operator: 'gte', value: 10 }],
|
|
653
590
|
{ count: 10 },
|
|
654
|
-
true
|
|
591
|
+
true
|
|
655
592
|
);
|
|
656
593
|
await testConditions(
|
|
657
|
-
[{ field:
|
|
594
|
+
[{ field: 'count', operator: 'gte', value: 10 }],
|
|
658
595
|
{ count: 9 },
|
|
659
|
-
false
|
|
596
|
+
false
|
|
660
597
|
);
|
|
661
598
|
});
|
|
662
599
|
|
|
663
|
-
it(
|
|
600
|
+
it('should evaluate lt operator', async () => {
|
|
664
601
|
await testConditions(
|
|
665
|
-
[{ field:
|
|
602
|
+
[{ field: 'count', operator: 'lt', value: 10 }],
|
|
666
603
|
{ count: 5 },
|
|
667
|
-
true
|
|
604
|
+
true
|
|
668
605
|
);
|
|
669
606
|
await testConditions(
|
|
670
|
-
[{ field:
|
|
607
|
+
[{ field: 'count', operator: 'lt', value: 10 }],
|
|
671
608
|
{ count: 15 },
|
|
672
|
-
false
|
|
609
|
+
false
|
|
673
610
|
);
|
|
674
611
|
});
|
|
675
612
|
|
|
676
|
-
it(
|
|
613
|
+
it('should evaluate lte operator', async () => {
|
|
677
614
|
await testConditions(
|
|
678
|
-
[{ field:
|
|
615
|
+
[{ field: 'count', operator: 'lte', value: 10 }],
|
|
679
616
|
{ count: 10 },
|
|
680
|
-
true
|
|
617
|
+
true
|
|
681
618
|
);
|
|
682
619
|
await testConditions(
|
|
683
|
-
[{ field:
|
|
620
|
+
[{ field: 'count', operator: 'lte', value: 10 }],
|
|
684
621
|
{ count: 11 },
|
|
685
|
-
false
|
|
622
|
+
false
|
|
686
623
|
);
|
|
687
624
|
});
|
|
688
625
|
|
|
689
|
-
it(
|
|
626
|
+
it('should evaluate contains operator for strings', async () => {
|
|
690
627
|
await testConditions(
|
|
691
|
-
[{ field:
|
|
692
|
-
{ name:
|
|
693
|
-
true
|
|
628
|
+
[{ field: 'name', operator: 'contains', value: 'test' }],
|
|
629
|
+
{ name: 'my-test-file' },
|
|
630
|
+
true
|
|
694
631
|
);
|
|
695
632
|
await testConditions(
|
|
696
|
-
[{ field:
|
|
697
|
-
{ name:
|
|
698
|
-
false
|
|
633
|
+
[{ field: 'name', operator: 'contains', value: 'test' }],
|
|
634
|
+
{ name: 'my-file' },
|
|
635
|
+
false
|
|
699
636
|
);
|
|
700
637
|
});
|
|
701
638
|
|
|
702
|
-
it(
|
|
639
|
+
it('should evaluate contains operator for arrays', async () => {
|
|
703
640
|
await testConditions(
|
|
704
|
-
[{ field:
|
|
705
|
-
{ tags: [
|
|
706
|
-
true
|
|
641
|
+
[{ field: 'tags', operator: 'contains', value: 'urgent' }],
|
|
642
|
+
{ tags: ['urgent', 'review'] },
|
|
643
|
+
true
|
|
707
644
|
);
|
|
708
645
|
await testConditions(
|
|
709
|
-
[{ field:
|
|
710
|
-
{ tags: [
|
|
711
|
-
false
|
|
646
|
+
[{ field: 'tags', operator: 'contains', value: 'urgent' }],
|
|
647
|
+
{ tags: ['normal'] },
|
|
648
|
+
false
|
|
712
649
|
);
|
|
713
650
|
});
|
|
714
651
|
|
|
715
|
-
it(
|
|
652
|
+
it('should evaluate matches operator', async () => {
|
|
716
653
|
await testConditions(
|
|
717
|
-
[{ field:
|
|
718
|
-
{ email:
|
|
719
|
-
true
|
|
654
|
+
[{ field: 'email', operator: 'matches', value: '^test@' }],
|
|
655
|
+
{ email: 'test@example.com' },
|
|
656
|
+
true
|
|
720
657
|
);
|
|
721
658
|
await testConditions(
|
|
722
|
-
[{ field:
|
|
723
|
-
{ email:
|
|
724
|
-
false
|
|
659
|
+
[{ field: 'email', operator: 'matches', value: '^test@' }],
|
|
660
|
+
{ email: 'user@example.com' },
|
|
661
|
+
false
|
|
725
662
|
);
|
|
726
663
|
});
|
|
727
664
|
|
|
728
|
-
it(
|
|
665
|
+
it('should evaluate exists operator', async () => {
|
|
729
666
|
await testConditions(
|
|
730
|
-
[{ field:
|
|
731
|
-
{ optional:
|
|
732
|
-
true
|
|
667
|
+
[{ field: 'optional', operator: 'exists' }],
|
|
668
|
+
{ optional: 'value' },
|
|
669
|
+
true
|
|
733
670
|
);
|
|
734
671
|
await testConditions(
|
|
735
|
-
[{ field:
|
|
736
|
-
{ other:
|
|
737
|
-
false
|
|
672
|
+
[{ field: 'optional', operator: 'exists' }],
|
|
673
|
+
{ other: 'value' },
|
|
674
|
+
false
|
|
738
675
|
);
|
|
739
676
|
await testConditions(
|
|
740
|
-
[{ field:
|
|
741
|
-
{ optional:
|
|
742
|
-
false
|
|
677
|
+
[{ field: 'optional', operator: 'exists' }],
|
|
678
|
+
{ optional: '' },
|
|
679
|
+
false
|
|
743
680
|
);
|
|
744
681
|
});
|
|
745
682
|
|
|
746
|
-
it(
|
|
683
|
+
it('should evaluate notExists operator', async () => {
|
|
747
684
|
await testConditions(
|
|
748
|
-
[{ field:
|
|
749
|
-
{ other:
|
|
750
|
-
true
|
|
685
|
+
[{ field: 'optional', operator: 'notExists' }],
|
|
686
|
+
{ other: 'value' },
|
|
687
|
+
true
|
|
751
688
|
);
|
|
752
689
|
await testConditions(
|
|
753
|
-
[{ field:
|
|
754
|
-
{ optional:
|
|
755
|
-
false
|
|
690
|
+
[{ field: 'optional', operator: 'notExists' }],
|
|
691
|
+
{ optional: 'value' },
|
|
692
|
+
false
|
|
756
693
|
);
|
|
757
694
|
});
|
|
758
695
|
|
|
759
|
-
it(
|
|
696
|
+
it('should evaluate nested fields', async () => {
|
|
760
697
|
await testConditions(
|
|
761
|
-
[{ field:
|
|
698
|
+
[{ field: 'config.settings.enabled', operator: 'eq', value: true }],
|
|
762
699
|
{ config: { settings: { enabled: true } } },
|
|
763
|
-
true
|
|
700
|
+
true
|
|
764
701
|
);
|
|
765
702
|
await testConditions(
|
|
766
|
-
[{ field:
|
|
703
|
+
[{ field: 'config.settings.enabled', operator: 'eq', value: true }],
|
|
767
704
|
{ config: { settings: { enabled: false } } },
|
|
768
|
-
false
|
|
705
|
+
false
|
|
769
706
|
);
|
|
770
707
|
});
|
|
771
708
|
|
|
772
|
-
it(
|
|
709
|
+
it('should require all conditions to match', async () => {
|
|
773
710
|
await testConditions(
|
|
774
711
|
[
|
|
775
|
-
{ field:
|
|
776
|
-
{ field:
|
|
712
|
+
{ field: 'type', operator: 'eq', value: 'fast' },
|
|
713
|
+
{ field: 'count', operator: 'gt', value: 5 },
|
|
777
714
|
],
|
|
778
|
-
{ type:
|
|
779
|
-
true
|
|
715
|
+
{ type: 'fast', count: 10 },
|
|
716
|
+
true
|
|
780
717
|
);
|
|
781
718
|
await testConditions(
|
|
782
719
|
[
|
|
783
|
-
{ field:
|
|
784
|
-
{ field:
|
|
720
|
+
{ field: 'type', operator: 'eq', value: 'fast' },
|
|
721
|
+
{ field: 'count', operator: 'gt', value: 5 },
|
|
785
722
|
],
|
|
786
|
-
{ type:
|
|
787
|
-
false
|
|
723
|
+
{ type: 'fast', count: 3 },
|
|
724
|
+
false
|
|
788
725
|
);
|
|
789
726
|
});
|
|
790
727
|
});
|