|
| 1 | +import { describe, expect, test } from 'vitest' |
| 2 | +import { computeLayeredLayout, TRIGGER_LAYOUT_ID } from './layeredLayout' |
| 3 | +import type { FlowNode } from '../types/soar.types' |
| 4 | + |
| 5 | +const exec = (patch: Partial<FlowNode> = {}): FlowNode => ({ kind: 'executor', executor: 'noop', ...patch }) |
| 6 | + |
| 7 | +describe('computeLayeredLayout', () => { |
| 8 | + test('trigger is layer 0 and sits above roots', () => { |
| 9 | + const pos = computeLayeredLayout(['a'], { a: exec() }) |
| 10 | + expect(pos[TRIGGER_LAYOUT_ID].y).toBeLessThan(pos.a.y) |
| 11 | + }) |
| 12 | + |
| 13 | + test('each child sits one layer below its parent', () => { |
| 14 | + const pos = computeLayeredLayout(['a'], { |
| 15 | + a: exec({ onSuccess: ['b'] }), |
| 16 | + b: exec({ onSuccess: ['c'] }), |
| 17 | + c: exec(), |
| 18 | + }) |
| 19 | + expect(pos.b.y - pos.a.y).toBe(pos.c.y - pos.b.y) |
| 20 | + expect(pos.a.y).toBeLessThan(pos.b.y) |
| 21 | + expect(pos.b.y).toBeLessThan(pos.c.y) |
| 22 | + }) |
| 23 | + |
| 24 | + test('multi-parent join settles at deepest ancestor + 1, not at layer 1', () => { |
| 25 | + // trigger -> a -> b -> d, trigger -> c -> d. d must be below b (deepest). |
| 26 | + const pos = computeLayeredLayout(['a', 'c'], { |
| 27 | + a: exec({ onSuccess: ['b'] }), |
| 28 | + b: exec({ onSuccess: ['d'] }), |
| 29 | + c: exec({ onSuccess: ['d'] }), |
| 30 | + d: exec(), |
| 31 | + }) |
| 32 | + expect(pos.d.y).toBeGreaterThan(pos.b.y) |
| 33 | + expect(pos.d.y).toBeGreaterThan(pos.c.y) |
| 34 | + }) |
| 35 | + |
| 36 | + test('siblings in the same layer share y and separate on x', () => { |
| 37 | + const pos = computeLayeredLayout(['a', 'b', 'c'], { a: exec(), b: exec(), c: exec() }) |
| 38 | + expect(pos.a.y).toBe(pos.b.y) |
| 39 | + expect(pos.b.y).toBe(pos.c.y) |
| 40 | + expect(pos.a.x).not.toBe(pos.b.x) |
| 41 | + expect(pos.b.x).not.toBe(pos.c.x) |
| 42 | + }) |
| 43 | + |
| 44 | + test('orphan nodes land in a trailing layer, not on top of the trigger', () => { |
| 45 | + const pos = computeLayeredLayout(['a'], { a: exec(), orphan: exec() }) |
| 46 | + expect(pos.orphan.y).toBeGreaterThan(pos.a.y) |
| 47 | + }) |
| 48 | + |
| 49 | + test('cycles do not hang the layout', () => { |
| 50 | + const pos = computeLayeredLayout(['a'], { |
| 51 | + a: exec({ onSuccess: ['b'] }), |
| 52 | + b: exec({ onSuccess: ['a'] }), |
| 53 | + }) |
| 54 | + expect(pos.a).toBeDefined() |
| 55 | + expect(pos.b).toBeDefined() |
| 56 | + }) |
| 57 | +}) |
0 commit comments