/** * scripts/region-adjacency.test.ts — validation for the committed * `public/region-adjacency.json` (built by `scripts/build-region-adjacency.mjs`). * * The manifest maps each region key to the list of region keys it **borders** * (shares an edge/point with). It is the data behind the model's `getNeighbours` * tool (v0.21, #16 P2), so it is a load-bearing data artifact — this test pins * its contract. * * Deliberately dependency-free: it reads the JSON with plain `fs`/`JSON.parse` * and never imports `@turf`/geojson types (importing one pulls `@types/geojson` * into the tsc program and breaks `WorldMap.tsx`; see the v0.17 changelog note). * Mirrors the style of `scripts/region-keys.test.ts`. */ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const here = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(here, '..'); function readJson(rel: string): Record { return JSON.parse(readFileSync(path.join(root, rel), 'utf8')); } describe('region-adjacency.json', () => { const adjacency = readJson('public/region-adjacency.json') as Record; const manifest = readJson('public/region-keys.json') as Record; const keys = new Set(Object.values(manifest)); it('has exactly one entry per region key (same key set as region-keys.json)', () => { const manifestKeys = new Set(Object.values(manifest)); const adjacencyKeys = new Set(Object.keys(adjacency)); expect(adjacencyKeys).toEqual(manifestKeys); expect(Object.keys(adjacency)).toHaveLength(keys.size); expect(adjacencyKeys.size).toBe(1796); }); it('every neighbour is itself a valid region key (referential integrity)', () => { for (const [key, neighbours] of Object.entries(adjacency)) { for (const n of neighbours) { expect(keys.has(n), `neighbour ${n} of ${key} is not a valid region key`).toBe(true); } } }); it('no region lists itself as a neighbour', () => { for (const [key, neighbours] of Object.entries(adjacency)) { expect(neighbours.includes(key), `${key} lists itself`).toBe(false); } }); it('adjacency is symmetric (A ∈ B ⟺ B ∈ A)', () => { let checked = 0; for (const [a, aList] of Object.entries(adjacency)) { const aSet = new Set(aList); for (const b of aList) { const bList = adjacency[b]; expect(Array.isArray(bList), `${b} has no adjacency entry`).toBe(true); // A lists B, so B must list A. expect(bList!.includes(a), `${a} → ${b} but ${b} does not list ${a}`).toBe(true); checked++; } // And the converse: every neighbour B of A must be a key (implied above) — // also assert no *missing* reverse entry by checking the full set equality. const bSet = new Set(aSet); for (const b of aList) bSet.add(b); // Symmetry means a's set equals the set of {b : a ∈ b}. const reverse = Object.entries(adjacency) .filter(([, list]) => list.includes(a)) .map(([k]) => k); expect(new Set(reverse).size, `asymmetric degree for ${a}`).toBe(aList.length); expect(new Set(aList).size).toBe(aSet.size); // no duplicate neighbours } expect(checked).toBeGreaterThan(0); }); it('spot-checks known real bordering pairs', () => { // Brazil ↔ Peru (Acre borders Loreto) — a real, unambiguous land border. expect(adjacency['acre-ekzw']?.includes('loreto-ipkx')).toBe(true); expect(adjacency['loreto-ipkx']?.includes('acre-ekzw')).toBe(true); // France ↔ Germany (Moselle borders Baden-Württemberg) — another real border. expect(adjacency['moselle-rygy']?.includes('baden-wurttemberg-oyje')).toBe(true); expect(adjacency['baden-wurttemberg-oyje']?.includes('moselle-rygy')).toBe(true); }); });