feat(stammbaum): bloodline-contiguous tidy-tree layout (replace per-generation packer) (#724) #725

Merged
marcel merged 24 commits from feat/issue-724-tidy-tree-layout into main 2026-06-04 14:55:12 +02:00
Showing only changes of commit fad3aa0373 - Show all commits

View File

@@ -651,3 +651,49 @@ describe('buildLayout — determinism (#724)', () => {
}
});
});
describe('buildLayout — termination + once-only on cyclic input (#724)', () => {
// buildLayout runs inside a $derived, so non-termination = a frozen tab.
// User-entered data can form parent cycles; the layout must fail closed —
// produce *a* finite layout, every node placed exactly once.
const A = '00000000-0000-0000-0000-0000000000a1';
const B = '00000000-0000-0000-0000-0000000000a2';
const C = '00000000-0000-0000-0000-0000000000a3';
it('A↔B parent cycle returns a finite layout with one position per node', () => {
let layout!: ReturnType<typeof buildLayout>;
expect(() => {
layout = buildLayout(
[node(A, 'A'), node(B, 'B')],
[parentEdge(A, B, 'ab'), parentEdge(B, A, 'ba')]
);
}).not.toThrow();
expect(layout.positions.size).toBe(2);
expect(layout.positions.get(A)).toBeDefined();
expect(layout.positions.get(B)).toBeDefined();
});
it('two founders both reaching a re-entrant 3-cycle terminate with every node placed', () => {
// F is a founder parenting A; A→B→C→A is a parent cycle, and C also
// re-enters via F. The visited/once-only guarantees keep it finite.
const F = '00000000-0000-0000-0000-0000000000a0';
let layout!: ReturnType<typeof buildLayout>;
expect(() => {
layout = buildLayout(
[node(F, 'F'), node(A, 'A'), node(B, 'B'), node(C, 'C')],
[
parentEdge(F, A, 'fa'),
parentEdge(A, B, 'ab'),
parentEdge(B, C, 'bc'),
parentEdge(C, A, 'ca')
]
);
}).not.toThrow();
// Every node is placed exactly once (Map keys are unique by construction;
// the count equality proves none were dropped or duplicated).
expect(layout.positions.size).toBe(4);
for (const id of [F, A, B, C]) expect(layout.positions.get(id)).toBeDefined();
});
});