SPB Git

spb/llmindex Public

The discriminative, contamination-resistant, fully transparent LLM ranking — updated live.

TypeScript 77.9% TeX 15.2% Python 3.7% SQL 1.4% JavaScript 1.1% Shell 0.5%
6.1 KB · 152 lines typescript
Raw Blame History
1/**2 * llmindex.io — item bank unit tests3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * License: Proprietary — © Simon-Pierre Boucher, all rights reserved6 */7import { describe, expect, it } from 'vitest';8import {9  TEMPLATES,10  createRng,11  extractAnswer,12  generateBatch,13  gradeAnswer,14  normalizeAnswer,15} from './index';16import { numToFrench, numToSpanish } from './templates/multilingual';17import { IRT_HYPERPARAMS } from '@llmindex/scoring';1819describe('rng', () => {20  it('is deterministic for the same seed', () => {21    const a = createRng('seed-1');22    const b = createRng('seed-1');23    expect([a.next(), a.next()]).toEqual([b.next(), b.next()]);24  });25  it('int stays within bounds', () => {26    const rng = createRng('bounds');27    for (let i = 0; i < 500; i++) {28      const v = rng.int(3, 7);29      expect(v).toBeGreaterThanOrEqual(3);30      expect(v).toBeLessThanOrEqual(7);31    }32  });33});3435describe('answer extraction and grading', () => {36  it('extracts the last ANSWER/CONFIDENCE lines', () => {37    const { answer, confidence } = extractAnswer(38      'Reasoning...\nANSWER: draft\nActually:\nANSWER: 42\nCONFIDENCE: 85',39    );40    expect(answer).toBe('42');41    expect(confidence).toBeCloseTo(0.85);42  });43  it('returns nulls when the format is missing', () => {44    expect(extractAnswer('no structured output')).toEqual({ answer: null, confidence: null });45  });46  it('grades numerically with tolerance and formatting noise', () => {47    expect(gradeAnswer('1,234', '1234', 'numeric')).toBe(true);48    expect(gradeAnswer('$1234.00', '1234', 'numeric')).toBe(true);49    expect(gradeAnswer('1235', '1234', 'numeric')).toBe(false);50  });51  it('grades exact answers after normalization (markdown, latex, hyphens)', () => {52    expect(gradeAnswer('  Canberra.', 'canberra', 'exact')).toBe(true);53    expect(normalizeAnswer('  VingT-Deux ! ')).toBe('vingt deux');54    expect(gradeAnswer('vingt-deux', 'vingt deux', 'exact')).toBe(true);55    expect(gradeAnswer('**42**', '42', 'numeric')).toBe(true);56    expect(gradeAnswer('\\boxed{721}', '721', 'numeric')).toBe(true);57  });58  it('extracts markdown-wrapped and boxed answers', () => {59    expect(extractAnswer('**ANSWER:** 42\nCONFIDENCE: 90').answer).toBe('42');60    expect(extractAnswer('thus $x=7$\n\\boxed{7}\nno tag here').answer).toBe('7');61    expect(extractAnswer('FINAL ANSWER: quatre-vingt-un\nCONFIDENCE: 55').answer).toBe(62      'quatre-vingt-un',63    );64  });65});6667describe('templates', () => {68  it('every template renders a valid, self-consistent item', () => {69    for (const t of TEMPLATES) {70      for (let i = 0; i < 25; i++) {71        const seed = `test:${t.id}:${i}`;72        const item = t.render(createRng(seed), seed);73        expect(item.templateId).toBe(t.id);74        expect(item.prompt.length).toBeGreaterThan(20);75        expect(item.answerKey.length).toBeGreaterThan(0);76        expect(item.prompt).toContain('CONFIDENCE');77        if (item.grading === 'constraints') {78          // key is a machine-checkable spec, not a literal answer79          expect(() => JSON.parse(item.answerKey)).not.toThrow();80        } else {81          // a perfect oracle must grade correct against its own key82          expect(gradeAnswer(item.answerKey, item.answerKey, item.grading)).toBe(true);83        }84      }85    }86  });87  it('constraint-stack specs accept a valid witness and reject violations', () => {88    const spec = {89      wordCount: 5,90      startsWithWord: 'nova',91      endsWithWord: 'ember',92      includeWordExactly: [{ word: 'drift', count: 2 }],93      forbiddenLetter: 'j',94      allLowercase: true,95    };96    const key = JSON.stringify(spec);97    expect(gradeAnswer('nova drift and drift ember', key, 'constraints')).toBe(true);98    expect(gradeAnswer('Nova drift and drift ember', key, 'constraints')).toBe(false); // casing99    expect(gradeAnswer('nova drift drift drift ember', key, 'constraints')).toBe(false); // count100  });101  it('renders are deterministic given a seed and differ across seeds', () => {102    const t = TEMPLATES[0]!;103    const one = t.render(createRng('s1'), 's1');104    const two = t.render(createRng('s1'), 's1');105    const three = t.render(createRng('s2'), 's2');106    expect(one.prompt).toBe(two.prompt);107    expect(one.prompt).not.toBe(three.prompt);108  });109});110111describe('generateBatch', () => {112  it('caps anchors at the methodology maximum', () => {113    const batch = generateBatch({ domain: 'math', n: 100, seed: 'run-x', anchorFraction: 0.9 });114    const anchors = batch.filter((i) => i.isAnchor).length;115    expect(anchors).toBeLessThanOrEqual(100 * IRT_HYPERPARAMS.maxAnchorFraction);116  });117  it('anchor items are stable across different batch seeds', () => {118    const a = generateBatch({ domain: 'math', n: 40, seed: 'run-1' });119    const b = generateBatch({ domain: 'math', n: 40, seed: 'run-2' });120    const anchorsA = a.filter((i) => i.isAnchor).map((i) => i.prompt);121    const anchorsB = b.filter((i) => i.isAnchor).map((i) => i.prompt);122    expect(anchorsA).toEqual(anchorsB);123    const freshA = a.filter((i) => !i.isAnchor).map((i) => i.prompt);124    const freshB = b.filter((i) => !i.isAnchor).map((i) => i.prompt);125    expect(freshA).not.toEqual(freshB);126  });127  it('throws for duel-only domains', () => {128    expect(() => generateBatch({ domain: 'writing', n: 5, seed: 's' })).toThrow();129  });130});131132describe('number words', () => {133  it('French including the irregular 70-99 zone and hundreds', () => {134    expect(numToFrench(21)).toBe('vingt et un');135    expect(numToFrench(37)).toBe('trente-sept');136    expect(numToFrench(71)).toBe('soixante et onze');137    expect(numToFrench(77)).toBe('soixante-dix-sept');138    expect(numToFrench(80)).toBe('quatre-vingts');139    expect(numToFrench(91)).toBe('quatre-vingt-onze');140    expect(numToFrench(100)).toBe('cent');141    expect(numToFrench(200)).toBe('deux cents');142    expect(numToFrench(281)).toBe('deux cent quatre-vingt-un');143  });144  it('Spanish including hundreds', () => {145    expect(numToSpanish(22)).toBe('veintidós');146    expect(numToSpanish(41)).toBe('cuarenta y uno');147    expect(numToSpanish(100)).toBe('cien');148    expect(numToSpanish(500)).toBe('quinientos');149    expect(numToSpanish(731)).toBe('setecientos treinta y uno');150  });151});152