Local-only developer utility that authenticates against the running backend, captures the current /api/network snapshot, and writes it to src/lib/person/genealogy/__fixtures__/stammbaum.json. Sanity gates exit non-zero on a vacuous capture (< 50 nodes, < 5 generations, 0 SPOUSE_OF edges). Fixture and script land together so the fixture is reproducible from the script that generated it. Captured snapshot: 62 nodes, 43 edges, 28 SPOUSE_OF (0 with fromYear), generations G0-G4. Albert de Gruyter is the canonical multi-spouse case with 4 marriages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
// Local-only. Never invoked from CI. Re-run intentionally; commit the
|
|
// resulting JSON in one atomic commit.
|
|
//
|
|
// Captures the current /api/network response into the canonical fixture used
|
|
// by buildLayout.test.ts. Asserts a minimum shape so a silently-empty backend
|
|
// can't write a vacuous fixture.
|
|
|
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
import { dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8080';
|
|
const EMAIL = process.env.CAPTURE_EMAIL ?? 'admin@familyarchive.local';
|
|
const PASSWORD = process.env.CAPTURE_PASSWORD ?? 'admin123';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const FIXTURE_PATH = `${HERE}/../src/lib/person/genealogy/__fixtures__/stammbaum.json`;
|
|
|
|
// Sanity floors — calibrated against the canonical dataset (May 2026: 62
|
|
// nodes, 5 generations, 28 SPOUSE_OF edges). The point is catching a silently
|
|
// empty backend, not strict size validation; raise these only if the
|
|
// canonical graph grows substantially.
|
|
const MIN_NODES = 50;
|
|
const MIN_GENERATIONS = 5;
|
|
const MIN_SPOUSE_OF_EDGES = 1;
|
|
|
|
function parseSetCookies(headers) {
|
|
const out = new Map();
|
|
const raw = headers.getSetCookie?.() ?? [];
|
|
for (const line of raw) {
|
|
const [pair] = line.split(';');
|
|
const eq = pair.indexOf('=');
|
|
if (eq < 0) continue;
|
|
out.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function serialiseCookies(jar) {
|
|
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
}
|
|
|
|
async function login(jar) {
|
|
const xsrf = randomUUID();
|
|
jar.set('XSRF-TOKEN', xsrf);
|
|
const res = await fetch(`${BACKEND_URL}/api/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-XSRF-TOKEN': xsrf,
|
|
Cookie: serialiseCookies(jar)
|
|
},
|
|
body: JSON.stringify({ email: EMAIL, password: PASSWORD })
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Login failed: ${res.status} ${await res.text()}`);
|
|
}
|
|
for (const [k, v] of parseSetCookies(res.headers)) jar.set(k, v);
|
|
}
|
|
|
|
async function fetchNetwork(jar) {
|
|
const res = await fetch(`${BACKEND_URL}/api/network`, {
|
|
headers: { Cookie: serialiseCookies(jar) }
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`GET /api/network failed: ${res.status} ${await res.text()}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
function validate(network) {
|
|
const nodes = Array.isArray(network.nodes) ? network.nodes : [];
|
|
const edges = Array.isArray(network.edges) ? network.edges : [];
|
|
const spouseEdges = edges.filter((e) => e.relationType === 'SPOUSE_OF');
|
|
const generations = new Set(nodes.map((n) => n.generation).filter((g) => g != null));
|
|
|
|
const failures = [];
|
|
if (nodes.length < MIN_NODES) {
|
|
failures.push(`expected >= ${MIN_NODES} nodes, got ${nodes.length}`);
|
|
}
|
|
if (generations.size < MIN_GENERATIONS) {
|
|
failures.push(`expected >= ${MIN_GENERATIONS} distinct generations, got ${generations.size}`);
|
|
}
|
|
if (spouseEdges.length < MIN_SPOUSE_OF_EDGES) {
|
|
failures.push(`expected >= ${MIN_SPOUSE_OF_EDGES} SPOUSE_OF edges, got ${spouseEdges.length}`);
|
|
}
|
|
if (failures.length > 0) {
|
|
throw new Error(`Sanity gates failed:\n - ${failures.join('\n - ')}`);
|
|
}
|
|
|
|
return {
|
|
nodes: nodes.length,
|
|
edges: edges.length,
|
|
spouseEdges: spouseEdges.length,
|
|
generations: [...generations].sort((a, b) => a - b)
|
|
};
|
|
}
|
|
|
|
function writeFixture(network) {
|
|
mkdirSync(dirname(FIXTURE_PATH), { recursive: true });
|
|
writeFileSync(FIXTURE_PATH, JSON.stringify(network, null, '\t') + '\n', 'utf8');
|
|
}
|
|
|
|
async function main() {
|
|
const jar = new Map();
|
|
console.error(`Capturing /api/network from ${BACKEND_URL} as ${EMAIL} ...`);
|
|
await login(jar);
|
|
const network = await fetchNetwork(jar);
|
|
const stats = validate(network);
|
|
writeFixture(network);
|
|
console.error(
|
|
`Wrote ${FIXTURE_PATH}\n nodes: ${stats.nodes}\n edges: ${stats.edges}\n SPOUSE_OF edges: ${stats.spouseEdges}\n generations: G${stats.generations.join(', G')}`
|
|
);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err.message);
|
|
process.exit(1);
|
|
});
|