import assert from 'node:assert/strict'; import { createServer } from 'vite'; const storage = createStorage(); globalThis.window = { localStorage: storage }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const { campaignSaveStorageKey, campaignStorageKey, createInitialCampaignState, setCampaignState } = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const { manualSaveSlotCount, manualSaveStorageKeyForSlot, readManualSave, listManualSaveSlots, writeManualSave, restoreManualSave, ManualSaveStorageError } = await server.ssrLoadModule( '/src/game/state/manualSaveStorage.ts' ); const { battleSaveStorageBaseKey, battleSaveStorageSlotKey, legacyFirstBattleSaveStorageKey } = await server.ssrLoadModule( '/src/game/state/battleSaveStorage.ts' ); assert.equal(manualSaveSlotCount, 3); assert.throws( () => manualSaveStorageKeyForSlot(0), (error) => error instanceof ManualSaveStorageError && error.code === 'invalid-slot' ); const firstBattleId = 'first-battle-zhuo-commandery'; const secondBattleId = 'second-battle-yellow-turban-pursuit'; const sourceCampaign = createInitialCampaignState(); sourceCampaign.step = 'first-battle'; sourceCampaign.activeSaveSlot = 3; sourceCampaign.gold = 321; sourceCampaign.inventory = { bean: 2, salve: 1 }; sourceCampaign.battleResumeGeneration = 7; sourceCampaign.updatedAt = '2026-07-29T01:00:00.000Z'; setCampaignState(sourceCampaign); const battle = createBattleSave({ battleId: firstBattleId, campaignStep: 'first-battle', battleResumeGeneration: 7, turnNumber: 4, activeFaction: 'enemy' }); const autosaveBeforeManualWrite = storage.snapshot(); const written = writeManualSave(2, { campaign: sourceCampaign, battle, context: 'battle' }); assert.equal(written.slot, 2); assert.equal(written.context, 'battle'); assert.equal( written.campaign.activeSaveSlot, 3, 'Writing a protected manual record must not rewrite its source campaign lane.' ); assertStorageEntriesEqual( storage.snapshot(), autosaveBeforeManualWrite, (key) => key !== manualSaveStorageKeyForSlot(2), 'Writing a manual save changed an autosave key.' ); const summaries = listManualSaveSlots(); const slotTwoSummary = summaries.find( (summary) => summary.slot === 2 ); assert.deepEqual( { exists: slotTwoSummary?.exists, status: slotTwoSummary?.status, context: slotTwoSummary?.context, gold: slotTwoSummary?.gold, inventoryCount: slotTwoSummary?.inventoryCount, battleId: slotTwoSummary?.battleId, turnNumber: slotTwoSummary?.turnNumber, activeFaction: slotTwoSummary?.activeFaction }, { exists: true, status: 'valid', context: 'battle', gold: 321, inventoryCount: 3, battleId: firstBattleId, turnNumber: 4, activeFaction: 'enemy' } ); assert.equal( summaries.find((summary) => summary.slot === 1) ?.status, 'empty' ); const protectedSlotTwoRaw = storage.getItem( manualSaveStorageKeyForSlot(2) ); const replacementCampaign = createInitialCampaignState(); replacementCampaign.step = 'second-battle'; replacementCampaign.activeSaveSlot = 2; replacementCampaign.gold = 9; replacementCampaign.battleResumeGeneration = 12; setCampaignState(replacementCampaign); const firstSlotTwoKey = battleSaveStorageSlotKey( firstBattleId, 2 ); const secondSlotTwoKey = battleSaveStorageSlotKey( secondBattleId, 2 ); const unrelatedSlotThreeKey = battleSaveStorageSlotKey( secondBattleId, 3 ); storage.setItem( firstSlotTwoKey, 'old-first-battle-autosave' ); storage.setItem( secondSlotTwoKey, 'old-second-battle-autosave' ); storage.setItem( unrelatedSlotThreeKey, 'unrelated-slot-three' ); const restored = restoreManualSave(2); assert.equal(restored.activeSaveSlot, 2); assert.equal(restored.step, 'first-battle'); assert.equal(restored.gold, 321); assert.equal( JSON.parse( storage.getItem(campaignStorageKey) ).activeSaveSlot, 2 ); assert.equal( JSON.parse( storage.getItem(campaignSaveStorageKey(2)) ).gold, 321 ); assert.equal( JSON.parse(storage.getItem(firstSlotTwoKey)) .turnNumber, 4 ); assert.equal( storage.getItem(secondSlotTwoKey), null, 'Restore retained a stale battle autosave in the destination lane.' ); assert.equal( storage.getItem(unrelatedSlotThreeKey), 'unrelated-slot-three', 'Restore changed a battle autosave in another lane.' ); assert.equal( storage.getItem(manualSaveStorageKeyForSlot(2)), protectedSlotTwoRaw, 'Restore changed the protected manual record bytes.' ); assert.equal( readManualSave(2)?.campaign.activeSaveSlot, 3, 'Restore mutated the campaign stored inside the protected manual envelope.' ); const campCampaign = createInitialCampaignState(); campCampaign.step = 'first-camp'; campCampaign.activeSaveSlot = 1; campCampaign.gold = 144; writeManualSave(1, { campaign: campCampaign, context: 'camp' }); assert.equal(readManualSave(1)?.battle, undefined); assert.throws( () => writeManualSave(3, { campaign: campCampaign, battle, context: 'camp' }), (error) => error instanceof ManualSaveStorageError && error.code === 'invalid-save' ); const slotOneCampaignRaw = '{"auto":"slot-one-before"}'; const currentCampaignRaw = '{"auto":"current-before"}'; const slotOneBattleKey = battleSaveStorageSlotKey(firstBattleId, 1); const slotOneBattleBaseKey = battleSaveStorageBaseKey(firstBattleId); const slotOneSecondBattleKey = battleSaveStorageSlotKey(secondBattleId, 1); const slotOneSecondBattleBaseKey = battleSaveStorageBaseKey(secondBattleId); storage.setItem( campaignStorageKey, currentCampaignRaw ); storage.setItem( campaignSaveStorageKey(1), slotOneCampaignRaw ); storage.setItem( slotOneBattleKey, 'slot-one-first-before' ); storage.setItem( slotOneBattleBaseKey, 'base-first-before' ); storage.setItem( legacyFirstBattleSaveStorageKey, 'legacy-first-before' ); storage.setItem( slotOneSecondBattleKey, 'slot-one-second-before' ); storage.setItem( slotOneSecondBattleBaseKey, 'base-second-before' ); const beforeFailedRestore = storage.snapshot(); storage.failNextSet(campaignSaveStorageKey(1)); assert.throws( () => restoreManualSave(1), (error) => error instanceof ManualSaveStorageError && error.code === 'restore-failed' && error.rollbackSucceeded === true ); assert.deepEqual( storage.snapshot(), beforeFailedRestore, 'A failed restore did not roll back every campaign, battle, compatibility, and manual byte.' ); const protectedSlotOneRaw = storage.getItem( manualSaveStorageKeyForSlot(1) ); storage.failNextSet( manualSaveStorageKeyForSlot(1) ); assert.throws( () => writeManualSave(1, { campaign: campCampaign, context: 'camp' }), (error) => error instanceof ManualSaveStorageError && error.code === 'write-failed' && error.rollbackSucceeded === true ); assert.equal( storage.getItem(manualSaveStorageKeyForSlot(1)), protectedSlotOneRaw, 'A failed overwrite changed the existing protected manual record.' ); storage.setItem( manualSaveStorageKeyForSlot(3), JSON.stringify({ version: 99, slot: 3, savedAt: '2026-07-29T02:00:00.000Z', context: 'camp', campaign: campCampaign }) ); assert.equal(readManualSave(3), undefined); assert.deepEqual( listManualSaveSlots().find( (summary) => summary.slot === 3 ), { slot: 3, exists: true, status: 'invalid' }, 'Unsupported manual-save bytes must remain visibly occupied rather than masquerading as an empty slot.' ); console.log( 'Verified protected manual-save envelopes, summaries, context/version/slot validation, autosave-write isolation, exact campaign+battle restore, stale battle cleanup, manual-byte invariance, and full rollback after write failure.' ); } finally { await server.close(); delete globalThis.window; } function createBattleSave(overrides = {}) { return { version: 1, battleId: 'first-battle-zhuo-commandery', campaignStep: 'first-battle', battleResumeGeneration: 0, savedAt: '2026-07-29T01:05:00.000Z', turnNumber: 1, activeFaction: 'ally', rosterTab: 'ally', actedUnitIds: [], attackIntents: [], battleLog: [], units: [], bonds: [], ...overrides }; } function assertStorageEntriesEqual( current, expected, include, message ) { const currentFiltered = Object.fromEntries( Object.entries(current).filter(([key]) => include(key) ) ); const expectedFiltered = Object.fromEntries( Object.entries(expected).filter(([key]) => include(key) ) ); assert.deepEqual( currentFiltered, expectedFiltered, message ); } function createStorage() { const values = new Map(); const failingSetCounts = new Map(); return { get length() { return values.size; }, key(index) { return [...values.keys()][index] ?? null; }, getItem(key) { return values.has(String(key)) ? values.get(String(key)) : null; }, setItem(key, value) { const normalizedKey = String(key); const remaining = failingSetCounts.get(normalizedKey) ?? 0; if (remaining > 0) { if (remaining === 1) { failingSetCounts.delete(normalizedKey); } else { failingSetCounts.set( normalizedKey, remaining - 1 ); } throw new DOMException( 'Injected manual-save write refusal', 'QuotaExceededError' ); } values.set(normalizedKey, String(value)); }, removeItem(key) { values.delete(String(key)); }, clear() { values.clear(); failingSetCounts.clear(); }, failNextSet(key) { const normalizedKey = String(key); failingSetCounts.set( normalizedKey, (failingSetCounts.get(normalizedKey) ?? 0) + 1 ); }, snapshot() { return Object.fromEntries( [...values.entries()].sort(([left], [right]) => left.localeCompare(right) ) ); } }; }