elsabro 7.3.0 → 7.3.2
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/README.md +42 -5
- package/agents/elsabro-orchestrator.md +2 -0
- package/commands/elsabro/execute.md +322 -5
- package/commands/elsabro/quick.md +11 -11
- package/commands/elsabro/start.md +156 -14
- package/flow-engine/src/callbacks.js +79 -0
- package/flow-engine/src/checkpoint.js +41 -0
- package/flow-engine/src/cli.js +214 -6
- package/flow-engine/src/executors.js +134 -3
- package/flow-engine/src/graph.js +30 -2
- package/flow-engine/src/template.js +152 -72
- package/flow-engine/tests/checkpoint.test.js +476 -0
- package/flow-engine/tests/execute-dispatcher.test.js +738 -0
- package/flow-engine/tests/executors-complex.test.js +259 -0
- package/flow-engine/tests/graph.test.js +193 -0
- package/flow-engine/tests/skill-install.test.js +254 -0
- package/flow-engine/tests/validation.test.js +137 -0
- package/flows/development-flow.json +3 -3
- package/hooks/skill-discovery.sh +0 -0
- package/package.json +1 -1
|
@@ -180,6 +180,265 @@ describe('executeParallel', () => {
|
|
|
180
180
|
);
|
|
181
181
|
assert.equal(result3.next, 'interrupt_manual_fix');
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
it('handles errorPolicy quorum with threshold - quorum met', async () => {
|
|
185
|
+
const callbacks = {
|
|
186
|
+
onParallel: async ({ branches }) => {
|
|
187
|
+
// 3 out of 4 branches succeed (75% success rate)
|
|
188
|
+
return [
|
|
189
|
+
{ id: 'a', result: 'success' },
|
|
190
|
+
{ id: 'b', result: 'success' },
|
|
191
|
+
{ id: 'c', result: 'success' },
|
|
192
|
+
{ id: 'd', error: 'failed', skipped: false }
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const ctx = makeContext();
|
|
198
|
+
const result = await executeParallel(
|
|
199
|
+
{
|
|
200
|
+
id: 'parallel_task',
|
|
201
|
+
type: 'parallel',
|
|
202
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }],
|
|
203
|
+
errorPolicy: 'quorum',
|
|
204
|
+
quorumThreshold: 0.5, // 50% threshold
|
|
205
|
+
next: 'success_node',
|
|
206
|
+
onError: 'error_node'
|
|
207
|
+
},
|
|
208
|
+
ctx,
|
|
209
|
+
callbacks
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert.equal(result.next, 'success_node');
|
|
213
|
+
assert.equal(result.outputs.quorumMet, true);
|
|
214
|
+
assert.equal(result.outputs.successCount, 3);
|
|
215
|
+
assert.equal(result.outputs.totalCount, 4);
|
|
216
|
+
assert.equal(result.outputs.failureCount, 1);
|
|
217
|
+
assert.equal(result.outputs.successRate, 0.75);
|
|
218
|
+
assert.equal(result.outputs.quorumType, 'threshold');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('handles errorPolicy quorum with threshold - quorum failed', async () => {
|
|
222
|
+
const callbacks = {
|
|
223
|
+
onParallel: async ({ branches }) => {
|
|
224
|
+
// Only 1 out of 4 branches succeed (25% success rate)
|
|
225
|
+
return [
|
|
226
|
+
{ id: 'a', result: 'success' },
|
|
227
|
+
{ id: 'b', error: 'failed' },
|
|
228
|
+
{ id: 'c', error: 'failed' },
|
|
229
|
+
{ id: 'd', error: 'failed' }
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const ctx = makeContext();
|
|
235
|
+
const result = await executeParallel(
|
|
236
|
+
{
|
|
237
|
+
id: 'parallel_task',
|
|
238
|
+
type: 'parallel',
|
|
239
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }],
|
|
240
|
+
errorPolicy: 'quorum',
|
|
241
|
+
quorumThreshold: 0.5, // 50% threshold
|
|
242
|
+
next: 'success_node',
|
|
243
|
+
onError: 'error_node'
|
|
244
|
+
},
|
|
245
|
+
ctx,
|
|
246
|
+
callbacks
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
assert.equal(result.next, 'error_node');
|
|
250
|
+
assert.equal(result.outputs.quorumMet, false);
|
|
251
|
+
assert.equal(result.outputs.successCount, 1);
|
|
252
|
+
assert.equal(result.outputs.totalCount, 4);
|
|
253
|
+
assert.equal(result.outputs.failureCount, 3);
|
|
254
|
+
assert.equal(result.outputs.successRate, 0.25);
|
|
255
|
+
assert.equal(result.outputs.quorumType, 'threshold');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('handles errorPolicy quorum with minSuccess - quorum met', async () => {
|
|
259
|
+
const callbacks = {
|
|
260
|
+
onParallel: async ({ branches }) => {
|
|
261
|
+
// 2 out of 3 branches succeed
|
|
262
|
+
return [
|
|
263
|
+
{ id: 'a', result: 'success' },
|
|
264
|
+
{ id: 'b', result: 'success' },
|
|
265
|
+
{ id: 'c', error: 'failed' }
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const ctx = makeContext();
|
|
271
|
+
const result = await executeParallel(
|
|
272
|
+
{
|
|
273
|
+
id: 'parallel_task',
|
|
274
|
+
type: 'parallel',
|
|
275
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
276
|
+
errorPolicy: 'quorum',
|
|
277
|
+
quorumMinSuccess: 2, // At least 2 must succeed
|
|
278
|
+
next: 'success_node',
|
|
279
|
+
onError: 'error_node'
|
|
280
|
+
},
|
|
281
|
+
ctx,
|
|
282
|
+
callbacks
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert.equal(result.next, 'success_node');
|
|
286
|
+
assert.equal(result.outputs.quorumMet, true);
|
|
287
|
+
assert.equal(result.outputs.successCount, 2);
|
|
288
|
+
assert.equal(result.outputs.quorumType, 'minSuccess');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('handles errorPolicy quorum with minSuccess - quorum failed', async () => {
|
|
292
|
+
const callbacks = {
|
|
293
|
+
onParallel: async ({ branches }) => {
|
|
294
|
+
// Only 1 out of 3 branches succeed
|
|
295
|
+
return [
|
|
296
|
+
{ id: 'a', result: 'success' },
|
|
297
|
+
{ id: 'b', error: 'failed' },
|
|
298
|
+
{ id: 'c', error: 'failed' }
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const ctx = makeContext();
|
|
304
|
+
const result = await executeParallel(
|
|
305
|
+
{
|
|
306
|
+
id: 'parallel_task',
|
|
307
|
+
type: 'parallel',
|
|
308
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
309
|
+
errorPolicy: 'quorum',
|
|
310
|
+
quorumMinSuccess: 2, // At least 2 must succeed
|
|
311
|
+
next: 'success_node',
|
|
312
|
+
onError: 'error_node'
|
|
313
|
+
},
|
|
314
|
+
ctx,
|
|
315
|
+
callbacks
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
assert.equal(result.next, 'error_node');
|
|
319
|
+
assert.equal(result.outputs.quorumMet, false);
|
|
320
|
+
assert.equal(result.outputs.successCount, 1);
|
|
321
|
+
assert.equal(result.outputs.quorumType, 'minSuccess');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('handles errorPolicy quorum with default (all must succeed)', async () => {
|
|
325
|
+
const callbacks = {
|
|
326
|
+
onParallel: async ({ branches }) => {
|
|
327
|
+
// All branches succeed
|
|
328
|
+
return branches.map(b => ({ id: b.id, result: 'success' }));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const ctx = makeContext();
|
|
333
|
+
const result = await executeParallel(
|
|
334
|
+
{
|
|
335
|
+
id: 'parallel_task',
|
|
336
|
+
type: 'parallel',
|
|
337
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
338
|
+
errorPolicy: 'quorum', // No threshold or minSuccess specified
|
|
339
|
+
next: 'success_node',
|
|
340
|
+
onError: 'error_node'
|
|
341
|
+
},
|
|
342
|
+
ctx,
|
|
343
|
+
callbacks
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
assert.equal(result.next, 'success_node');
|
|
347
|
+
assert.equal(result.outputs.quorumMet, true);
|
|
348
|
+
assert.equal(result.outputs.quorumType, 'all');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('handles errorPolicy quorum default - one failure fails quorum', async () => {
|
|
352
|
+
const callbacks = {
|
|
353
|
+
onParallel: async ({ branches }) => {
|
|
354
|
+
// One branch fails
|
|
355
|
+
return [
|
|
356
|
+
{ id: 'a', result: 'success' },
|
|
357
|
+
{ id: 'b', result: 'success' },
|
|
358
|
+
{ id: 'c', error: 'failed' }
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const ctx = makeContext();
|
|
364
|
+
const result = await executeParallel(
|
|
365
|
+
{
|
|
366
|
+
id: 'parallel_task',
|
|
367
|
+
type: 'parallel',
|
|
368
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
369
|
+
errorPolicy: 'quorum', // Default: all must succeed
|
|
370
|
+
next: 'success_node',
|
|
371
|
+
onError: 'error_node'
|
|
372
|
+
},
|
|
373
|
+
ctx,
|
|
374
|
+
callbacks
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
assert.equal(result.next, 'error_node');
|
|
378
|
+
assert.equal(result.outputs.quorumMet, false);
|
|
379
|
+
assert.equal(result.outputs.successCount, 2);
|
|
380
|
+
assert.equal(result.outputs.totalCount, 3);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('handles errorPolicy quorum - skipped branches count as failures', async () => {
|
|
384
|
+
const callbacks = {
|
|
385
|
+
onParallel: async ({ branches }) => {
|
|
386
|
+
return [
|
|
387
|
+
{ id: 'a', result: 'success' },
|
|
388
|
+
{ id: 'b', skipped: true },
|
|
389
|
+
{ id: 'c', result: 'success' }
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const ctx = makeContext();
|
|
395
|
+
const result = await executeParallel(
|
|
396
|
+
{
|
|
397
|
+
id: 'parallel_task',
|
|
398
|
+
type: 'parallel',
|
|
399
|
+
branches: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
400
|
+
errorPolicy: 'quorum',
|
|
401
|
+
quorumThreshold: 0.7, // Need 70% success
|
|
402
|
+
next: 'success_node',
|
|
403
|
+
onError: 'error_node'
|
|
404
|
+
},
|
|
405
|
+
ctx,
|
|
406
|
+
callbacks
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// 2/3 = 66.7% < 70%, quorum fails
|
|
410
|
+
assert.equal(result.next, 'error_node');
|
|
411
|
+
assert.equal(result.outputs.quorumMet, false);
|
|
412
|
+
assert.equal(result.outputs.successCount, 2);
|
|
413
|
+
assert.equal(result.outputs.successRate, 2 / 3);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('handles errorPolicy quorum - falls back to next if no onError defined', async () => {
|
|
417
|
+
const callbacks = {
|
|
418
|
+
onParallel: async ({ branches }) => {
|
|
419
|
+
return branches.map(() => ({ error: 'all failed' }));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const ctx = makeContext();
|
|
424
|
+
const result = await executeParallel(
|
|
425
|
+
{
|
|
426
|
+
id: 'parallel_task',
|
|
427
|
+
type: 'parallel',
|
|
428
|
+
branches: [{ id: 'a' }, { id: 'b' }],
|
|
429
|
+
errorPolicy: 'quorum',
|
|
430
|
+
quorumThreshold: 0.5,
|
|
431
|
+
next: 'next_node'
|
|
432
|
+
// No onError defined
|
|
433
|
+
},
|
|
434
|
+
ctx,
|
|
435
|
+
callbacks
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// Should fall back to 'next' when no onError defined
|
|
439
|
+
assert.equal(result.next, 'next_node');
|
|
440
|
+
assert.equal(result.outputs.quorumMet, false);
|
|
441
|
+
});
|
|
183
442
|
});
|
|
184
443
|
|
|
185
444
|
describe('executeInterrupt', () => {
|
|
@@ -150,6 +150,181 @@ describe('getNextNodes', () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
describe('flow validation enhancements', () => {
|
|
154
|
+
it('detects orphaned nodes in simple case', () => {
|
|
155
|
+
const graph = buildGraph({
|
|
156
|
+
nodes: [
|
|
157
|
+
{ id: 'start', type: 'entry', next: 'end' },
|
|
158
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} },
|
|
159
|
+
{ id: 'orphan', type: 'sequence', next: 'end' }
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
const result = validateGraph(graph);
|
|
163
|
+
assert.equal(result.valid, false);
|
|
164
|
+
assert.ok(result.errors.some(e => e.includes('orphan') && e.includes('unreachable')));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('detects orphaned nodes in complex branching', () => {
|
|
168
|
+
const graph = buildGraph({
|
|
169
|
+
nodes: [
|
|
170
|
+
{ id: 'start', type: 'entry', next: 'cond' },
|
|
171
|
+
{ id: 'cond', type: 'condition', true: 'end', false: 'end' },
|
|
172
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} },
|
|
173
|
+
{ id: 'isolated_branch', type: 'sequence', next: 'isolated_end' },
|
|
174
|
+
{ id: 'isolated_end', type: 'exit', status: 'error', outputs: {} }
|
|
175
|
+
]
|
|
176
|
+
});
|
|
177
|
+
const result = validateGraph(graph);
|
|
178
|
+
assert.equal(result.valid, false);
|
|
179
|
+
assert.ok(result.errors.some(e => e.includes('isolated_branch') && e.includes('unreachable')));
|
|
180
|
+
assert.ok(result.errors.some(e => e.includes('isolated_end') && e.includes('unreachable')));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('validates onMaxIterations handler references', () => {
|
|
184
|
+
const graph = buildGraph({
|
|
185
|
+
nodes: [
|
|
186
|
+
{ id: 'start', type: 'entry', next: 'loop' },
|
|
187
|
+
{ id: 'loop', type: 'sequence', next: 'loop', onMaxIterations: 'missing_exit' },
|
|
188
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
const result = validateGraph(graph);
|
|
192
|
+
assert.equal(result.valid, false);
|
|
193
|
+
assert.ok(result.errors.some(e => e.includes('missing_exit')));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('validates onError handler references', () => {
|
|
197
|
+
const graph = buildGraph({
|
|
198
|
+
nodes: [
|
|
199
|
+
{ id: 'start', type: 'entry', next: 'task' },
|
|
200
|
+
{ id: 'task', type: 'sequence', next: 'end', onError: 'non_existent_handler' },
|
|
201
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
const result = validateGraph(graph);
|
|
205
|
+
assert.equal(result.valid, false);
|
|
206
|
+
assert.ok(result.errors.some(e => e.includes('non_existent_handler')));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('validates all router routes', () => {
|
|
210
|
+
const graph = buildGraph({
|
|
211
|
+
nodes: [
|
|
212
|
+
{ id: 'start', type: 'entry', next: 'router' },
|
|
213
|
+
{ id: 'router', type: 'router', routes: { path_a: 'valid', path_b: 'invalid_node', default: 'also_invalid' } },
|
|
214
|
+
{ id: 'valid', type: 'exit', status: 'success', outputs: {} }
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
const result = validateGraph(graph);
|
|
218
|
+
assert.equal(result.valid, false);
|
|
219
|
+
assert.ok(result.errors.some(e => e.includes('invalid_node')));
|
|
220
|
+
assert.ok(result.errors.some(e => e.includes('also_invalid')));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('detects multiple validation errors at once', () => {
|
|
224
|
+
const graph = buildGraph({
|
|
225
|
+
nodes: [
|
|
226
|
+
{ id: 'start', type: 'entry', next: 'missing_next' },
|
|
227
|
+
{ id: 'orphan', type: 'sequence', next: 'another_orphan' },
|
|
228
|
+
{ id: 'another_orphan', type: 'exit', status: 'success', outputs: {} }
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
const result = validateGraph(graph);
|
|
232
|
+
assert.equal(result.valid, false);
|
|
233
|
+
// Should have dangling reference error (missing_next)
|
|
234
|
+
assert.ok(result.errors.some(e => e.includes('missing_next')));
|
|
235
|
+
// Should have orphaned node errors
|
|
236
|
+
assert.ok(result.errors.some(e => e.includes('orphan')));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('passes validation for graph with all error handlers', () => {
|
|
240
|
+
const graph = buildGraph({
|
|
241
|
+
nodes: [
|
|
242
|
+
{ id: 'start', type: 'entry', next: 'task' },
|
|
243
|
+
{ id: 'task', type: 'sequence', next: 'end', onError: 'error_handler', onMaxIterations: 'max_handler' },
|
|
244
|
+
{ id: 'error_handler', type: 'exit', status: 'error', outputs: {} },
|
|
245
|
+
{ id: 'max_handler', type: 'exit', status: 'max_iterations', outputs: {} },
|
|
246
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
247
|
+
]
|
|
248
|
+
});
|
|
249
|
+
const result = validateGraph(graph);
|
|
250
|
+
assert.equal(result.valid, true);
|
|
251
|
+
assert.equal(result.errors.length, 0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('passes validation for complex branching with no orphans', () => {
|
|
255
|
+
const graph = buildGraph({
|
|
256
|
+
nodes: [
|
|
257
|
+
{ id: 'start', type: 'entry', next: 'cond' },
|
|
258
|
+
{ id: 'cond', type: 'condition', true: 'router', false: 'fallback' },
|
|
259
|
+
{ id: 'router', type: 'router', routes: { a: 'end', b: 'fallback' }, default: 'fallback' },
|
|
260
|
+
{ id: 'fallback', type: 'sequence', next: 'end' },
|
|
261
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
262
|
+
]
|
|
263
|
+
});
|
|
264
|
+
const result = validateGraph(graph);
|
|
265
|
+
assert.equal(result.valid, true);
|
|
266
|
+
assert.equal(result.errors.length, 0);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('detects orphaned node in circular subgraph', () => {
|
|
270
|
+
const graph = buildGraph({
|
|
271
|
+
nodes: [
|
|
272
|
+
{ id: 'start', type: 'entry', next: 'end' },
|
|
273
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} },
|
|
274
|
+
{ id: 'circular_a', type: 'sequence', next: 'circular_b' },
|
|
275
|
+
{ id: 'circular_b', type: 'sequence', next: 'circular_a' }
|
|
276
|
+
]
|
|
277
|
+
});
|
|
278
|
+
const result = validateGraph(graph);
|
|
279
|
+
assert.equal(result.valid, false);
|
|
280
|
+
assert.ok(result.errors.some(e => e.includes('circular_a') && e.includes('unreachable')));
|
|
281
|
+
assert.ok(result.errors.some(e => e.includes('circular_b') && e.includes('unreachable')));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('validates condition node with both branches pointing to same target', () => {
|
|
285
|
+
const graph = buildGraph({
|
|
286
|
+
nodes: [
|
|
287
|
+
{ id: 'start', type: 'entry', next: 'cond' },
|
|
288
|
+
{ id: 'cond', type: 'condition', true: 'end', false: 'end' },
|
|
289
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
const result = validateGraph(graph);
|
|
293
|
+
assert.equal(result.valid, true);
|
|
294
|
+
assert.equal(result.errors.length, 0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('validates router with duplicate route targets', () => {
|
|
298
|
+
const graph = buildGraph({
|
|
299
|
+
nodes: [
|
|
300
|
+
{ id: 'start', type: 'entry', next: 'router' },
|
|
301
|
+
{ id: 'router', type: 'router', routes: { a: 'end', b: 'end', c: 'end' }, default: 'end' },
|
|
302
|
+
{ id: 'end', type: 'exit', status: 'success', outputs: {} }
|
|
303
|
+
]
|
|
304
|
+
});
|
|
305
|
+
const result = validateGraph(graph);
|
|
306
|
+
assert.equal(result.valid, true);
|
|
307
|
+
assert.equal(result.errors.length, 0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('provides clear error messages for all error types', () => {
|
|
311
|
+
const graph = buildGraph({
|
|
312
|
+
nodes: [
|
|
313
|
+
{ id: 'start', type: 'entry', next: 'task' },
|
|
314
|
+
{ id: 'task', type: 'sequence', next: 'missing', onError: 'no_such_handler' },
|
|
315
|
+
{ id: 'orphan', type: 'exit', status: 'error', outputs: {} }
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
const result = validateGraph(graph);
|
|
319
|
+
assert.equal(result.valid, false);
|
|
320
|
+
// Check error message format includes node IDs and issue type
|
|
321
|
+
result.errors.forEach(err => {
|
|
322
|
+
assert.ok(typeof err === 'string');
|
|
323
|
+
assert.ok(err.length > 0);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
153
328
|
describe('real flow loading', () => {
|
|
154
329
|
it('loads the full development-flow.json', () => {
|
|
155
330
|
const flow = require('../../flows/development-flow.json');
|
|
@@ -158,4 +333,22 @@ describe('real flow loading', () => {
|
|
|
158
333
|
assert.equal(graph.entryNode, 'start');
|
|
159
334
|
assert.equal(graph.meta.version, '5.3.0');
|
|
160
335
|
});
|
|
336
|
+
|
|
337
|
+
it('validates the full development-flow.json and detects known issues', () => {
|
|
338
|
+
const flow = require('../../flows/development-flow.json');
|
|
339
|
+
const graph = buildGraph(flow);
|
|
340
|
+
const result = validateGraph(graph);
|
|
341
|
+
// Development flow has known orphaned nodes (P0.3 - teams mode deprecated nodes)
|
|
342
|
+
// Expected orphaned nodes: teams_spawn, interrupt_teams_failed, design_ui, interrupt_design_complete
|
|
343
|
+
if (!result.valid) {
|
|
344
|
+
const orphanedNodes = result.errors.filter(e => e.includes('unreachable'));
|
|
345
|
+
assert.ok(orphanedNodes.length > 0, 'Should detect orphaned nodes');
|
|
346
|
+
// Verify these are the known teams mode orphaned nodes
|
|
347
|
+
const hasTeamsNodes = orphanedNodes.some(e =>
|
|
348
|
+
e.includes('teams_spawn') || e.includes('interrupt_teams') ||
|
|
349
|
+
e.includes('design_ui') || e.includes('interrupt_design')
|
|
350
|
+
);
|
|
351
|
+
assert.ok(hasTeamsNodes, 'Orphaned nodes should include known teams mode nodes');
|
|
352
|
+
}
|
|
353
|
+
});
|
|
161
354
|
});
|