#!/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); });