test(stammbaum): cyclic input fails closed — finite layout, one position per node (#724)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m23s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m37s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s

An A<->B parent cycle and a founder reaching a re-entrant 3-cycle both return a
finite layout (no frozen $derived) with every node placed exactly once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-04 13:53:44 +02:00
parent 445d2c6eaa
commit fad3aa0373

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();
});
});