CI / build + unit + e2e (pull_request) Successful in 2m34s
#41: re-partition the RU-ARK mainland's 28-part lat>=68 Arctic-coast band into two new regions via a new turf-free, idempotent pipeline stage scripts/split-arkhangel-band.mjs (npm run split:regions:band), mirroring the #31 split: - RU-PEO (Pechora, 13 parts, ~37,678 km2) -> key pechora-qwhs - RU-YAM (Yamal, 15 parts, ~51,722 km2) -> key yamal-gosw - RU-ARK keeps its id/name/key arkhangel-sk-lhbk (ownership-safe); the 5-part lat<68 core (~305,229 km2) stays RU-ARK. Pure re-partition: no geometry moved (byte-identical union). The two new features are inserted immediately after RU-ARK-SZ. Manifests regenerated with the real scripts (1,798 -> 1,800 keys + adjacency; adjacency pairs 4,471 -> 4,473). Fixed-point dataset unchanged except feature count (11,794 parts / 11,795 rings / 164,801 verts / 1 KZ-KZY hole). Four dataset/manifest tests updated (RUS 82 -> 84 sub-regions). Version 0.25.0 -> 0.26.0. Fixes #41
116 lines
5.3 KiB
TypeScript
116 lines
5.3 KiB
TypeScript
/**
|
|
* 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<string, unknown> {
|
|
return JSON.parse(readFileSync(path.join(root, rel), 'utf8'));
|
|
}
|
|
|
|
describe('region-adjacency.json', () => {
|
|
const adjacency = readJson('public/region-adjacency.json') as Record<string, string[]>;
|
|
const manifest = readJson('public/region-keys.json') as Record<string, string>;
|
|
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(1800);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
it("the #31 + #41 Arctic splits: New Earth isolated; Severnaya Zemlya, Pechora and Yamal adjacency", () => {
|
|
// v0.25 (#31) split RU-ARK into the mainland (arkhangel-sk-lhbk), the
|
|
// Franz Josef Land islets (new-earth-eikt) and the Severnaya Zemlya
|
|
// islets (severnaya-zemlya-awqp). New Earth sits ~100+ km from any land
|
|
// → a genuinely isolated island (empty list).
|
|
// v0.26 (#41) repartitioned the mainland: the 5 lat < 68 parts stay
|
|
// arkhangel-sk-lhbk, and the 28 lat ≥ 68 band parts became pechora-qwhs
|
|
// (the SW cell) and yamal-gosw (the NE cell). The Severnaya Zemlya
|
|
// islets' digitized rings touch the NE band part (now yamal-gosw), not
|
|
// the trimmed core, so SZ now borders Yamal (its former mainland contact
|
|
// moved to Yamal). The trimmed core keeps its real mainland neighbours
|
|
// but loses the SZ contact. Pechora (SW) borders the existing Nenets
|
|
// region to the south and Yamal to the north.
|
|
expect(adjacency['new-earth-eikt']).toEqual([]);
|
|
expect(adjacency['severnaya-zemlya-awqp']).toEqual(['yamal-gosw']);
|
|
expect(adjacency['pechora-qwhs']).toEqual(['nenets-qyga', 'yamal-gosw']);
|
|
expect(adjacency['yamal-gosw']).toEqual(['pechora-qwhs', 'severnaya-zemlya-awqp']);
|
|
// the trimmed core keeps its real neighbours (the band's SZ contact moved to Yamal)
|
|
expect(adjacency['arkhangel-sk-lhbk']).toEqual([
|
|
'karelia-ohem',
|
|
'kirov-gfvm',
|
|
'komi-eoeh',
|
|
'murmansk-etas',
|
|
'nenets-qyga',
|
|
'vologda-ynvr',
|
|
]);
|
|
});
|
|
});
|