import { evaluatePrereq } from './prereq-evaluator'; import type { CharacterContext, EvalResult } from './types'; function isFail(r: EvalResult): r is { ok: false; reason: string } { return 'ok' in r && r.ok === false; } 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(isFail(result)).toBe(true); if (isFail(result)) { 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(isFail(result)).toBe(true); }); }); 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(isFail(result)).toBe(true); }); }); 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(isFail(result)).toBe(true); }); }); 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(isFail(result)).toBe(true); }); }); 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); }); });