refactor: complete error handling standardization (issue #101)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m26s

Prestige Store:
- Convert doPrestige() to return Result<void> with specific error codes
  (INVALID_PRESTIGE_ID, PRESTIGE_MAX_LEVEL, INSUFFICIENT_INSIGHT)
- Convert startPactRitual() to return Result<void> with specific error codes
  (GUARDIAN_NOT_DEFEATED, PACT_ALREADY_SIGNED, PACT_SLOTS_FULL,
   INSUFFICIENT_MANA, RITUAL_IN_PROGRESS)

Combat Actions:
- Add try/catch wrapper inside processCombatTick with safe fallback defaults
- Add makeDefaultCombatTickResult helper for error recovery

LocalStorage Error Handling:
- Create safe-persist.ts utility wrapping localStorage with error handling
  (corrupted JSON, quota exceeded, unexpected failures)
- Update all 8 Zustand stores to use createSafeStorage() in persist middleware

UI Updates:
- Update GuardianPactsTab to use Result pattern for ritual error messages

Tests:
- Update store-actions-combat-prestige.test.ts for Result return types
- Update store-actions.test.ts ManaStore tests for Result pattern
- Remove duplicate Prestige/Discipline sections from store-actions.test.ts
- All files under 400 line limit

601 tests pass (3 pre-existing failures in spire-utils.test.ts)
This commit is contained in:
2026-05-22 09:19:20 +02:00
parent 8a7ddaae27
commit 49f8de01ca
21 changed files with 542 additions and 547 deletions
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { useCombatStore } from '../stores/combatStore';
import { usePrestigeStore } from '../stores/prestigeStore';
import { getFloorMaxHP } from '../utils';
import { ErrorCode } from '../utils/result';
function resetCombatStore() {
useCombatStore.setState({
@@ -188,7 +189,7 @@ describe('PrestigeStore', () => {
describe('doPrestige', () => {
it('should purchase upgrade when affordable', () => {
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(usePrestigeStore.getState().prestigeUpgrades.manaWell).toBe(1);
expect(usePrestigeStore.getState().insight).toBeLessThan(500);
});
@@ -196,18 +197,25 @@ describe('PrestigeStore', () => {
it('should return false when cannot afford', () => {
usePrestigeStore.setState({ insight: 0 });
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_INSIGHT);
}
});
it('should return false for invalid upgrade id', () => {
it('should fail for invalid upgrade id', () => {
const result = usePrestigeStore.getState().doPrestige('nonexistent');
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INVALID_PRESTIGE_ID);
}
});
it('should increase memorySlots with deepMemory', () => {
usePrestigeStore.setState({ insight: 2000 });
const before = usePrestigeStore.getState().memorySlots;
usePrestigeStore.getState().doPrestige('deepMemory');
const deepResult = usePrestigeStore.getState().doPrestige('deepMemory');
expect(deepResult.success).toBe(true);
expect(usePrestigeStore.getState().memorySlots).toBe(before + 1);
});
});
@@ -280,31 +288,43 @@ describe('PrestigeStore', () => {
it('should start ritual when conditions met', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [], insight: 10000 });
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(usePrestigeStore.getState().pactRitualFloor).toBe(10);
});
it('should return false when guardian not defeated', () => {
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.GUARDIAN_NOT_DEFEATED);
}
});
it('should return false when already signed', () => {
it('should fail when already signed', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] });
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.PACT_ALREADY_SIGNED);
}
});
it('should return false when pact slots full', () => {
it('should fail when pact slots full', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [20], pactSlots: 1 });
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.PACT_SLOTS_FULL);
}
});
it('should return false when insufficient mana', () => {
it('should fail when insufficient mana', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [] });
const result = usePrestigeStore.getState().startPactRitual(10, 0);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});