/** * llmindex.io — item bank unit tests * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * License: Proprietary — © Simon-Pierre Boucher, all rights reserved */ import { describe, expect, it } from 'vitest'; import { TEMPLATES, createRng, extractAnswer, generateBatch, gradeAnswer, normalizeAnswer, } from './index'; import { numToFrench, numToSpanish } from './templates/multilingual'; import { IRT_HYPERPARAMS } from '@llmindex/scoring'; describe('rng', () => { it('is deterministic for the same seed', () => { const a = createRng('seed-1'); const b = createRng('seed-1'); expect([a.next(), a.next()]).toEqual([b.next(), b.next()]); }); it('int stays within bounds', () => { const rng = createRng('bounds'); for (let i = 0; i < 500; i++) { const v = rng.int(3, 7); expect(v).toBeGreaterThanOrEqual(3); expect(v).toBeLessThanOrEqual(7); } }); }); describe('answer extraction and grading', () => { it('extracts the last ANSWER/CONFIDENCE lines', () => { const { answer, confidence } = extractAnswer( 'Reasoning...\nANSWER: draft\nActually:\nANSWER: 42\nCONFIDENCE: 85', ); expect(answer).toBe('42'); expect(confidence).toBeCloseTo(0.85); }); it('returns nulls when the format is missing', () => { expect(extractAnswer('no structured output')).toEqual({ answer: null, confidence: null }); }); it('grades numerically with tolerance and formatting noise', () => { expect(gradeAnswer('1,234', '1234', 'numeric')).toBe(true); expect(gradeAnswer('$1234.00', '1234', 'numeric')).toBe(true); expect(gradeAnswer('1235', '1234', 'numeric')).toBe(false); }); it('grades exact answers after normalization (markdown, latex, hyphens)', () => { expect(gradeAnswer(' Canberra.', 'canberra', 'exact')).toBe(true); expect(normalizeAnswer(' VingT-Deux ! ')).toBe('vingt deux'); expect(gradeAnswer('vingt-deux', 'vingt deux', 'exact')).toBe(true); expect(gradeAnswer('**42**', '42', 'numeric')).toBe(true); expect(gradeAnswer('\\boxed{721}', '721', 'numeric')).toBe(true); }); it('extracts markdown-wrapped and boxed answers', () => { expect(extractAnswer('**ANSWER:** 42\nCONFIDENCE: 90').answer).toBe('42'); expect(extractAnswer('thus $x=7$\n\\boxed{7}\nno tag here').answer).toBe('7'); expect(extractAnswer('FINAL ANSWER: quatre-vingt-un\nCONFIDENCE: 55').answer).toBe( 'quatre-vingt-un', ); }); }); describe('templates', () => { it('every template renders a valid, self-consistent item', () => { for (const t of TEMPLATES) { for (let i = 0; i < 25; i++) { const seed = `test:${t.id}:${i}`; const item = t.render(createRng(seed), seed); expect(item.templateId).toBe(t.id); expect(item.prompt.length).toBeGreaterThan(20); expect(item.answerKey.length).toBeGreaterThan(0); expect(item.prompt).toContain('CONFIDENCE'); if (item.grading === 'constraints') { // key is a machine-checkable spec, not a literal answer expect(() => JSON.parse(item.answerKey)).not.toThrow(); } else { // a perfect oracle must grade correct against its own key expect(gradeAnswer(item.answerKey, item.answerKey, item.grading)).toBe(true); } } } }); it('constraint-stack specs accept a valid witness and reject violations', () => { const spec = { wordCount: 5, startsWithWord: 'nova', endsWithWord: 'ember', includeWordExactly: [{ word: 'drift', count: 2 }], forbiddenLetter: 'j', allLowercase: true, }; const key = JSON.stringify(spec); expect(gradeAnswer('nova drift and drift ember', key, 'constraints')).toBe(true); expect(gradeAnswer('Nova drift and drift ember', key, 'constraints')).toBe(false); // casing expect(gradeAnswer('nova drift drift drift ember', key, 'constraints')).toBe(false); // count }); it('renders are deterministic given a seed and differ across seeds', () => { const t = TEMPLATES[0]!; const one = t.render(createRng('s1'), 's1'); const two = t.render(createRng('s1'), 's1'); const three = t.render(createRng('s2'), 's2'); expect(one.prompt).toBe(two.prompt); expect(one.prompt).not.toBe(three.prompt); }); }); describe('generateBatch', () => { it('caps anchors at the methodology maximum', () => { const batch = generateBatch({ domain: 'math', n: 100, seed: 'run-x', anchorFraction: 0.9 }); const anchors = batch.filter((i) => i.isAnchor).length; expect(anchors).toBeLessThanOrEqual(100 * IRT_HYPERPARAMS.maxAnchorFraction); }); it('anchor items are stable across different batch seeds', () => { const a = generateBatch({ domain: 'math', n: 40, seed: 'run-1' }); const b = generateBatch({ domain: 'math', n: 40, seed: 'run-2' }); const anchorsA = a.filter((i) => i.isAnchor).map((i) => i.prompt); const anchorsB = b.filter((i) => i.isAnchor).map((i) => i.prompt); expect(anchorsA).toEqual(anchorsB); const freshA = a.filter((i) => !i.isAnchor).map((i) => i.prompt); const freshB = b.filter((i) => !i.isAnchor).map((i) => i.prompt); expect(freshA).not.toEqual(freshB); }); it('throws for duel-only domains', () => { expect(() => generateBatch({ domain: 'writing', n: 5, seed: 's' })).toThrow(); }); }); describe('number words', () => { it('French including the irregular 70-99 zone and hundreds', () => { expect(numToFrench(21)).toBe('vingt et un'); expect(numToFrench(37)).toBe('trente-sept'); expect(numToFrench(71)).toBe('soixante et onze'); expect(numToFrench(77)).toBe('soixante-dix-sept'); expect(numToFrench(80)).toBe('quatre-vingts'); expect(numToFrench(91)).toBe('quatre-vingt-onze'); expect(numToFrench(100)).toBe('cent'); expect(numToFrench(200)).toBe('deux cents'); expect(numToFrench(281)).toBe('deux cent quatre-vingt-un'); }); it('Spanish including hundreds', () => { expect(numToSpanish(22)).toBe('veintidós'); expect(numToSpanish(41)).toBe('cuarenta y uno'); expect(numToSpanish(100)).toBe('cien'); expect(numToSpanish(500)).toBe('quinientos'); expect(numToSpanish(731)).toBe('setecientos treinta y uno'); }); });