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.
@@ -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
  });