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:
@@ -614,3 +614,40 @@ describe('buildLayout — ancestor centring invariant (#724)', () => {
|
|||||||
expect(widest).toBeLessThanOrEqual(OLD_FULL_CANVAS / 4);
|
expect(widest).toBeLessThanOrEqual(OLD_FULL_CANVAS / 4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Deterministic Fisher–Yates 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user