test(stammbaum): every unit centre sits within its child-units span (#724)

Fixture-wide loop over the canonical forest and a synthetic tree: each unit's
run centre is within [min, max] of its child-unit centres — the ancestor
centring invariant, asserted on real data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-04 13:35:54 +02:00
committed by marcel
parent 7627589844
commit a85b22efcf

View File

@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { buildLayout, NODE_W, NODE_H, COL_GAP, ROW_GAP } from './buildLayout'; import { buildLayout, NODE_W, NODE_H, COL_GAP, ROW_GAP } from './buildLayout';
import { buildFamilyForest, type Unit } from './familyForest';
import canonicalFixture from '../__fixtures__/stammbaum.json'; import canonicalFixture from '../__fixtures__/stammbaum.json';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
@@ -461,3 +462,43 @@ describe('buildLayout — named-bug guard: deep bloodline (#724)', () => {
expect(centerX(layout, d)).toBe(mid); expect(centerX(layout, d)).toBe(mid);
}); });
}); });
// Centre-x of a unit's run, derived from its primary's left edge + run width.
function unitCenter(layout: ReturnType<typeof buildLayout>, u: Unit): number {
const left = layout.positions.get(u.id)!.x;
const width = u.members.length * NODE_W + (u.members.length - 1) * COL_GAP;
return left + width / 2;
}
describe('buildLayout — ancestor centring invariant (#724)', () => {
const fixtureNodes = canonicalFixture.nodes as unknown as PersonNodeDTO[];
const fixtureEdges = canonicalFixture.edges as unknown as RelationshipDTO[];
it('every unit centre sits within its child units span (canonical + synthetic)', () => {
const cases: [string, PersonNodeDTO[], RelationshipDTO[]][] = [
['canonical', fixtureNodes, fixtureEdges],
[
'synthetic',
[node('R', 'R', 0), node('c1', 'c1', 1), node('c2', 'c2', 1), node('c3', 'c3', 1)],
[parentEdge('R', 'c1'), parentEdge('R', 'c2'), parentEdge('R', 'c3')]
]
];
for (const [label, nodes, edges] of cases) {
const layout = buildLayout(nodes, edges);
const forest = buildFamilyForest(nodes, edges);
const walk = (u: Unit) => {
if (u.children.length > 0) {
const childCenters = u.children.map((c) => unitCenter(layout, c));
const lo = Math.min(...childCenters);
const hi = Math.max(...childCenters);
const c = unitCenter(layout, u);
expect(c, `${label}: unit ${u.id} centred in child span`).toBeGreaterThanOrEqual(lo);
expect(c, `${label}: unit ${u.id} centred in child span`).toBeLessThanOrEqual(hi);
}
u.children.forEach(walk);
};
forest.roots.forEach(walk);
}
});
});