/ __tests__ / integration / bias-testing.test.ts
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  });