From 66d9d5cc0abe83f7e4924764a99696d2e9f9e391 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Mon, 27 Apr 2026 14:07:02 +0200 Subject: [PATCH] =?UTF-8?q?test(01-02):=20RED=20=E2=80=94=20prereq-evaluat?= =?UTF-8?q?or=20spec=20(D-01..D-04)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 3 RED phase: 18 failing tests covering prereq DSL evaluation. Coverage: - empty/null inputs (always ok) - skill rank: trained/expert/master/legendary in - disjunctive (comma + or) and conjunctive (semicolon) - bare feat name lookup - heritage / class refs - non-evaluable patterns: spellcasting, deity, age, vision, free-text → {unknown, raw} - German failure reasons asserted Verified failure: module './prereq-evaluator' not found. --- .../leveling/lib/prereq-evaluator.spec.ts | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 server/src/modules/leveling/lib/prereq-evaluator.spec.ts diff --git a/server/src/modules/leveling/lib/prereq-evaluator.spec.ts b/server/src/modules/leveling/lib/prereq-evaluator.spec.ts new file mode 100644 index 0000000..bab4d3e --- /dev/null +++ b/server/src/modules/leveling/lib/prereq-evaluator.spec.ts @@ -0,0 +1,141 @@ +import { evaluatePrereq } from './prereq-evaluator'; +import type { CharacterContext } from './types'; + +function makeCtx(overrides: Partial = {}): CharacterContext { + return { + level: 5, + className: 'Fighter', + ancestryName: 'Human', + heritageName: undefined, + abilities: { STR: 16, DEX: 14, CON: 14, INT: 10, WIS: 12, CHA: 10 }, + skills: {}, + feats: new Set(), + ...overrides, + }; +} + +describe('evaluatePrereq — empty/null', () => { + it('returns ok for null prereq', () => { + expect(evaluatePrereq(null, makeCtx())).toEqual({ ok: true }); + }); + + it('returns ok for empty string', () => { + expect(evaluatePrereq('', makeCtx())).toEqual({ ok: true }); + }); +}); + +describe('evaluatePrereq — skill rank', () => { + it('returns ok when skill rank meets requirement', () => { + const ctx = makeCtx({ skills: { Athletics: 'TRAINED' } }); + expect(evaluatePrereq('Trained in Athletics', ctx)).toEqual({ ok: true }); + }); + + it('returns ok when skill rank exceeds requirement', () => { + const ctx = makeCtx({ skills: { Athletics: 'EXPERT' } }); + expect(evaluatePrereq('Trained in Athletics', ctx)).toEqual({ ok: true }); + }); + + it('returns ok:false with German reason when skill rank below requirement', () => { + const ctx = makeCtx({ skills: { Athletics: 'UNTRAINED' } }); + const result = evaluatePrereq('Trained in Athletics', ctx); + expect(result.ok).toBe(false); + if (result.ok === false) { + expect(result.reason).toMatch(/Athletics/i); + // German wording check — must contain a German keyword + expect(result.reason).toMatch(/(benötig|fehlt|Voraussetzung|mindestens)/); + } + }); + + it('returns ok:false when skill is missing entirely', () => { + const ctx = makeCtx({ skills: {} }); + const result = evaluatePrereq('Trained in Athletics', ctx); + expect(result.ok).toBe(false); + }); +}); + +describe('evaluatePrereq — disjunctive (OR-list)', () => { + it('returns ok when any of the listed skills matches', () => { + const ctx = makeCtx({ skills: { Arcana: 'TRAINED' } }); + expect( + evaluatePrereq('Trained in Arcana, Trained in Nature, or Trained in Religion', ctx), + ).toEqual({ ok: true }); + }); + + it('returns ok:false when no listed skill matches', () => { + const ctx = makeCtx({ skills: { Athletics: 'TRAINED' } }); + const result = evaluatePrereq( + 'Trained in Arcana, Trained in Nature, or Trained in Religion', + ctx, + ); + expect(result.ok).toBe(false); + }); +}); + +describe('evaluatePrereq — conjunctive (semicolon AND)', () => { + it('returns ok when both clauses match', () => { + const ctx = makeCtx({ skills: { Deception: 'TRAINED', Stealth: 'TRAINED' } }); + expect(evaluatePrereq('Trained in Deception; Trained in Stealth', ctx)).toEqual({ + ok: true, + }); + }); + + it('returns ok:false when one clause is missing', () => { + const ctx = makeCtx({ skills: { Deception: 'TRAINED' } }); + const result = evaluatePrereq('Trained in Deception; Trained in Stealth', ctx); + expect(result.ok).toBe(false); + }); +}); + +describe('evaluatePrereq — bare feat name', () => { + it('returns ok when the feat is held', () => { + const ctx = makeCtx({ feats: new Set(['Power Attack']) }); + expect(evaluatePrereq('Power Attack', ctx)).toEqual({ ok: true }); + }); + + it('returns ok:false when the feat is missing', () => { + const ctx = makeCtx({ feats: new Set() }); + const result = evaluatePrereq('Power Attack', ctx); + expect(result.ok).toBe(false); + }); +}); + +describe('evaluatePrereq — heritage', () => { + it('returns ok when heritage matches', () => { + const ctx = makeCtx({ heritageName: 'Unbreakable Goblin' }); + expect(evaluatePrereq('Unbreakable Goblin heritage', ctx)).toEqual({ ok: true }); + }); +}); + +describe('evaluatePrereq — class ref', () => { + it('returns ok when class matches', () => { + const ctx = makeCtx({ className: 'Fighter' }); + expect(evaluatePrereq('Fighter', ctx)).toEqual({ ok: true }); + }); +}); + +describe('evaluatePrereq — non-evaluable patterns (D-02 → unknown)', () => { + it('returns unknown for spellcasting refs', () => { + const result = evaluatePrereq('spellcasting class feature', makeCtx()); + expect(result).toEqual({ unknown: true, raw: 'spellcasting class feature' }); + }); + + it('returns unknown for deity refs', () => { + const result = evaluatePrereq('worshipper of Droskar', makeCtx()); + expect('unknown' in result && result.unknown).toBe(true); + }); + + it('returns unknown for age refs', () => { + const result = evaluatePrereq('at least 100 years old', makeCtx()); + expect('unknown' in result && result.unknown).toBe(true); + }); + + it('returns unknown for vision-trait refs', () => { + const result = evaluatePrereq('low-light vision', makeCtx()); + expect('unknown' in result && result.unknown).toBe(true); + }); + + it('returns unknown for free-text patterns the parser cannot classify', () => { + const result = evaluatePrereq('You worship a god of fire and destruction', makeCtx()); + expect('unknown' in result && result.unknown).toBe(true); + }); +});