@arke-institute/rhiza 0.4.1 → 0.5.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.
- package/dist/__tests__/unit/handoff/recurse.test.d.ts +10 -0
- package/dist/__tests__/unit/handoff/recurse.test.d.ts.map +1 -0
- package/dist/__tests__/unit/handoff/recurse.test.js +256 -0
- package/dist/__tests__/unit/handoff/recurse.test.js.map +1 -0
- package/dist/__tests__/unit/validation/rhiza.test.js +266 -1
- package/dist/__tests__/unit/validation/rhiza.test.js.map +1 -1
- package/dist/__tests__/unit/worker/job.test.js +52 -0
- package/dist/__tests__/unit/worker/job.test.js.map +1 -1
- package/dist/handoff/interpret.d.ts +3 -1
- package/dist/handoff/interpret.d.ts.map +1 -1
- package/dist/handoff/interpret.js +79 -0
- package/dist/handoff/interpret.js.map +1 -1
- package/dist/handoff/invoke.d.ts +5 -0
- package/dist/handoff/invoke.d.ts.map +1 -1
- package/dist/handoff/invoke.js +1 -0
- package/dist/handoff/invoke.js.map +1 -1
- package/dist/handoff/target.d.ts.map +1 -1
- package/dist/handoff/target.js +4 -0
- package/dist/handoff/target.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registration/collection.d.ts +6 -5
- package/dist/registration/collection.d.ts.map +1 -1
- package/dist/registration/collection.js +25 -23
- package/dist/registration/collection.js.map +1 -1
- package/dist/registration/klados/sync.d.ts.map +1 -1
- package/dist/registration/klados/sync.js +5 -5
- package/dist/registration/klados/sync.js.map +1 -1
- package/dist/registration/klados/types.d.ts +3 -1
- package/dist/registration/klados/types.d.ts.map +1 -1
- package/dist/registration/rhiza/sync.js +4 -4
- package/dist/registration/rhiza/sync.js.map +1 -1
- package/dist/registration/rhiza/types.d.ts +3 -1
- package/dist/registration/rhiza/types.d.ts.map +1 -1
- package/dist/types/log.d.ts +4 -2
- package/dist/types/log.d.ts.map +1 -1
- package/dist/types/request.d.ts +7 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/rhiza.d.ts +5 -1
- package/dist/types/rhiza.d.ts.map +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/validate-rhiza.d.ts +14 -0
- package/dist/validation/validate-rhiza.d.ts.map +1 -1
- package/dist/validation/validate-rhiza.js +73 -2
- package/dist/validation/validate-rhiza.js.map +1 -1
- package/dist/worker/job.d.ts +5 -0
- package/dist/worker/job.d.ts.map +1 -1
- package/dist/worker/job.js +8 -0
- package/dist/worker/job.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recurse.test.d.ts","sourceRoot":"","sources":["../../../../src/__tests__/unit/handoff/recurse.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recurse Handoff Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for recurse handoff functionality:
|
|
5
|
+
* - groupOutputsByTarget with recurse ThenSpec
|
|
6
|
+
* - Route resolution for recurse
|
|
7
|
+
* - Depth tracking behavior
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { groupOutputsByTarget, resolveTarget } from '../../../handoff/target';
|
|
11
|
+
describe('Recurse Handoff', () => {
|
|
12
|
+
describe('resolveTarget with recurse', () => {
|
|
13
|
+
it('resolves to default recurse target when no routes', () => {
|
|
14
|
+
const then = { recurse: 'cluster' };
|
|
15
|
+
const properties = { entity_id: 'ent_1' };
|
|
16
|
+
const target = resolveTarget(then, properties);
|
|
17
|
+
expect(target).toBe('cluster');
|
|
18
|
+
});
|
|
19
|
+
it('resolves to route target when route matches', () => {
|
|
20
|
+
const then = {
|
|
21
|
+
recurse: 'cluster',
|
|
22
|
+
route: [
|
|
23
|
+
{ where: { property: 'should_terminate', equals: true }, target: 'done' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
const properties = { entity_id: 'ent_1', should_terminate: true };
|
|
27
|
+
const target = resolveTarget(then, properties);
|
|
28
|
+
expect(target).toBe('done');
|
|
29
|
+
});
|
|
30
|
+
it('resolves to default when no route matches', () => {
|
|
31
|
+
const then = {
|
|
32
|
+
recurse: 'cluster',
|
|
33
|
+
route: [
|
|
34
|
+
{ where: { property: 'should_terminate', equals: true }, target: 'done' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const properties = { entity_id: 'ent_1', should_terminate: false };
|
|
38
|
+
const target = resolveTarget(then, properties);
|
|
39
|
+
expect(target).toBe('cluster');
|
|
40
|
+
});
|
|
41
|
+
it('handles recurse with max_depth (max_depth ignored in routing)', () => {
|
|
42
|
+
const then = {
|
|
43
|
+
recurse: 'cluster',
|
|
44
|
+
max_depth: 10,
|
|
45
|
+
};
|
|
46
|
+
const properties = { entity_id: 'ent_1' };
|
|
47
|
+
// max_depth doesn't affect routing - it's handled in handleRecurse
|
|
48
|
+
const target = resolveTarget(then, properties);
|
|
49
|
+
expect(target).toBe('cluster');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('groupOutputsByTarget with recurse', () => {
|
|
53
|
+
it('groups all outputs to recurse target when no routes', () => {
|
|
54
|
+
const outputs = ['ent_1', 'ent_2', 'ent_3'];
|
|
55
|
+
const then = { recurse: 'cluster' };
|
|
56
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
57
|
+
expect(groups.size).toBe(1);
|
|
58
|
+
expect(groups.get('cluster')).toHaveLength(3);
|
|
59
|
+
expect(groups.get('cluster')[0].entity_id).toBe('ent_1');
|
|
60
|
+
expect(groups.get('cluster')[1].entity_id).toBe('ent_2');
|
|
61
|
+
expect(groups.get('cluster')[2].entity_id).toBe('ent_3');
|
|
62
|
+
});
|
|
63
|
+
it('routes items to "done" when condition matches', () => {
|
|
64
|
+
const outputs = [
|
|
65
|
+
{ entity_id: 'ent_1', is_root: false },
|
|
66
|
+
{ entity_id: 'ent_2', is_root: true },
|
|
67
|
+
{ entity_id: 'ent_3', is_root: false },
|
|
68
|
+
];
|
|
69
|
+
const then = {
|
|
70
|
+
recurse: 'cluster',
|
|
71
|
+
route: [
|
|
72
|
+
{ where: { property: 'is_root', equals: true }, target: 'done' },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
76
|
+
expect(groups.size).toBe(2);
|
|
77
|
+
expect(groups.get('cluster')).toHaveLength(2);
|
|
78
|
+
expect(groups.get('done')).toHaveLength(1);
|
|
79
|
+
expect(groups.get('done')[0].entity_id).toBe('ent_2');
|
|
80
|
+
});
|
|
81
|
+
it('handles all outputs routed to "done" (termination case)', () => {
|
|
82
|
+
const outputs = [
|
|
83
|
+
{ entity_id: 'ent_1', complete: true },
|
|
84
|
+
{ entity_id: 'ent_2', complete: true },
|
|
85
|
+
];
|
|
86
|
+
const then = {
|
|
87
|
+
recurse: 'cluster',
|
|
88
|
+
route: [
|
|
89
|
+
{ where: { property: 'complete', equals: true }, target: 'done' },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
93
|
+
expect(groups.size).toBe(1);
|
|
94
|
+
expect(groups.get('done')).toHaveLength(2);
|
|
95
|
+
expect(groups.has('cluster')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('handles empty outputs (base case)', () => {
|
|
98
|
+
const outputs = [];
|
|
99
|
+
const then = { recurse: 'cluster', max_depth: 20 };
|
|
100
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
101
|
+
expect(groups.size).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
it('handles complex routing with AND condition', () => {
|
|
104
|
+
const outputs = [
|
|
105
|
+
{ entity_id: 'ent_1', layer: 3, count: 1 },
|
|
106
|
+
{ entity_id: 'ent_2', layer: 3, count: 5 },
|
|
107
|
+
{ entity_id: 'ent_3', layer: 2, count: 1 },
|
|
108
|
+
];
|
|
109
|
+
const then = {
|
|
110
|
+
recurse: 'cluster',
|
|
111
|
+
route: [
|
|
112
|
+
{
|
|
113
|
+
where: {
|
|
114
|
+
and: [
|
|
115
|
+
{ property: 'layer', equals: 3 },
|
|
116
|
+
{ property: 'count', equals: 1 },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
target: 'done',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
124
|
+
expect(groups.size).toBe(2);
|
|
125
|
+
expect(groups.get('done')).toHaveLength(1);
|
|
126
|
+
expect(groups.get('done')[0].entity_id).toBe('ent_1');
|
|
127
|
+
expect(groups.get('cluster')).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
it('handles OR condition routing', () => {
|
|
130
|
+
const outputs = [
|
|
131
|
+
{ entity_id: 'ent_1', status: 'complete' },
|
|
132
|
+
{ entity_id: 'ent_2', status: 'error' },
|
|
133
|
+
{ entity_id: 'ent_3', status: 'pending' },
|
|
134
|
+
];
|
|
135
|
+
const then = {
|
|
136
|
+
recurse: 'process',
|
|
137
|
+
route: [
|
|
138
|
+
{
|
|
139
|
+
where: {
|
|
140
|
+
or: [
|
|
141
|
+
{ property: 'status', equals: 'complete' },
|
|
142
|
+
{ property: 'status', equals: 'error' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
target: 'done',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
150
|
+
expect(groups.get('done')).toHaveLength(2);
|
|
151
|
+
expect(groups.get('process')).toHaveLength(1);
|
|
152
|
+
expect(groups.get('process')[0].entity_id).toBe('ent_3');
|
|
153
|
+
});
|
|
154
|
+
it('preserves OutputItem properties after grouping', () => {
|
|
155
|
+
const outputs = [
|
|
156
|
+
{ entity_id: 'ent_1', cluster_id: 'c1', member_count: 5 },
|
|
157
|
+
{ entity_id: 'ent_2', cluster_id: 'c2', member_count: 3 },
|
|
158
|
+
];
|
|
159
|
+
const then = { recurse: 'describe' };
|
|
160
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
161
|
+
const grouped = groups.get('describe');
|
|
162
|
+
expect(grouped[0]).toEqual({ entity_id: 'ent_1', cluster_id: 'c1', member_count: 5 });
|
|
163
|
+
expect(grouped[1]).toEqual({ entity_id: 'ent_2', cluster_id: 'c2', member_count: 3 });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('recurse routing scenarios', () => {
|
|
167
|
+
describe('knowledge graph clustering', () => {
|
|
168
|
+
it('simulates cluster decision: continue recursion', () => {
|
|
169
|
+
// Multiple cluster leaders returned -> recurse to describe
|
|
170
|
+
const outputs = [
|
|
171
|
+
{ entity_id: 'leader_1', is_leader: true },
|
|
172
|
+
{ entity_id: 'leader_2', is_leader: true },
|
|
173
|
+
{ entity_id: 'leader_3', is_leader: true },
|
|
174
|
+
];
|
|
175
|
+
const then = { recurse: 'cluster' };
|
|
176
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
177
|
+
// All should go to cluster (for the next iteration)
|
|
178
|
+
expect(groups.get('cluster')).toHaveLength(3);
|
|
179
|
+
});
|
|
180
|
+
it('simulates cluster decision: single root terminates', () => {
|
|
181
|
+
// Single output means we've reached the root
|
|
182
|
+
// In practice, the cluster klados would return empty,
|
|
183
|
+
// but we can also use routing to terminate
|
|
184
|
+
const outputs = [
|
|
185
|
+
{ entity_id: 'root', is_single: true },
|
|
186
|
+
];
|
|
187
|
+
const then = {
|
|
188
|
+
recurse: 'cluster',
|
|
189
|
+
route: [
|
|
190
|
+
{ where: { property: 'is_single', equals: true }, target: 'done' },
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
194
|
+
expect(groups.get('done')).toHaveLength(1);
|
|
195
|
+
expect(groups.has('cluster')).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
it('simulates mixed routing: some done, some continue', () => {
|
|
198
|
+
// Some entities are already clustered (joiners), others are new leaders
|
|
199
|
+
const outputs = [
|
|
200
|
+
{ entity_id: 'leader_1', action: 'created_leader' },
|
|
201
|
+
{ entity_id: 'joiner_1', action: 'joined_existing' },
|
|
202
|
+
{ entity_id: 'leader_2', action: 'created_leader' },
|
|
203
|
+
{ entity_id: 'joiner_2', action: 'joined_existing' },
|
|
204
|
+
];
|
|
205
|
+
const then = {
|
|
206
|
+
recurse: 'describe',
|
|
207
|
+
route: [
|
|
208
|
+
{ where: { property: 'action', equals: 'joined_existing' }, target: 'done' },
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
212
|
+
expect(groups.get('describe')).toHaveLength(2);
|
|
213
|
+
expect(groups.get('done')).toHaveLength(2);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe('iterative processing', () => {
|
|
217
|
+
it('routes based on iteration depth property', () => {
|
|
218
|
+
// Items carry their own depth tracking
|
|
219
|
+
const outputs = [
|
|
220
|
+
{ entity_id: 'item_1', iteration: 1 },
|
|
221
|
+
{ entity_id: 'item_2', iteration: 5 },
|
|
222
|
+
{ entity_id: 'item_3', iteration: 10 },
|
|
223
|
+
];
|
|
224
|
+
const then = {
|
|
225
|
+
recurse: 'process',
|
|
226
|
+
max_depth: 100,
|
|
227
|
+
route: [
|
|
228
|
+
{ where: { property: 'iteration', equals: 10 }, target: 'done' },
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
232
|
+
expect(groups.get('process')).toHaveLength(2);
|
|
233
|
+
expect(groups.get('done')).toHaveLength(1);
|
|
234
|
+
expect(groups.get('done')[0].entity_id).toBe('item_3');
|
|
235
|
+
});
|
|
236
|
+
it('handles convergence check routing', () => {
|
|
237
|
+
// Items with no change should terminate
|
|
238
|
+
const outputs = [
|
|
239
|
+
{ entity_id: 'item_1', changed: true },
|
|
240
|
+
{ entity_id: 'item_2', changed: false },
|
|
241
|
+
{ entity_id: 'item_3', changed: true },
|
|
242
|
+
];
|
|
243
|
+
const then = {
|
|
244
|
+
recurse: 'refine',
|
|
245
|
+
route: [
|
|
246
|
+
{ where: { property: 'changed', equals: false }, target: 'done' },
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
const groups = groupOutputsByTarget(outputs, then);
|
|
250
|
+
expect(groups.get('refine')).toHaveLength(2);
|
|
251
|
+
expect(groups.get('done')).toHaveLength(1);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
//# sourceMappingURL=recurse.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recurse.test.js","sourceRoot":"","sources":["../../../../src/__tests__/unit/handoff/recurse.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - ThenSpec targets are step names (strings)
|
|
21
21
|
*/
|
|
22
22
|
import { describe, it, expect } from 'vitest';
|
|
23
|
-
import { validateRhizaProperties } from '../../../validation';
|
|
23
|
+
import { validateRhizaProperties, validateRhizaUpdate } from '../../../validation';
|
|
24
24
|
import { ref } from '../../../types';
|
|
25
25
|
import { linearRhizaProperties, scatterGatherRhizaProperties, conditionalRhizaProperties, complexRoutingRhizaProperties, invalidRhizaProperties, } from '../../fixtures';
|
|
26
26
|
describe('validateRhizaProperties', () => {
|
|
@@ -212,6 +212,52 @@ describe('validateRhizaProperties', () => {
|
|
|
212
212
|
const cycleError = result.errors.find((e) => e.code === 'CYCLE_DETECTED');
|
|
213
213
|
expect(cycleError?.message).toContain('->');
|
|
214
214
|
});
|
|
215
|
+
it('does NOT flag recurse as a cycle (recurse is allowed)', () => {
|
|
216
|
+
const result = validateRhizaProperties({
|
|
217
|
+
label: 'Recurse Loop',
|
|
218
|
+
version: '1.0.0',
|
|
219
|
+
entry: 'step_a',
|
|
220
|
+
flow: {
|
|
221
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
222
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a' } },
|
|
223
|
+
},
|
|
224
|
+
status: 'active',
|
|
225
|
+
});
|
|
226
|
+
// Recurse should NOT be flagged as a cycle
|
|
227
|
+
expect(result.valid).toBe(true);
|
|
228
|
+
expect(result.errors.filter(e => e.code === 'CYCLE_DETECTED')).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
it('does NOT flag recurse self-reference as a cycle', () => {
|
|
231
|
+
const result = validateRhizaProperties({
|
|
232
|
+
label: 'Self Recurse',
|
|
233
|
+
version: '1.0.0',
|
|
234
|
+
entry: 'step_a',
|
|
235
|
+
flow: {
|
|
236
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { recurse: 'step_a' } },
|
|
237
|
+
},
|
|
238
|
+
status: 'active',
|
|
239
|
+
});
|
|
240
|
+
// Recurse to self should be valid (bounded by max_depth)
|
|
241
|
+
expect(result.valid).toBe(true);
|
|
242
|
+
expect(result.errors.filter(e => e.code === 'CYCLE_DETECTED')).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
it('still flags regular pass cycles even when recurse is present elsewhere', () => {
|
|
245
|
+
const result = validateRhizaProperties({
|
|
246
|
+
label: 'Mixed Cycles',
|
|
247
|
+
version: '1.0.0',
|
|
248
|
+
entry: 'step_a',
|
|
249
|
+
flow: {
|
|
250
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
251
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { pass: 'step_c' } },
|
|
252
|
+
'step_c': { klados: ref('II01klados_c', { type: 'klados' }), then: { pass: 'step_a' } }, // Regular cycle - should fail
|
|
253
|
+
},
|
|
254
|
+
status: 'active',
|
|
255
|
+
});
|
|
256
|
+
expect(result.valid).toBe(false);
|
|
257
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
258
|
+
code: 'CYCLE_DETECTED',
|
|
259
|
+
}));
|
|
260
|
+
});
|
|
215
261
|
});
|
|
216
262
|
// =========================================================================
|
|
217
263
|
// Unreachable Detection
|
|
@@ -310,6 +356,118 @@ describe('validateRhizaProperties', () => {
|
|
|
310
356
|
});
|
|
311
357
|
expect(result.valid).toBe(true);
|
|
312
358
|
});
|
|
359
|
+
it('accepts recurse handoff', () => {
|
|
360
|
+
const result = validateRhizaProperties({
|
|
361
|
+
label: 'Recurse Test',
|
|
362
|
+
version: '1.0.0',
|
|
363
|
+
entry: 'step_a',
|
|
364
|
+
flow: {
|
|
365
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
366
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a' } },
|
|
367
|
+
},
|
|
368
|
+
status: 'active',
|
|
369
|
+
});
|
|
370
|
+
expect(result.valid).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
it('accepts recurse handoff with max_depth', () => {
|
|
373
|
+
const result = validateRhizaProperties({
|
|
374
|
+
label: 'Recurse with Depth',
|
|
375
|
+
version: '1.0.0',
|
|
376
|
+
entry: 'step_a',
|
|
377
|
+
flow: {
|
|
378
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
379
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a', max_depth: 20 } },
|
|
380
|
+
},
|
|
381
|
+
status: 'active',
|
|
382
|
+
});
|
|
383
|
+
expect(result.valid).toBe(true);
|
|
384
|
+
});
|
|
385
|
+
it('fails when recurse max_depth is negative', () => {
|
|
386
|
+
const result = validateRhizaProperties({
|
|
387
|
+
label: 'Invalid Max Depth',
|
|
388
|
+
version: '1.0.0',
|
|
389
|
+
entry: 'step_a',
|
|
390
|
+
flow: {
|
|
391
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
392
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a', max_depth: -1 } },
|
|
393
|
+
},
|
|
394
|
+
status: 'active',
|
|
395
|
+
});
|
|
396
|
+
expect(result.valid).toBe(false);
|
|
397
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
398
|
+
code: 'INVALID_MAX_DEPTH',
|
|
399
|
+
}));
|
|
400
|
+
});
|
|
401
|
+
it('fails when recurse max_depth is zero', () => {
|
|
402
|
+
const result = validateRhizaProperties({
|
|
403
|
+
label: 'Zero Max Depth',
|
|
404
|
+
version: '1.0.0',
|
|
405
|
+
entry: 'step_a',
|
|
406
|
+
flow: {
|
|
407
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
408
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a', max_depth: 0 } },
|
|
409
|
+
},
|
|
410
|
+
status: 'active',
|
|
411
|
+
});
|
|
412
|
+
expect(result.valid).toBe(false);
|
|
413
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
414
|
+
code: 'INVALID_MAX_DEPTH',
|
|
415
|
+
}));
|
|
416
|
+
});
|
|
417
|
+
it('fails when recurse max_depth is not an integer', () => {
|
|
418
|
+
const result = validateRhizaProperties({
|
|
419
|
+
label: 'Float Max Depth',
|
|
420
|
+
version: '1.0.0',
|
|
421
|
+
entry: 'step_a',
|
|
422
|
+
flow: {
|
|
423
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
424
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a', max_depth: 10.5 } },
|
|
425
|
+
},
|
|
426
|
+
status: 'active',
|
|
427
|
+
});
|
|
428
|
+
expect(result.valid).toBe(false);
|
|
429
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
430
|
+
code: 'INVALID_MAX_DEPTH',
|
|
431
|
+
}));
|
|
432
|
+
});
|
|
433
|
+
it('fails when recurse target does not exist', () => {
|
|
434
|
+
const result = validateRhizaProperties({
|
|
435
|
+
label: 'Invalid Recurse Target',
|
|
436
|
+
version: '1.0.0',
|
|
437
|
+
entry: 'step_a',
|
|
438
|
+
flow: {
|
|
439
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
440
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'nonexistent' } },
|
|
441
|
+
},
|
|
442
|
+
status: 'active',
|
|
443
|
+
});
|
|
444
|
+
expect(result.valid).toBe(false);
|
|
445
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
446
|
+
code: 'INVALID_TARGET',
|
|
447
|
+
}));
|
|
448
|
+
});
|
|
449
|
+
it('accepts recurse with route rules', () => {
|
|
450
|
+
const result = validateRhizaProperties({
|
|
451
|
+
label: 'Recurse with Routes',
|
|
452
|
+
version: '1.0.0',
|
|
453
|
+
entry: 'step_a',
|
|
454
|
+
flow: {
|
|
455
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
456
|
+
'step_b': {
|
|
457
|
+
klados: ref('II01klados_b', { type: 'klados' }),
|
|
458
|
+
then: {
|
|
459
|
+
recurse: 'step_a',
|
|
460
|
+
max_depth: 10,
|
|
461
|
+
route: [
|
|
462
|
+
{ where: { property: 'should_terminate', equals: true }, target: 'done' },
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
status: 'active',
|
|
468
|
+
});
|
|
469
|
+
expect(result.valid).toBe(true);
|
|
470
|
+
});
|
|
313
471
|
});
|
|
314
472
|
// =========================================================================
|
|
315
473
|
// Route Rules Validation
|
|
@@ -639,4 +797,111 @@ describe('validateRhizaProperties', () => {
|
|
|
639
797
|
});
|
|
640
798
|
});
|
|
641
799
|
});
|
|
800
|
+
// =========================================================================
|
|
801
|
+
// validateRhizaUpdate Tests
|
|
802
|
+
// =========================================================================
|
|
803
|
+
describe('validateRhizaUpdate', () => {
|
|
804
|
+
const existingProperties = {
|
|
805
|
+
label: 'Original Workflow',
|
|
806
|
+
version: '1.0.0',
|
|
807
|
+
entry: 'step_a',
|
|
808
|
+
flow: {
|
|
809
|
+
'step_a': { klados: ref('II01klados_a', { type: 'klados' }), then: { pass: 'step_b' } },
|
|
810
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { done: true } },
|
|
811
|
+
},
|
|
812
|
+
status: 'active',
|
|
813
|
+
};
|
|
814
|
+
it('skips validation when updating label only (no structural changes)', () => {
|
|
815
|
+
const update = { label: 'Updated Workflow' };
|
|
816
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
817
|
+
// When neither entry nor flow is being updated, validation is skipped
|
|
818
|
+
expect(result.valid).toBe(true);
|
|
819
|
+
expect(result.errors).toHaveLength(0);
|
|
820
|
+
expect(result.warnings).toHaveLength(0);
|
|
821
|
+
});
|
|
822
|
+
it('skips validation when updating version only (no structural changes)', () => {
|
|
823
|
+
const update = { version: '2.0.0' };
|
|
824
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
825
|
+
// When neither entry nor flow is being updated, validation is skipped
|
|
826
|
+
expect(result.valid).toBe(true);
|
|
827
|
+
expect(result.errors).toHaveLength(0);
|
|
828
|
+
expect(result.warnings).toHaveLength(0);
|
|
829
|
+
});
|
|
830
|
+
it('skips validation when update is empty', () => {
|
|
831
|
+
const update = {};
|
|
832
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
833
|
+
// When neither entry nor flow is being updated, validation is skipped
|
|
834
|
+
expect(result.valid).toBe(true);
|
|
835
|
+
expect(result.errors).toHaveLength(0);
|
|
836
|
+
});
|
|
837
|
+
it('validates merged properties when changing entry', () => {
|
|
838
|
+
const update = { entry: 'step_b' };
|
|
839
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
840
|
+
expect(result.valid).toBe(true);
|
|
841
|
+
expect(result.errors).toHaveLength(0);
|
|
842
|
+
});
|
|
843
|
+
it('fails when changing entry to non-existent step', () => {
|
|
844
|
+
const update = { entry: 'nonexistent' };
|
|
845
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
846
|
+
expect(result.valid).toBe(false);
|
|
847
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
848
|
+
code: 'ENTRY_NOT_IN_FLOW',
|
|
849
|
+
}));
|
|
850
|
+
});
|
|
851
|
+
it('merges flow at step level when updating flow', () => {
|
|
852
|
+
// Add a new step to the existing flow
|
|
853
|
+
const update = {
|
|
854
|
+
flow: {
|
|
855
|
+
'step_c': { klados: ref('II01klados_c', { type: 'klados' }), then: { done: true } },
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
859
|
+
// Should be valid - the new step is added but also existing steps remain
|
|
860
|
+
expect(result.valid).toBe(true);
|
|
861
|
+
});
|
|
862
|
+
it('can update an existing step in flow', () => {
|
|
863
|
+
// Change step_b to point somewhere else
|
|
864
|
+
const update = {
|
|
865
|
+
flow: {
|
|
866
|
+
'step_b': { klados: ref('II01klados_b_updated', { type: 'klados' }), then: { pass: 'step_c' } },
|
|
867
|
+
'step_c': { klados: ref('II01klados_c', { type: 'klados' }), then: { done: true } },
|
|
868
|
+
},
|
|
869
|
+
};
|
|
870
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
871
|
+
expect(result.valid).toBe(true);
|
|
872
|
+
});
|
|
873
|
+
it('fails when update introduces invalid target', () => {
|
|
874
|
+
const update = {
|
|
875
|
+
flow: {
|
|
876
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { pass: 'nonexistent' } },
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
880
|
+
expect(result.valid).toBe(false);
|
|
881
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
882
|
+
code: 'INVALID_TARGET',
|
|
883
|
+
}));
|
|
884
|
+
});
|
|
885
|
+
it('fails when update introduces a cycle', () => {
|
|
886
|
+
const update = {
|
|
887
|
+
flow: {
|
|
888
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { pass: 'step_a' } },
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
892
|
+
expect(result.valid).toBe(false);
|
|
893
|
+
expect(result.errors).toContainEqual(expect.objectContaining({
|
|
894
|
+
code: 'CYCLE_DETECTED',
|
|
895
|
+
}));
|
|
896
|
+
});
|
|
897
|
+
it('succeeds when update introduces recurse (not a cycle)', () => {
|
|
898
|
+
const update = {
|
|
899
|
+
flow: {
|
|
900
|
+
'step_b': { klados: ref('II01klados_b', { type: 'klados' }), then: { recurse: 'step_a', max_depth: 10 } },
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
const result = validateRhizaUpdate(update, existingProperties);
|
|
904
|
+
expect(result.valid).toBe(true);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
642
907
|
//# sourceMappingURL=rhiza.test.js.map
|