import { describe, expect, it } from 'vitest'; import { buildContent } from '../../content/schema'; import { createGameState, enqueueAction, startAction } from '../../engine/game'; import { createSave, serializeSave } from '../../engine/save'; import { createMemoryBackend, loadGame, saveGame } from '../persistence'; const DEFAULT_GROUP = { id: 'test', label: 'Test' }; function testContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], actions: [ { id: 'forage', name: 'Forage', group: DEFAULT_GROUP, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], }, ], }); } function queueTestContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], actions: [ { id: 'a', name: 'A', group: DEFAULT_GROUP, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], }, { id: 'b', name: 'B', group: DEFAULT_GROUP, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }], }, ], }); } describe('loadGame()', () => { it('returns a fresh game when no save exists', async () => { const content = testContent(); const result = await loadGame(content, createMemoryBackend(), 1000); expect(result.state.resources.gold).toBe(0); expect(result.offlineMs).toBe(0); }); it('round-trips a saved game and credits offline progress', async () => { const content = testContent(); const state = createGameState(content); state.resources.gold = 10; startAction(state, content, 'forage'); const backend = createMemoryBackend(); await saveGame(state, backend, 1000); const result = await loadGame(content, backend, 1000 + 9000); // 9s offline expect(result.offlineMs).toBe(9000); expect(result.state.resources.gold).toBe(11); // 10 + one completion expect(result.state.activeActionId).toBeNull(); }); it('falls back to a fresh game on a corrupt save instead of throwing', async () => { const content = testContent(); const result = await loadGame(content, createMemoryBackend('@@@garbage@@@'), 1000); expect(result.state.resources.gold).toBe(0); expect(result.offlineMs).toBe(0); }); it('restores actionQueue on load', async () => { const content = queueTestContent(); const state = createGameState(content); enqueueAction(state, content, 'a'); enqueueAction(state, content, 'b'); const backend = createMemoryBackend(); await saveGame(state, backend, 1000); const result = await loadGame(content, backend, 1000); expect(result.state.activeActionId).toBe('a'); expect(result.state.actionQueue).toEqual(['b']); }); it('restores story fields on load', async () => { const content = testContent(); const backend = createMemoryBackend(); const state = createGameState(content); state.storyFlags = { route_b: true }; state.currentStoryNodeId = 'fork_choice'; state.seenStoryNodeIds = ['boot_intro']; await saveGame(state, backend, 1000); const loaded = await loadGame(content, backend, 1000); expect(loaded.state.storyFlags.route_b).toBe(true); expect(loaded.state.currentStoryNodeId).toBe('fork_choice'); }); it('drops an active action that no longer exists in content', async () => { const content = testContent(); const stale = serializeSave( createSave( { resources: { gold: 1 }, activeActionId: 'ghost-action', actionElapsedMs: 0, actionQueue: [], storyFlags: {}, currentStoryNodeId: '', seenStoryNodeIds: [], enabledLoopActionIds: {}, }, 1000, ), ); const result = await loadGame(content, createMemoryBackend(stale), 1000); expect(result.state.activeActionId).toBeNull(); }); });