From 70ec7bb3b70fe3b0cfdb2b86061d6403bd261281 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Mon, 27 Apr 2026 14:12:49 +0200 Subject: [PATCH] =?UTF-8?q?test(01-02):=20RED=20=E2=80=94=20compute-applic?= =?UTF-8?q?able-steps=20spec=20(D-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 5 RED phase: 13 failing tests covering wizard step ordering. - Fighter L5 exact step list (no FA, no caster) - L4 (boost-skip) and L3 (odd-level) edge cases - L2 has feats but no skill-increase - Free Archetype toggle (D-13) - Spellcaster toggle (D-18) - class-feature-choice when progression has choiceType (D-19) - Invariants: always starts with class-features, always ends with review Verified failure: module './compute-applicable-steps' not found. --- .../lib/compute-applicable-steps.spec.ts | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 server/src/modules/leveling/lib/compute-applicable-steps.spec.ts diff --git a/server/src/modules/leveling/lib/compute-applicable-steps.spec.ts b/server/src/modules/leveling/lib/compute-applicable-steps.spec.ts new file mode 100644 index 0000000..dc65212 --- /dev/null +++ b/server/src/modules/leveling/lib/compute-applicable-steps.spec.ts @@ -0,0 +1,184 @@ +import { computeApplicableSteps } from './compute-applicable-steps'; + +describe('computeApplicableSteps — Fighter (martial, no FA, no caster)', () => { + it('at L5 returns [class-features, boost, skill-increase, feat-class, feat-skill, feat-ancestry, review]', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toEqual([ + 'class-features', + 'boost', + 'skill-increase', + 'feat-class', + 'feat-skill', + 'feat-ancestry', + 'review', + ]); + }); + + it('at L4 (not a boost level) does NOT contain boost', () => { + const steps = computeApplicableSteps({ + targetLevel: 4, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).not.toContain('boost'); + }); + + it('at L4 (even level) contains feat-class AND feat-skill', () => { + const steps = computeApplicableSteps({ + targetLevel: 4, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toContain('feat-class'); + expect(steps).toContain('feat-skill'); + }); + + it('at L3 contains skill-increase AND feat-general but NOT feat-class (odd level)', () => { + const steps = computeApplicableSteps({ + targetLevel: 3, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toContain('skill-increase'); + expect(steps).toContain('feat-general'); + expect(steps).not.toContain('feat-class'); + }); + + it('at L2 (even but no skill-increase yet) contains feat-class+feat-skill but NOT skill-increase', () => { + const steps = computeApplicableSteps({ + targetLevel: 2, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toContain('feat-class'); + expect(steps).toContain('feat-skill'); + expect(steps).not.toContain('skill-increase'); + }); +}); + +describe('computeApplicableSteps — Free Archetype', () => { + it('with FA enabled at L5 includes feat-archetype', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Fighter', + hasFreeArchetype: true, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toContain('feat-archetype'); + }); + + it('without FA never includes feat-archetype', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).not.toContain('feat-archetype'); + }); +}); + +describe('computeApplicableSteps — Spellcaster', () => { + it('with isCaster=true includes spellcaster step', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Wizard', + hasFreeArchetype: false, + isCaster: true, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).toContain('spellcaster'); + }); + + it('with isCaster=false never includes spellcaster step', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).not.toContain('spellcaster'); + }); +}); + +describe('computeApplicableSteps — class-feature-choice (D-19)', () => { + it('includes class-feature-choice when ClassProgression carries choiceType', () => { + const steps = computeApplicableSteps({ + targetLevel: 1, + className: 'Cleric', + hasFreeArchetype: false, + isCaster: true, + isSpontaneousCaster: false, + classProgressionHasChoiceType: true, + }); + expect(steps).toContain('class-feature-choice'); + }); + + it('does NOT include class-feature-choice when no choiceType', () => { + const steps = computeApplicableSteps({ + targetLevel: 5, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps).not.toContain('class-feature-choice'); + }); +}); + +describe('computeApplicableSteps — invariants', () => { + it('always starts with class-features', () => { + for (const level of [1, 2, 3, 5, 10, 15, 20]) { + const steps = computeApplicableSteps({ + targetLevel: level, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps[0]).toBe('class-features'); + } + }); + + it('always ends with review', () => { + for (const level of [1, 2, 3, 5, 10, 15, 20]) { + const steps = computeApplicableSteps({ + targetLevel: level, + className: 'Fighter', + hasFreeArchetype: false, + isCaster: false, + isSpontaneousCaster: false, + classProgressionHasChoiceType: false, + }); + expect(steps[steps.length - 1]).toBe('review'); + } + }); +});