test(01-02): RED — compute-applicable-steps spec (D-10)
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.
This commit is contained in:
184
server/src/modules/leveling/lib/compute-applicable-steps.spec.ts
Normal file
184
server/src/modules/leveling/lib/compute-applicable-steps.spec.ts
Normal file
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user