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
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user