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:
2026-04-27 14:12:49 +02:00
parent 6011024e87
commit 70ec7bb3b7

View 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');
}
});
});