import assert from 'node:assert/strict'; import { createServer } from 'vite'; const storage = new Map(); globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { storage.set(key, String(value)); }, removeItem(key) { storage.delete(key); }, clear() { storage.clear(); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true }, appType: 'custom' }); try { const campaign = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const { battleScenarios } = await server.ssrLoadModule( '/src/game/data/battles.ts' ); const firstPursuit = await server.ssrLoadModule( '/src/game/data/firstPursuitScoutMemory.ts' ); const secondRelief = await server.ssrLoadModule( '/src/game/data/secondBattleReliefExploration.ts' ); const thirdCamp = await server.ssrLoadModule( '/src/game/data/thirdCampExploration.ts' ); const campFixtures = [ { label: 'first camp', visitId: firstPursuit.firstPursuitScoutVisitId, sourceBattleId: firstPursuit.firstPursuitScoutSourceBattleId, step: 'first-camp', dialogueKeys: [ 'visit-interaction', 'camp-exit', 'first-choice-response' ], choiceModes: ['visit'], invalidOverlays: [ { type: 'dialogue', dialogueKey: 'second-choice-response', lineIndex: 1 }, { type: 'choice', mode: 'third-companion' }, { type: 'shop', sourceNpcId: 'quartermaster' } ] }, { label: 'second camp', visitId: secondRelief.secondBattleReliefVisitId, sourceBattleId: secondRelief.secondBattleReliefSourceBattleId, step: 'second-camp', dialogueKeys: [ 'visit-interaction', 'camp-exit', 'second-choice-response' ], choiceModes: ['visit'], invalidOverlays: [ { type: 'dialogue', dialogueKey: 'first-choice-response', lineIndex: 1 }, { type: 'choice', mode: 'city-resonance' } ] }, { label: 'third camp', visitId: thirdCamp.thirdCampExplorationVisitId, sourceBattleId: thirdCamp.thirdCampExplorationSourceBattleId, step: 'third-camp', dialogueKeys: [ 'third-intro', 'visit-interaction', 'camp-exit', 'third-companion-response' ], choiceModes: ['third-companion'], invalidOverlays: [ { type: 'dialogue', dialogueKey: 'first-choice-response', lineIndex: 1 }, { type: 'choice', mode: 'visit' } ] } ]; const routes = [ ...campFixtures.map((fixture) => ({ label: fixture.label, state: () => validCampState(fixture), scene: 'camp-visit-exploration', contextId: fixture.visitId, dialogueKeys: fixture.dialogueKeys, choiceOverlays: fixture.choiceModes.map((mode) => ({ type: 'choice', mode, sourceNpcId: `${fixture.label}-npc` })), shopOverlays: [], invalidOverlays: fixture.invalidOverlays })), { label: 'city', state: validCityState, scene: 'city-stay', contextId: 'xuzhou', dialogueKeys: [ 'city-interaction', 'city-exit', 'city-resonance-response' ], choiceOverlays: [ { type: 'choice', mode: 'city-resonance', sourceNpcId: 'city-companion' } ], shopOverlays: [ { type: 'shop', sourceNpcId: 'city-quartermaster' } ], invalidOverlays: [ { type: 'dialogue', dialogueKey: 'first-choice-response', lineIndex: 1 }, { type: 'choice', mode: 'visit' } ] }, { label: 'prologue village', state: validVillageState, scene: 'prologue-village', contextId: 'prologue-village', dialogueKeys: [ 'prologue-village-interaction', 'prologue-village-oath' ], choiceOverlays: [], shopOverlays: [], invalidOverlays: [ { type: 'choice', mode: 'city-resonance' }, { type: 'dialogue', dialogueKey: 'prologue-camp-interaction', lineIndex: 1 } ] }, { label: 'prologue militia camp', state: validMilitiaState, scene: 'prologue-militia-camp', contextId: 'prologue-militia-camp', dialogueKeys: [ 'prologue-camp-interaction', 'prologue-camp-departure', 'prologue-camp-command-response' ], choiceOverlays: [ { type: 'choice', mode: 'prologue-command', sourceNpcId: 'liu-bei' }, { type: 'choice', mode: 'prologue-command', sourceNpcId: 'liu-bei', choiceId: 'mobile' } ], shopOverlays: [], invalidOverlays: [ { type: 'choice', mode: 'prologue-command', choiceId: 'unknown-order' }, { type: 'shop', sourceNpcId: 'camp-quartermaster' }, { type: 'dialogue', dialogueKey: 'prologue-village-oath', lineIndex: 1 } ] } ]; verifyValidPolicyMatrix(); verifyStoredInvalidOverlayPreservesPlayer(); verifyInvalidSetterIsCompleteNoop(); verifyStaleCampToCityRace(); console.log( 'Verified exploration checkpoint normalization: complete per-context overlay policy matrix, stored invalid-overlay player preservation, invalid-setter state/timestamp/base+slot byte invariance, sortie-order choice validation, and stale camp-to-city write rejection.' ); function verifyValidPolicyMatrix() { for (const route of routes) { const overlays = [ ...route.dialogueKeys.map((dialogueKey) => ({ type: 'dialogue', dialogueKey, sourceNpcId: `${route.label}-npc`, lineIndex: 4 })), ...route.choiceOverlays, ...route.shopOverlays ]; for (const overlay of overlays) { const checkpoint = checkpointFor(route, overlay); const loaded = loadRawState( withCheckpoint(route.state(), checkpoint) ); assert.deepEqual( loaded.explorationCheckpoint, checkpoint, `${route.label}: valid ${overlay.type} overlay must survive stored-save normalization.` ); } const checkpointWithoutOverlay = checkpointFor(route); const loadedWithoutOverlay = loadRawState( withCheckpoint(route.state(), checkpointWithoutOverlay) ); assert.deepEqual( loadedWithoutOverlay.explorationCheckpoint, checkpointWithoutOverlay, `${route.label}: an absent overlay must remain absent.` ); } } function verifyStoredInvalidOverlayPreservesPlayer() { for (const route of routes) { for (const invalidOverlay of [ ...route.invalidOverlays, null, { type: 'unknown-overlay' } ]) { const rawCheckpoint = checkpointFor( route, invalidOverlay ); const expectedCheckpoint = checkpointFor(route); const loaded = loadRawState( withCheckpoint(route.state(), rawCheckpoint) ); assert.deepEqual( loaded.explorationCheckpoint, expectedCheckpoint, `${route.label}: an incompatible stored overlay must be removed without losing the player checkpoint.` ); } } } function verifyInvalidSetterIsCompleteNoop() { const firstRoute = routes[0]; assertSetterNoop( firstRoute, { ...checkpointFor(firstRoute), player: { x: -1, y: 512, direction: 'south' } }, 'invalid player coordinates' ); assertSetterNoop( firstRoute, { ...checkpointFor(firstRoute), player: { x: 420, y: 512, direction: 'upward' } }, 'invalid player direction' ); assertSetterNoop( firstRoute, checkpointFor(firstRoute, { type: 'dialogue', dialogueKey: 'city-interaction', lineIndex: 0 }), 'context-incompatible overlay' ); assertSetterNoop( firstRoute, { ...checkpointFor(firstRoute), contextId: campFixtures[1].visitId }, 'stale context' ); const militiaRoute = routes.find( ({ scene }) => scene === 'prologue-militia-camp' ); assert(militiaRoute); assertSetterNoop( militiaRoute, checkpointFor(militiaRoute, { type: 'choice', mode: 'prologue-command', choiceId: 'unknown-order' }), 'invalid sortie order choice id' ); const cityRoute = routes.find( ({ scene }) => scene === 'city-stay' ); assert(cityRoute); assertSetterNoop( cityRoute, checkpointFor(cityRoute, { type: 'choice', mode: 'city-resonance', choiceId: 'mobile' }), 'unexpected choice id outside prologue command' ); } function assertSetterNoop(route, invalidCheckpoint, label) { storage.clear(); campaign.setCampaignState(route.state()); campaign.setCampaignExplorationCheckpoint( setterCheckpointFor( checkpointFor(route, validOverlayFor(route)) ) ); const beforeState = campaign.getCampaignState(); const beforeBase = storage.get(campaign.campaignStorageKey); const beforeSlot = storage.get( slotKey(beforeState.activeSaveSlot) ); assert(beforeBase, `${route.label}: base fixture is missing.`); assert(beforeSlot, `${route.label}: slot fixture is missing.`); const returned = campaign.setCampaignExplorationCheckpoint( setterCheckpointFor(invalidCheckpoint) ); const afterState = campaign.getCampaignState(); assert.deepEqual( returned, beforeState, `${route.label} ${label}: setter return must preserve the complete state.` ); assert.deepEqual( afterState, beforeState, `${route.label} ${label}: in-memory state and updatedAt must remain unchanged.` ); assert.equal( storage.get(campaign.campaignStorageKey), beforeBase, `${route.label} ${label}: base-save bytes must remain unchanged.` ); assert.equal( storage.get(slotKey(beforeState.activeSaveSlot)), beforeSlot, `${route.label} ${label}: slot-save bytes must remain unchanged.` ); } function verifyStaleCampToCityRace() { const cityRoute = routes.find( ({ scene }) => scene === 'city-stay' ); assert(cityRoute); storage.clear(); campaign.setCampaignState(cityRoute.state()); campaign.setCampaignExplorationCheckpoint( setterCheckpointFor( checkpointFor(cityRoute, validOverlayFor(cityRoute)) ) ); const beforeState = campaign.getCampaignState(); const beforeBase = storage.get(campaign.campaignStorageKey); const beforeSlot = storage.get( slotKey(beforeState.activeSaveSlot) ); const staleCampCheckpoint = { version: 1, scene: 'camp-visit-exploration', contextId: campFixtures[0].visitId, player: { x: 701, y: 601, direction: 'west' }, overlay: { type: 'dialogue', dialogueKey: 'visit-interaction', sourceNpcId: 'stale-camp-npc', lineIndex: 1 } }; const returned = campaign.setCampaignExplorationCheckpoint( staleCampCheckpoint ); assert.deepEqual( returned, beforeState, 'A stale camp pagehide write after city activation must return the unchanged city state.' ); assert.deepEqual( campaign.getCampaignState(), beforeState, 'A stale camp pagehide write must not erase or replace the city checkpoint.' ); assert.equal( storage.get(campaign.campaignStorageKey), beforeBase, 'A stale camp pagehide write must not change base-save bytes.' ); assert.equal( storage.get(slotKey(beforeState.activeSaveSlot)), beforeSlot, 'A stale camp pagehide write must not change slot-save bytes.' ); } function validOverlayFor(route) { if (route.choiceOverlays.length > 0) { return route.choiceOverlays.at(-1); } return { type: 'dialogue', dialogueKey: route.dialogueKeys[0], sourceNpcId: `${route.label}-npc`, lineIndex: 1 }; } function checkpointFor(route, overlay) { return { version: 1, scene: route.scene, contextId: route.contextId, player: { x: 420.25, y: 512.5, direction: 'south' }, ...(arguments.length >= 2 ? { overlay } : {}), savedAt: '2026-07-28T10:30:00.000Z' }; } function setterCheckpointFor(checkpoint) { const { savedAt: _savedAt, ...setterCheckpoint } = checkpoint; return setterCheckpoint; } function withCheckpoint(state, checkpoint) { return { ...state, explorationCheckpoint: clone(checkpoint) }; } function loadRawState(state) { storage.clear(); const serialized = JSON.stringify(state); storage.set(campaign.campaignStorageKey, serialized); storage.set( slotKey(state.activeSaveSlot ?? 1), serialized ); return campaign.loadCampaignState(); } function validCampState(fixture, slot = 2) { const report = battleReport(fixture.sourceBattleId); const state = campaign.createInitialCampaignState(); state.updatedAt = '2026-07-28T10:00:00.000Z'; state.step = fixture.step; state.activeSaveSlot = slot; state.gold = 420; state.roster = clone(report.units); state.bonds = clone(report.bonds); state.latestBattleId = fixture.sourceBattleId; state.firstBattleReport = report; state.battleHistory = { [fixture.sourceBattleId]: battleSettlement(report) }; state.activeCampVisitId = fixture.visitId; delete state.pendingAftermathBattleId; delete state.activeCityStayId; return state; } function validCityState(slot = 2) { const battleId = 'seventh-battle-xuzhou-rescue'; const report = battleReport(battleId); const state = campaign.createInitialCampaignState(); state.updatedAt = '2026-07-28T10:05:00.000Z'; state.step = 'seventh-camp'; state.activeSaveSlot = slot; state.gold = 640; state.roster = clone(report.units); state.bonds = clone(report.bonds); state.latestBattleId = battleId; state.firstBattleReport = report; state.battleHistory = { [battleId]: battleSettlement(report) }; state.activeCityStayId = 'xuzhou'; delete state.pendingAftermathBattleId; delete state.activeCampVisitId; return state; } function validVillageState(slot = 2) { const state = campaign.createInitialCampaignState(); state.updatedAt = '2026-07-28T10:10:00.000Z'; state.step = 'prologue-town'; state.activeSaveSlot = slot; return state; } function validMilitiaState(slot = 2) { const state = campaign.createInitialCampaignState(); state.updatedAt = '2026-07-28T10:15:00.000Z'; state.step = 'prologue-camp'; state.activeSaveSlot = slot; return state; } function battleReport(battleId) { const scenario = battleScenarios[battleId]; assert(scenario, `Missing battle scenario fixture ${battleId}.`); const alliedUnits = scenario.units .filter((unit) => unit.faction === 'ally') .map((unit) => clone(unit)); return { battleId, battleTitle: scenario.title, outcome: 'victory', turnNumber: 5, rewardGold: 100, defeatedEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, totalEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, objectives: [], units: alliedUnits, bonds: scenario.bonds.map((bond) => ({ ...clone(bond), battleExp: 0 })), itemRewards: [], campaignRewards: { supplies: [], equipment: [], reputation: [], recruits: [], unlocks: [] }, completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-28T10:00:00.000Z' }; } function battleSettlement(report) { return { battleId: report.battleId, battleTitle: report.battleTitle, outcome: report.outcome, turnNumber: report.turnNumber, rewardGold: report.rewardGold, itemRewards: [...report.itemRewards], campaignRewards: clone(report.campaignRewards), objectives: [], units: [], bonds: [], completedAt: report.createdAt }; } function slotKey(slot) { return `${campaign.campaignStorageKey}:slot-${slot}`; } function clone(value) { return JSON.parse(JSON.stringify(value)); } } finally { await server.close(); }