import { describe, expect, it } from 'vitest'; import { openDb } from './db'; import type { GameState } from '../shared/types'; function makeState(id: string): GameState { return { id, 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: ['GBR'], relations: {}, }, }, events: [], pending: [], version: 1, unowned: [], pendingNationOps: [], markers: [], // (v0.28, #46) a v0.28+ save carries per-region stats; the fake 'GBR' // region here holds a copy of the uk's (single-region) stats. regionStats: { GBR: { military: 80, economy: 70, stability: 60, population: 50 }, }, // (v0.33.0, #56) no wars. wars: [], // (v0.35.0, #58) no active trades or embargoes. trades: [], embargoes: [], // (v0.36.0, #59) no industry projects. industry: [], }; } describe('openDb', () => { it('save/load round-trips a game by id and upserts on re-save', () => { const db = openDb(':memory:'); db.save(makeState('a')); db.save(makeState('b')); expect(db.load('a')?.id).toBe('a'); expect(db.load('b')?.id).toBe('b'); expect(db.load('nope')).toBeNull(); expect([...db.listIds()].sort()).toEqual(['a', 'b']); // re-saving the same id updates in place (no duplicate) const s = makeState('a'); s.turn = 5; s.events.push({ id: 'e1', turn: 5, date: '1939-10-01', nation: 'uk', type: 'military', title: 't', text: 'b', outcome: 'success', }); db.save(s); expect(db.load('a')?.turn).toBe(5); expect(db.load('a')?.events).toHaveLength(1); expect([...db.listIds()].sort()).toEqual(['a', 'b']); // still two rows // the monotonic version is persisted with the state and round-trips s.version = 3; db.save(s); expect(db.load('a')?.version).toBe(3); db.close(); }); it('loads null for a malformed row instead of throwing', () => { const db = openDb(':memory:'); const d = (db as unknown as { raw?: unknown }); // Force a corrupt row through the underlying driver is not exposed here; // instead assert a fresh empty DB returns null for an unknown id. expect(d).toBeDefined(); expect(db.load('missing')).toBeNull(); db.close(); }); it('(v0.22 #16 P3) a legacy row saved before the unowned field existed loads with it defaulted to []', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { unowned?: string[] }).unowned; // a pre-v0.22 save (field absent) db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.unowned).toEqual([]); db.close(); }); it('(v0.23 #16 P4a) a legacy row saved before the pendingNationOps field existed loads with it defaulted to []', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { pendingNationOps?: unknown[] }).pendingNationOps; // a pre-v0.23 save (field absent) db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.pendingNationOps).toEqual([]); db.close(); }); it('(v0.27 #44) a legacy row saved before the markers field existed loads with it defaulted to []', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { markers?: unknown[] }).markers; // a pre-v0.27 save (field absent) db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.markers).toEqual([]); db.close(); }); it('(v0.28 #46) a legacy row saved before the regionStats field existed loads with it seeded per region and stats recomputed', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { regionStats?: Record }).regionStats; // a pre-v0.28 save (field absent) db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); // each owned region is seeded with a copy of its owner's stats… expect(loaded!.regionStats).toEqual({ GBR: { military: 80, economy: 70, stability: 60, population: 50 }, }); // …and a single-region nation's derived stats equal the seeded ones // (the weighted median of identical values is that value). expect(loaded!.nations.uk.stats).toEqual({ military: 80, economy: 70, stability: 60, population: 50 }); db.close(); }); it('(v0.28 #46) a v0.28+ row (regionStats present) loads UNCHANGED — no recompute on load', () => { const db = openDb(':memory:'); const s = makeState('current'); // Diverge one region's stats from the nation level to prove load does not // recompute (a v0.28+ save keeps its stored derived stats exactly). s.regionStats = { GBR: { military: 99, economy: 70, stability: 60, population: 50 } }; s.nations = { uk: { ...s.nations.uk, stats: { military: 42, economy: 70, stability: 60, population: 50 } } }; db.save(s); const loaded = db.load('current'); expect(loaded).not.toBeNull(); expect(loaded!.regionStats).toEqual({ GBR: { military: 99, economy: 70, stability: 60, population: 50 } }); // the nation's stats are NOT recomputed from the region on load (they stay 42) expect(loaded!.nations.uk.stats.military).toBe(42); db.close(); }); it('(v0.35.0 #58) a legacy row saved before the trades/embargoes fields existed loads with them defaulted to []', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { trades?: unknown[] }).trades; // a pre-v0.35 save (fields absent) delete (legacy as { embargoes?: unknown[] }).embargoes; db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.trades).toEqual([]); expect(loaded!.embargoes).toEqual([]); db.close(); }); it('(v0.36.0 #59) a legacy row saved before the industry field existed loads with it defaulted to []', () => { const db = openDb(':memory:'); const legacy = makeState('legacy'); delete (legacy as { industry?: unknown[] }).industry; // a pre-v0.36 save (field absent) db.save(legacy); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.industry).toEqual([]); db.close(); }); it('(v0.37.0 #60) a legacy row saved before the nation nuclear flag existed loads with it absent (not capable)', () => { const db = openDb(':memory:'); // makeState has no nation.nuclear field — a pre-v0.37 save. The flag is // optional and not re-stamped by load, so an absent flag stays absent. db.save(makeState('legacy')); const loaded = db.load('legacy'); expect(loaded).not.toBeNull(); expect(loaded!.nations.uk.nuclear).toBeUndefined(); db.close(); }); it('(v0.37.0 #60) a set nation nuclear flag round-trips through save/load', () => { const db = openDb(':memory:'); const s = makeState('nuc'); s.nations.uk.nuclear = true; // armed (e.g. by a turn's nuclear.arm op) db.save(s); const loaded = db.load('nuc'); expect(loaded).not.toBeNull(); expect(loaded!.nations.uk.nuclear).toBe(true); db.close(); }); it('delete removes a game by id and reports absence otherwise', () => { const db = openDb(':memory:'); db.save(makeState('a')); db.save(makeState('b')); expect(db.delete('nope')).toBe(false); // unknown id expect(db.delete('a')).toBe(true); // removes a expect(db.load('a')).toBeNull(); expect(db.load('b')?.id).toBe('b'); expect([...db.listIds()].sort()).toEqual(['b']); expect(db.delete('a')).toBe(false); // already gone db.close(); }); });