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);
}
});
});
@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useManaStore, makeInitialElements } from '../stores/manaStore';
import { MANA_PER_ELEMENT } from '../constants';
import { ErrorCode } from '../utils/result';
function resetManaStore() {
useManaStore.setState({
@@ -74,47 +75,65 @@ describe('ManaStore', () => {
it('should convert raw mana to element mana', () => {
useManaStore.setState({ rawMana: 500 });
const result = useManaStore.getState().convertMana('transference', 2);
expect(result).toBe(true);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.converted).toBe(2);
}
expect(useManaStore.getState().rawMana).toBe(500 - 2 * MANA_PER_ELEMENT);
expect(useManaStore.getState().elements.transference.current).toBe(2);
});
it('should return false for locked element', () => {
it('should fail for locked element', () => {
const result = useManaStore.getState().convertMana('fire', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.ELEMENT_NOT_UNLOCKED);
}
});
it('should return false when insufficient raw mana', () => {
it('should fail when insufficient raw mana', () => {
useManaStore.setState({ rawMana: 50 });
const result = useManaStore.getState().convertMana('transference', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
it('should return false when element is at max', () => {
it('should fail when element is at max', () => {
const elements = useManaStore.getState().elements;
elements.transference.current = elements.transference.max;
useManaStore.setState({ elements });
const result = useManaStore.getState().convertMana('transference', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.ELEMENT_MAX_CAPACITY);
}
});
});
describe('unlockElement', () => {
it('should unlock element and deduct cost', () => {
const result = useManaStore.getState().unlockElement('fire', 50);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().rawMana).toBe(50);
expect(useManaStore.getState().elements.fire.unlocked).toBe(true);
});
it('should return false when already unlocked', () => {
it('should fail when already unlocked', () => {
const result = useManaStore.getState().unlockElement('transference', 0);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INVALID_INPUT);
}
});
it('should return false when insufficient mana', () => {
it('should fail when insufficient mana', () => {
const result = useManaStore.getState().unlockElement('fire', 200);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
@@ -132,13 +151,16 @@ describe('ManaStore', () => {
it('should deduct element mana when sufficient', () => {
useManaStore.getState().addElementMana('transference', 20, 50);
const result = useManaStore.getState().spendElementMana('transference', 10);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().elements.transference.current).toBe(10);
});
it('should return false when insufficient element mana', () => {
it('should fail when insufficient element mana', () => {
const result = useManaStore.getState().spendElementMana('transference', 10);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
@@ -149,16 +171,19 @@ describe('ManaStore', () => {
useManaStore.getState().addElementMana('fire', 5, 50);
useManaStore.getState().addElementMana('earth', 5, 50);
const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().elements.fire.current).toBe(4);
expect(useManaStore.getState().elements.earth.current).toBe(4);
expect(useManaStore.getState().elements.metal.current).toBe(1);
expect(useManaStore.getState().elements.metal.unlocked).toBe(true);
});
it('should return false when missing ingredients', () => {
it('should fail when missing ingredients', () => {
const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
+43 -321
View File
@@ -1,10 +1,9 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useManaStore, makeInitialElements } from '../stores/manaStore';
import { useCombatStore } from '../stores/combatStore';
import { usePrestigeStore } from '../stores/prestigeStore';
import { useDisciplineStore } from '../stores/discipline-slice';
import { MANA_PER_ELEMENT } from '../constants';
import { getFloorMaxHP } from '../utils';
import { ErrorCode } from '../utils/result';
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -44,37 +43,6 @@ function resetCombatStore() {
});
}
function resetPrestigeStore() {
usePrestigeStore.setState({
loopCount: 0,
insight: 500,
totalInsight: 500,
loopInsight: 0,
prestigeUpgrades: {},
memorySlots: 3,
pactSlots: 1,
memories: [],
defeatedGuardians: [],
signedPacts: [],
signedPactDetails: {},
pactRitualFloor: null,
pactRitualProgress: 0,
});
}
function resetDisciplineStore() {
useDisciplineStore.setState({
disciplines: {},
activeIds: [],
concurrentLimit: 1,
totalXP: 0,
});
}
// ═══════════════════════════════════════════════════════════════════════════════
// MANA STORE
// ═══════════════════════════════════════════════════════════════════════════════
describe('ManaStore', () => {
beforeEach(resetManaStore);
@@ -138,47 +106,65 @@ describe('ManaStore', () => {
it('should convert raw mana to element mana', () => {
useManaStore.setState({ rawMana: 500 });
const result = useManaStore.getState().convertMana('transference', 2);
expect(result).toBe(true);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.converted).toBe(2);
}
expect(useManaStore.getState().rawMana).toBe(500 - 2 * MANA_PER_ELEMENT);
expect(useManaStore.getState().elements.transference.current).toBe(2);
});
it('should return false for locked element', () => {
it('should fail for locked element', () => {
const result = useManaStore.getState().convertMana('fire', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.ELEMENT_NOT_UNLOCKED);
}
});
it('should return false when insufficient raw mana', () => {
it('should fail when insufficient raw mana', () => {
useManaStore.setState({ rawMana: 50 });
const result = useManaStore.getState().convertMana('transference', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
it('should return false when element is at max', () => {
it('should fail when element is at max', () => {
const elements = useManaStore.getState().elements;
elements.transference.current = elements.transference.max;
useManaStore.setState({ elements });
const result = useManaStore.getState().convertMana('transference', 1);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.ELEMENT_MAX_CAPACITY);
}
});
});
describe('unlockElement', () => {
it('should unlock element and deduct cost', () => {
const result = useManaStore.getState().unlockElement('fire', 50);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().rawMana).toBe(50);
expect(useManaStore.getState().elements.fire.unlocked).toBe(true);
});
it('should return false when already unlocked', () => {
it('should fail when already unlocked', () => {
const result = useManaStore.getState().unlockElement('transference', 0);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INVALID_INPUT);
}
});
it('should return false when insufficient mana', () => {
it('should fail when insufficient mana', () => {
const result = useManaStore.getState().unlockElement('fire', 200);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
@@ -196,13 +182,16 @@ describe('ManaStore', () => {
it('should deduct element mana when sufficient', () => {
useManaStore.getState().addElementMana('transference', 20, 50);
const result = useManaStore.getState().spendElementMana('transference', 10);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().elements.transference.current).toBe(10);
});
it('should return false when insufficient element mana', () => {
it('should fail when insufficient element mana', () => {
const result = useManaStore.getState().spendElementMana('transference', 10);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
@@ -213,16 +202,19 @@ describe('ManaStore', () => {
useManaStore.getState().addElementMana('fire', 5, 50);
useManaStore.getState().addElementMana('earth', 5, 50);
const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']);
expect(result).toBe(true);
expect(result.success).toBe(true);
expect(useManaStore.getState().elements.fire.current).toBe(4);
expect(useManaStore.getState().elements.earth.current).toBe(4);
expect(useManaStore.getState().elements.metal.current).toBe(1);
expect(useManaStore.getState().elements.metal.unlocked).toBe(true);
});
it('should return false when missing ingredients', () => {
it('should fail when missing ingredients', () => {
const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']);
expect(result).toBe(false);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.code).toBe(ErrorCode.INSUFFICIENT_MANA);
}
});
});
@@ -402,273 +394,3 @@ describe('CombatStore', () => {
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// PRESTIGE STORE
// ═══════════════════════════════════════════════════════════════════════════════
describe('PrestigeStore', () => {
beforeEach(resetPrestigeStore);
describe('doPrestige', () => {
it('should purchase upgrade when affordable', () => {
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(true);
expect(usePrestigeStore.getState().prestigeUpgrades.manaWell).toBe(1);
expect(usePrestigeStore.getState().insight).toBeLessThan(500);
});
it('should return false when cannot afford', () => {
usePrestigeStore.setState({ insight: 0 });
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(false);
});
it('should return false for invalid upgrade id', () => {
const result = usePrestigeStore.getState().doPrestige('nonexistent');
expect(result).toBe(false);
});
it('should increase memorySlots with deepMemory', () => {
usePrestigeStore.setState({ insight: 2000 });
const before = usePrestigeStore.getState().memorySlots;
usePrestigeStore.getState().doPrestige('deepMemory');
expect(usePrestigeStore.getState().memorySlots).toBe(before + 1);
});
});
describe('addMemory / removeMemory', () => {
it('should add a memory when slots available', () => {
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 });
expect(usePrestigeStore.getState().memories.length).toBe(1);
});
it('should not add duplicate memory', () => {
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 });
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 5 });
expect(usePrestigeStore.getState().memories.length).toBe(1);
});
it('should not exceed memory slots', () => {
usePrestigeStore.setState({ memorySlots: 1 });
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 });
usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 });
expect(usePrestigeStore.getState().memories.length).toBe(1);
});
it('should remove memory by skillId', () => {
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 });
usePrestigeStore.getState().removeMemory('manaFlow');
expect(usePrestigeStore.getState().memories.length).toBe(0);
});
it('should clear all memories', () => {
usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 });
usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 });
usePrestigeStore.getState().clearMemories();
expect(usePrestigeStore.getState().memories.length).toBe(0);
});
});
describe('defeatGuardian / signedPacts', () => {
it('should add defeated guardian', () => {
usePrestigeStore.getState().defeatGuardian(10);
expect(usePrestigeStore.getState().defeatedGuardians).toContain(10);
});
it('should not duplicate defeated guardian', () => {
usePrestigeStore.getState().defeatGuardian(10);
usePrestigeStore.getState().defeatGuardian(10);
expect(usePrestigeStore.getState().defeatedGuardians.filter(f => f === 10).length).toBe(1);
});
it('should not defeat already signed guardian', () => {
usePrestigeStore.setState({ signedPacts: [10] });
usePrestigeStore.getState().defeatGuardian(10);
expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10);
});
it('should add signed pact', () => {
usePrestigeStore.getState().addSignedPact(10);
expect(usePrestigeStore.getState().signedPacts).toContain(10);
});
it('should remove pact', () => {
usePrestigeStore.setState({ signedPacts: [10, 20] });
usePrestigeStore.getState().removePact(10);
expect(usePrestigeStore.getState().signedPacts).not.toContain(10);
expect(usePrestigeStore.getState().signedPacts).toContain(20);
});
});
describe('startPactRitual', () => {
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(usePrestigeStore.getState().pactRitualFloor).toBe(10);
});
it('should return false when guardian not defeated', () => {
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
});
it('should return false when already signed', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] });
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
});
it('should return false when pact slots full', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [20], pactSlots: 1 });
const result = usePrestigeStore.getState().startPactRitual(10, 10000);
expect(result).toBe(false);
});
it('should return false when insufficient mana', () => {
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [] });
const result = usePrestigeStore.getState().startPactRitual(10, 0);
expect(result).toBe(false);
});
});
describe('cancelPactRitual', () => {
it('should cancel active ritual', () => {
usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 1 });
usePrestigeStore.getState().cancelPactRitual();
expect(usePrestigeStore.getState().pactRitualFloor).toBeNull();
expect(usePrestigeStore.getState().pactRitualProgress).toBe(0);
});
});
describe('startNewLoop', () => {
it('should increment loop count and add insight', () => {
usePrestigeStore.setState({ insight: 100, totalInsight: 100 });
usePrestigeStore.getState().startNewLoop(50);
expect(usePrestigeStore.getState().loopCount).toBe(1);
expect(usePrestigeStore.getState().insight).toBe(150);
expect(usePrestigeStore.getState().totalInsight).toBe(150);
});
it('should reset loop-specific state', () => {
usePrestigeStore.setState({
defeatedGuardians: [10],
signedPacts: [20],
pactRitualFloor: 10,
pactRitualProgress: 5,
});
usePrestigeStore.getState().startNewLoop(0);
expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]);
expect(usePrestigeStore.getState().signedPacts).toEqual([]);
expect(usePrestigeStore.getState().pactRitualFloor).toBeNull();
});
});
describe('resetPrestigeForNewLoop', () => {
it('should preserve insight and upgrades, reset loop state', () => {
usePrestigeStore.getState().resetPrestigeForNewLoop(200, { manaWell: 2 }, [], 4);
expect(usePrestigeStore.getState().insight).toBe(200);
expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({ manaWell: 2 });
expect(usePrestigeStore.getState().memorySlots).toBe(4);
expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]);
});
});
describe('resetPrestige', () => {
it('should reset everything to initial state', () => {
usePrestigeStore.setState({ insight: 1000, loopCount: 5, prestigeUpgrades: { manaWell: 3 } });
usePrestigeStore.getState().resetPrestige();
expect(usePrestigeStore.getState().insight).toBe(0);
expect(usePrestigeStore.getState().loopCount).toBe(0);
expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({});
});
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// DISCIPLINE STORE
// ═══════════════════════════════════════════════════════════════════════════════
describe('DisciplineStore', () => {
beforeEach(resetDisciplineStore);
describe('activate', () => {
it('should activate raw discipline', () => {
useDisciplineStore.getState().activate('raw-mastery');
expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery');
expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(false);
});
it('should not activate same discipline twice', () => {
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().activate('raw-mastery');
expect(useDisciplineStore.getState().activeIds.filter(id => id === 'raw-mastery').length).toBe(1);
});
it('should not activate when concurrent limit reached', () => {
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().activate('elemental-attunement');
expect(useDisciplineStore.getState().activeIds.length).toBe(1);
});
it('should activate when no prior discipline state (optimistic)', () => {
useDisciplineStore.getState().activate('elemental-attunement', {
elements: { fire: { unlocked: false } },
});
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
});
it('should not activate when existing state has insufficient mana', () => {
useDisciplineStore.setState({
disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false } },
});
useDisciplineStore.getState().activate('raw-mastery', { elements: {} });
expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery');
});
it('should activate when required element is unlocked', () => {
useDisciplineStore.getState().activate('elemental-attunement', {
elements: { fire: { unlocked: true } },
});
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
});
});
describe('deactivate', () => {
it('should remove discipline from active list', () => {
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().deactivate('raw-mastery');
expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery');
});
});
describe('processTick', () => {
it('should accrue XP for active discipline', () => {
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} });
expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(1);
expect(useDisciplineStore.getState().totalXP).toBe(1);
});
it('should drain raw mana for raw discipline', () => {
useDisciplineStore.getState().activate('raw-mastery');
const result = useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} });
expect(result.rawMana).toBeLessThan(1000);
});
it('should pause discipline when insufficient mana', () => {
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} });
expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true);
});
it('should increase concurrent limit at 500 total XP', () => {
useDisciplineStore.setState({ totalXP: 499 });
useDisciplineStore.getState().activate('raw-mastery');
useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} });
expect(useDisciplineStore.getState().totalXP).toBe(500);
expect(useDisciplineStore.getState().concurrentLimit).toBeGreaterThan(1);
});
});
});