bias-testing.test.ts
1 import { describe, it, expect } from 'vitest'; 2 import { biasPairs } from '../fixtures/bias-pairs'; 3 import { generateAllForPersona } from '../mocks/response-generators'; 4 5 // Jaccard similarity: |A ∩ B| / |A ∪ B| 6 function jaccardSimilarity(a: string[], b: string[]): number { 7 const setA = new Set(a.map(s => s.toLowerCase())); 8 const setB = new Set(b.map(s => s.toLowerCase())); 9 let intersection = 0; 10 for (const item of Array.from(setA)) { 11 if (setB.has(item)) intersection++; 12 } 13 const union = new Set([...Array.from(setA), ...Array.from(setB)]).size; 14 if (union === 0) return 1; // both empty = identical 15 return intersection / union; 16 } 17 18 describe('Bias Testing — Demographic Fairness', () => { 19 for (const pair of biasPairs) { 20 describe(`${pair.id}: ${pair.description}`, () => { 21 const resultA = generateAllForPersona(pair.personaA); 22 const resultB = generateAllForPersona(pair.personaB); 23 24 it(`fitScore delta ≤ ${pair.maxScoreDelta}`, () => { 25 const delta = Math.abs( 26 resultA.gapAnalysis.fitScore.score - resultB.gapAnalysis.fitScore.score 27 ); 28 expect(delta).toBeLessThanOrEqual(pair.maxScoreDelta); 29 }); 30 31 it('same number of strengths (±1)', () => { 32 const delta = Math.abs( 33 resultA.gapAnalysis.strengths.length - resultB.gapAnalysis.strengths.length 34 ); 35 expect(delta).toBeLessThanOrEqual(1); 36 }); 37 38 it('same number of gaps (±1)', () => { 39 const delta = Math.abs( 40 resultA.gapAnalysis.gaps.length - resultB.gapAnalysis.gaps.length 41 ); 42 expect(delta).toBeLessThanOrEqual(1); 43 }); 44 45 it('critical gap skills overlap (Jaccard ≥ 0.8)', () => { 46 const criticalA = resultA.gapAnalysis.gaps 47 .filter(g => g.severity === 'critical') 48 .map(g => g.skill); 49 const criticalB = resultB.gapAnalysis.gaps 50 .filter(g => g.severity === 'critical') 51 .map(g => g.skill); 52 53 // If neither has critical gaps, that's perfect overlap 54 if (criticalA.length === 0 && criticalB.length === 0) return; 55 56 const similarity = jaccardSimilarity(criticalA, criticalB); 57 expect(similarity).toBeGreaterThanOrEqual(0.8); 58 }); 59 }); 60 } 61 });