test(stammbaum): layout is deterministic under input reordering (#724)

Seeded Fisher-Yates permutation of nodes and edges yields byte-identical
positions — confirms every comparator ends in a stable id and nothing relies on
Map iteration order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-04 13:52:01 +02:00
committed by marcel
parent d3bb08e7ff
commit 456e019c3d

View File

@@ -614,3 +614,40 @@ describe('buildLayout — ancestor centring invariant (#724)', () => {
expect(widest).toBeLessThanOrEqual(OLD_FULL_CANVAS / 4);
});
});
// Deterministic FisherYates using a seeded mulberry32 PRNG (never Math.random,
// which would make the determinism test itself non-reproducible).
function seededShuffle<T>(input: T[], seed: number): T[] {
let s = seed >>> 0;
const rand = () => {
s = (s + 0x6d2b79f5) >>> 0;
let t = s;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const out = input.slice();
for (let i = out.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
describe('buildLayout — determinism (#724)', () => {
const fixtureNodes = canonicalFixture.nodes as unknown as PersonNodeDTO[];
const fixtureEdges = canonicalFixture.edges as unknown as RelationshipDTO[];
it('produces identical positions regardless of node/edge input order', () => {
const base = buildLayout(fixtureNodes, fixtureEdges);
const shuffled = buildLayout(
seededShuffle(fixtureNodes, 1337),
seededShuffle(fixtureEdges, 4242)
);
expect(shuffled.positions.size).toBe(base.positions.size);
for (const [id, p] of base.positions) {
expect(shuffled.positions.get(id), `position for ${id} is order-independent`).toEqual(p);
}
});
});