Files
History/server/map.test.ts
T
kamilkrzywda 1c64aa1da4 Tests: region-key contract (map rewrite + regions loader + engine) (v0.19 P1 of #16)
- map.test.ts: rewrite the fixture for region keys; the #6 regression guard now
  proves the owner resolves via properties.regionKey while the repacked numeric
  feature.id and the sub-region regionId do not.
- regions.test.ts (new): the loader sees 1,796 valid keys; FRA expands to its
  42 sub-region keys (all valid, all labelled '… (FRA)'); allTerritoryKeys
  unions correctly; and with no data it degrades to the raw input.
- engine.test.ts: buildUserPrompt includes a regions table only when provided;
  applyTurnResult with a validKeys set applies real keys and ignores bogus ones,
  and without one applies all (the legacy/unit path).
2026-10-14 09:14:24 +00:00

146 lines
5.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildRegionMap } from './map';
import { mapFeatureOwner, nationColor, regionOwner, type RegionMap } from '../shared/protocol';
import type { GameState } from '../shared/types';
// Region-key fixtures (v0.19, #16 P1): ownership and the RegionMap are now keyed
// by region key (one per owned sub-region), not by a parent-country ISO_A3. The
// keys below are deliberately fake (obviously not real manifest entries) so this
// test stays self-contained and format-agnostic.
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: ['albania-aaaa', 'wales-bbbb'],
relations: {},
},
de: {
id: 'de',
name: 'DE',
type: 'major',
stats: { military: 90, economy: 70, stability: 70, population: 90 },
territory: ['brandenburg-cccc'],
relations: {},
},
},
events: [],
pending: [],
version: 1,
};
// The RegionMap built from the fixture: the authoritative region key → owner source.
const regions: RegionMap = buildRegionMap(state);
describe('buildRegionMap', () => {
it('maps each owned region key to its owner + that nation\'s stable color', () => {
const m = buildRegionMap(state);
expect(m['albania-aaaa']).toEqual({ color: nationColor('uk'), owner: 'UK', ownerId: 'uk' });
expect(m['wales-bbbb']).toEqual({ color: nationColor('uk'), owner: 'UK', ownerId: 'uk' });
expect(m['brandenburg-cccc']).toEqual({ color: nationColor('de'), owner: 'DE', ownerId: 'de' });
// a region nobody owns is simply absent
expect(m['normandie-dddd']).toBeUndefined();
});
});
describe('regionOwner', () => {
it('resolves an owned region key to its owner record', () => {
expect(regionOwner(regions, 'albania-aaaa')).toEqual({
color: nationColor('uk'),
owner: 'UK',
ownerId: 'uk',
});
expect(regionOwner(regions, 'brandenburg-cccc')).toEqual({
color: nationColor('de'),
owner: 'DE',
ownerId: 'de',
});
});
it('returns undefined for an unowned region (no RegionMap entry)', () => {
expect(regionOwner(regions, 'normandie-dddd')).toBeUndefined();
expect(regionOwner(regions, 'yorkshire-eeee')).toBeUndefined();
});
it('returns undefined for an empty or missing key', () => {
expect(regionOwner(regions, '')).toBeUndefined();
expect(regionOwner(regions, null)).toBeUndefined();
expect(regionOwner(regions, undefined)).toBeUndefined();
});
});
/**
* #6 regression guard (sub-national map, region-keyed). The click/hover handlers
* resolve the owning nation from a rendered map feature. MapLibre repacks a
* GeoJSON source's top-level `id` into a *numeric index* when it stores the
* source, so a feature the map hands back at interaction time has a number for
* `id`. The choropleth is sub-national: a feature's own sub-region id is a code
* (`DEU-03`), and its owner now lives at its **region key** (`brandenburg-cccc`),
* which the client stamps into `properties.regionKey` from `region-keys.json`
* (the RegionMap is keyed by that key, never the sub-region id nor the repacked
* id). This pins the exact data shape and proves the resolution:
* `mapFeatureOwner` works via `properties.regionKey`, while the two wrong paths
* (the repacked numeric `feature.id`, and the sub-region `regionId`, which is
* not a RegionMap key) do not.
*/
describe('#6 — sub-region owner must be resolved from properties.regionKey, not the sub-region id / repacked feature.id', () => {
// The shape MapLibre returns for a sub-region feature after setData(): `id`
// is a number (repacked), and the stable identifiers live only in properties —
// `regionId` (the sub-region code) and `regionKey` (the region key).
const repackedFeature = {
id: 0,
properties: { name: 'Brandenburg R03', regionId: 'DEU-03', regionKey: 'brandenburg-cccc' },
};
it('resolves the owner from properties.regionKey (the fix)', () => {
expect(mapFeatureOwner(regions, repackedFeature)).toEqual({
color: nationColor('de'),
owner: 'DE',
ownerId: 'de',
});
});
it('the sub-region regionId is NOT a RegionMap key — resolving from it fails', () => {
// 'DEU-03' is a sub-region id; the RegionMap is keyed by region key.
expect(regionOwner(regions, repackedFeature.properties.regionId)).toBeUndefined();
});
it('the repacked numeric feature.id does NOT resolve — the bug', () => {
// String(0) === '0' is not a valid key, so the pre-fix lookup failed and
// reported "No owner" even for owned regions.
expect(regionOwner(regions, String(repackedFeature.id))).toBeUndefined();
});
});
describe('mapFeatureOwner', () => {
it('returns undefined for a missing feature, missing regionKey, or non-string regionKey', () => {
expect(mapFeatureOwner(regions, null)).toBeUndefined();
expect(mapFeatureOwner(regions, undefined)).toBeUndefined();
expect(mapFeatureOwner(regions, {})).toBeUndefined();
expect(mapFeatureOwner(regions, { properties: {} })).toBeUndefined();
expect(mapFeatureOwner(regions, { properties: { regionKey: 42 } })).toBeUndefined();
});
it('returns undefined for a region key nobody owns', () => {
expect(mapFeatureOwner(regions, { properties: { regionKey: 'normandie-dddd' } })).toBeUndefined();
expect(mapFeatureOwner(regions, { properties: { regionKey: 'yorkshire-eeee' } })).toBeUndefined();
});
});
describe('nationColor', () => {
it('is deterministic per id and distinct across different ids', () => {
expect(nationColor('uk')).toBe(nationColor('uk'));
expect(nationColor('uk')).not.toBe(nationColor('de'));
// always a valid HSL string
expect(nationColor('uk')).toMatch(/^hsl\(\d+ 58% 46%\)$/);
});
});