import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'; import { fileURLToPath } from 'node:url'; import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { allTerritoryKeys, expandCountriesToKeys, hasRegionData, loadRegions, referenceForKeys, regionName, resetRegions, validKeys, } from './regions'; import type { GameState } from '../shared/types'; // The repo root: this file lives in server/, so '..' is the directory holding // public/region-keys.json and public/regions.geo.json. cwd-independent (mirrors // how server/index.ts locates its root via import.meta.url). const root = fileURLToPath(new URL('..', import.meta.url)); describe('regions (region-key loader, v0.19 #16 P1)', () => { beforeEach(() => { expect(loadRegions(root)).toBe(true); }); it('loads the full manifest: 1,796 unique valid region keys', () => { expect(hasRegionData()).toBe(true); expect(validKeys().size).toBe(1796); }); it('expands an ISO_A3 country to the region keys of all its sub-regions', () => { const fra = expandCountriesToKeys(['FRA']); // FRA has 42 sub-regions in the committed dataset. expect(fra).toHaveLength(42); // every expanded key is a real region key for (const k of fra) expect(validKeys().has(k)).toBe(true); // and the reference table labels each one with its parent country const ref = referenceForKeys(fra); expect(Object.keys(ref)).toHaveLength(42); for (const [k, label] of Object.entries(ref)) { expect(label.endsWith('(FRA)')).toBe(true); expect(regionName(k)).toBeTruthy(); } // sample known FRA keys, and their exact reference strings expect(fra).toContain('ain-lqtt'); expect(ref['ain-lqtt']).toBe('Ain (FRA)'); expect(ref['aisne-gstu']).toBe('Aisne (FRA)'); }); it('referenceForKeys labels only known keys and omits unknown ones', () => { const ref = referenceForKeys(['ain-lqtt', 'not-a-real-key-xyz']); expect(ref['ain-lqtt']).toBe('Ain (FRA)'); expect(ref['not-a-real-key-xyz']).toBeUndefined(); }); it('allTerritoryKeys unions every nation\'s territory keys (no overlap)', () => { const state: GameState = { id: 'g', presetId: 'p', presetTitle: 'P', playerNationId: 'uk', turn: 1, date: '1939-09-01', nations: { uk: { id: 'uk', name: 'UK', type: 'major', stats: { military: 80, economy: 70, stability: 60, population: 50 }, territory: expandCountriesToKeys(['GBR']), relations: {}, }, fr: { id: 'fr', name: 'FR', type: 'major', stats: { military: 80, economy: 70, stability: 60, population: 50 }, territory: expandCountriesToKeys(['FRA']), relations: {}, }, }, events: [], pending: [], version: 1, }; // 16 GBR sub-regions + 42 FRA sub-regions, no overlap between the two. expect(allTerritoryKeys(state).size).toBe(58); }); describe('graceful degradation with no region data', () => { beforeEach(() => resetRegions()); it('expands to the raw input, omits references, and reports no data', () => { expect(hasRegionData()).toBe(false); // no manifest -> the country list passes through unchanged (old model) expect(expandCountriesToKeys(['FRA', 'GBR'])).toEqual(['FRA', 'GBR']); expect(referenceForKeys(['FRA'])).toEqual({}); expect(validKeys().size).toBe(0); expect(regionName('FRA')).toBeUndefined(); }); }); afterAll(() => resetRegions()); }); describe('loadRegions resolves the manifest per environment (dev public/ vs prod dist/)', () => { const realRoot = root; // A root that mimics the prod Docker image: no public/, the region files live // in dist/ (where Vite copies public/* at build). This is the exact layout that // broke v0.19.0 in prod (the loader only looked in public/). const base = mkdtempSync(path.join(tmpdir(), 'history-regions-')); const prodRoot = path.join(base, 'prod'); beforeEach(() => { mkdirSync(path.join(prodRoot, 'dist'), { recursive: true }); copyFileSync( path.join(realRoot, 'public', 'region-keys.json'), path.join(prodRoot, 'dist', 'region-keys.json'), ); copyFileSync( path.join(realRoot, 'public', 'regions.geo.json'), path.join(prodRoot, 'dist', 'regions.geo.json'), ); }); afterEach(() => resetRegions()); afterAll(() => { rmSync(base, { recursive: true, force: true }); }); it('loads from dist/ when public/ is absent (the prod container layout)', () => { expect(loadRegions(prodRoot)).toBe(true); expect(hasRegionData()).toBe(true); expect(validKeys().size).toBe(1796); expect(expandCountriesToKeys(['FRA'])).toHaveLength(42); }); it('still loads from public/ (the dev layout, the first candidate)', () => { expect(loadRegions(realRoot)).toBe(true); expect(validKeys().size).toBe(1796); }); it('returns false and no data when neither public/ nor dist/ holds the files', () => { const emptyRoot = path.join(base, 'empty'); mkdirSync(emptyRoot, { recursive: true }); expect(loadRegions(emptyRoot)).toBe(false); expect(hasRegionData()).toBe(false); expect(validKeys().size).toBe(0); }); });