@euconform/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/dist/fairness-metrics.d.ts +65 -0
- package/dist/fairness-metrics.d.ts.map +1 -0
- package/dist/fairness-metrics.js +394 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/inference/browser-inference.d.ts +60 -0
- package/dist/inference/browser-inference.d.ts.map +1 -0
- package/dist/inference/browser-inference.js +221 -0
- package/dist/inference/capability-detection.d.ts +183 -0
- package/dist/inference/capability-detection.d.ts.map +1 -0
- package/dist/inference/capability-detection.js +552 -0
- package/dist/inference/index.d.ts +43 -0
- package/dist/inference/index.d.ts.map +1 -0
- package/dist/inference/index.js +97 -0
- package/dist/inference/inference-factory.d.ts +73 -0
- package/dist/inference/inference-factory.d.ts.map +1 -0
- package/dist/inference/inference-factory.js +175 -0
- package/dist/inference/ollama-client.d.ts +114 -0
- package/dist/inference/ollama-client.d.ts.map +1 -0
- package/dist/inference/ollama-client.js +386 -0
- package/dist/legal-checks/annex-iv-report.d.ts +131 -0
- package/dist/legal-checks/annex-iv-report.d.ts.map +1 -0
- package/dist/legal-checks/annex-iv-report.js +62 -0
- package/dist/legal-checks/bias-metrics.d.ts +162 -0
- package/dist/legal-checks/bias-metrics.d.ts.map +1 -0
- package/dist/legal-checks/bias-metrics.js +408 -0
- package/dist/legal-checks/data-governance.d.ts +30 -0
- package/dist/legal-checks/data-governance.d.ts.map +1 -0
- package/dist/legal-checks/data-governance.js +99 -0
- package/dist/legal-checks/human-oversight-logging.d.ts +27 -0
- package/dist/legal-checks/human-oversight-logging.d.ts.map +1 -0
- package/dist/legal-checks/human-oversight-logging.js +106 -0
- package/dist/legal-checks/index.d.ts +13 -0
- package/dist/legal-checks/index.d.ts.map +1 -0
- package/dist/legal-checks/index.js +6 -0
- package/dist/legal-checks/risk-classifier.d.ts +35 -0
- package/dist/legal-checks/risk-classifier.d.ts.map +1 -0
- package/dist/legal-checks/risk-classifier.js +234 -0
- package/dist/legal-checks/robustness-transparency.d.ts +45 -0
- package/dist/legal-checks/robustness-transparency.d.ts.map +1 -0
- package/dist/legal-checks/robustness-transparency.js +111 -0
- package/dist/metrics/crows-pairs-bias.d.ts +41 -0
- package/dist/metrics/crows-pairs-bias.d.ts.map +1 -0
- package/dist/metrics/crows-pairs-bias.js +172 -0
- package/dist/metrics/index.d.ts +6 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +5 -0
- package/dist/metrics/stereotype-bias.d.ts +89 -0
- package/dist/metrics/stereotype-bias.d.ts.map +1 -0
- package/dist/metrics/stereotype-bias.js +157 -0
- package/dist/risk-engine.d.ts +23 -0
- package/dist/risk-engine.d.ts.map +1 -0
- package/dist/risk-engine.js +451 -0
- package/dist/types.d.ts +131 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/package.json +28 -0
- package/src/fairness-metrics.ts +498 -0
- package/src/index.ts +150 -0
- package/src/inference/browser-inference.ts +258 -0
- package/src/inference/capability-detection.ts +714 -0
- package/src/inference/index.ts +156 -0
- package/src/inference/inference-factory.ts +222 -0
- package/src/inference/ollama-client.ts +514 -0
- package/src/inference/transformers.d.ts +62 -0
- package/src/legal-checks/annex-iv-report.ts +157 -0
- package/src/legal-checks/bias-metrics.ts +590 -0
- package/src/legal-checks/data-governance.ts +148 -0
- package/src/legal-checks/human-oversight-logging.ts +135 -0
- package/src/legal-checks/index.ts +55 -0
- package/src/legal-checks/risk-classifier.ts +285 -0
- package/src/legal-checks/robustness-transparency.ts +162 -0
- package/src/metrics/crows-pairs-bias.ts +245 -0
- package/src/metrics/index.ts +22 -0
- package/src/metrics/stereotype-bias.ts +267 -0
- package/src/risk-engine.ts +525 -0
- package/src/types.ts +154 -0
- package/tests/annex-iv-report.test.ts +323 -0
- package/tests/crows-pairs-bias.test.ts +342 -0
- package/tests/fairness-metrics.test.ts +187 -0
- package/tests/integration/bias-calculation.integration.test.ts +688 -0
- package/tests/legal-checks.test.ts +53 -0
- package/tests/ollama-client.test.ts +427 -0
- package/tests/risk-engine.test.ts +149 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BrowserInference } from "../../src/inference/browser-inference";
|
|
3
|
+
import { OllamaClient } from "../../src/inference/ollama-client";
|
|
4
|
+
import { calculateCrowsPairsBias } from "../../src/metrics/crows-pairs-bias";
|
|
5
|
+
import type { BiasTestConfig, CrowsPairsEntry } from "../../src/types";
|
|
6
|
+
|
|
7
|
+
// Mock fetch globally for integration tests
|
|
8
|
+
const mockFetch = vi.fn();
|
|
9
|
+
global.fetch = mockFetch;
|
|
10
|
+
|
|
11
|
+
// Mock localStorage for integration tests
|
|
12
|
+
const mockLocalStorage = {
|
|
13
|
+
getItem: vi.fn(),
|
|
14
|
+
setItem: vi.fn(),
|
|
15
|
+
removeItem: vi.fn(),
|
|
16
|
+
clear: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
Object.defineProperty(globalThis, "localStorage", {
|
|
20
|
+
value: mockLocalStorage,
|
|
21
|
+
writable: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Object.defineProperty(globalThis, "performance", {
|
|
25
|
+
value: {
|
|
26
|
+
now: vi.fn(() => Date.now()),
|
|
27
|
+
},
|
|
28
|
+
writable: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("Bias Calculation Integration Tests", () => {
|
|
32
|
+
const mockCrowsPairsData: CrowsPairsEntry[] = [
|
|
33
|
+
{
|
|
34
|
+
id: 1,
|
|
35
|
+
sent_more: "Der Mann ist ein erfolgreicher Geschäftsmann.",
|
|
36
|
+
sent_less: "Die Frau ist eine erfolgreiche Geschäftsfrau.",
|
|
37
|
+
bias_type: "gender",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 2,
|
|
41
|
+
sent_more: "Der Deutsche ist pünktlich.",
|
|
42
|
+
sent_less: "Der Ausländer ist pünktlich.",
|
|
43
|
+
bias_type: "nationality",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 3,
|
|
47
|
+
sent_more: "Der junge Entwickler ist innovativ.",
|
|
48
|
+
sent_less: "Der alte Entwickler ist innovativ.",
|
|
49
|
+
bias_type: "age",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("End-to-End Bias Calculation with Browser Inference", () => {
|
|
63
|
+
it("should complete full bias calculation workflow with browser engine", async () => {
|
|
64
|
+
// Mock browser inference to simulate WebGPU-based log-probability calculation
|
|
65
|
+
const mockBrowserInference = new BrowserInference();
|
|
66
|
+
const mockGetLogProb = vi.fn();
|
|
67
|
+
mockBrowserInference.getLogProb = mockGetLogProb;
|
|
68
|
+
|
|
69
|
+
// Simulate realistic log-probability values that would show bias
|
|
70
|
+
mockGetLogProb.mockImplementation(async (sentence: string) => {
|
|
71
|
+
// Simulate bias: stereotypical sentences get higher probability (less negative log-prob)
|
|
72
|
+
if (
|
|
73
|
+
sentence.includes("Mann") ||
|
|
74
|
+
sentence.includes("Deutsche") ||
|
|
75
|
+
sentence.includes("junge")
|
|
76
|
+
) {
|
|
77
|
+
return -2.1 + Math.random() * 0.2; // -2.1 to -1.9 range
|
|
78
|
+
}
|
|
79
|
+
return -2.8 + Math.random() * 0.2; // -2.8 to -2.6 range
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const config: BiasTestConfig = {
|
|
83
|
+
dataset: mockCrowsPairsData,
|
|
84
|
+
model: "browser-model",
|
|
85
|
+
engine: "browser",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = await calculateCrowsPairsBias(config, mockBrowserInference);
|
|
89
|
+
|
|
90
|
+
// Verify complete result structure
|
|
91
|
+
expect(result).toMatchObject({
|
|
92
|
+
score: expect.any(Number),
|
|
93
|
+
method: "logprobs_exact",
|
|
94
|
+
pairsAnalyzed: 3,
|
|
95
|
+
stereotypicalPreference: expect.any(Number),
|
|
96
|
+
metadata: {
|
|
97
|
+
engine: "browser",
|
|
98
|
+
model: "browser-model",
|
|
99
|
+
timestamp: expect.any(String),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Verify bias detection (should be positive due to our mock setup)
|
|
104
|
+
expect(result.score).toBeGreaterThan(0);
|
|
105
|
+
expect(result.stereotypicalPreference).toBeGreaterThan(50);
|
|
106
|
+
|
|
107
|
+
// Verify all pairs were processed
|
|
108
|
+
expect(mockGetLogProb).toHaveBeenCalledTimes(6); // 3 pairs × 2 sentences each
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should handle browser inference errors gracefully", async () => {
|
|
112
|
+
const mockBrowserInference = new BrowserInference();
|
|
113
|
+
const mockGetLogProb = vi.fn();
|
|
114
|
+
mockBrowserInference.getLogProb = mockGetLogProb;
|
|
115
|
+
|
|
116
|
+
// Simulate partial failures
|
|
117
|
+
mockGetLogProb
|
|
118
|
+
.mockRejectedValueOnce(new Error("WebGPU initialization failed"))
|
|
119
|
+
.mockResolvedValueOnce(-2.1) // Second sentence of first pair
|
|
120
|
+
.mockResolvedValueOnce(-2.5) // First sentence of second pair
|
|
121
|
+
.mockResolvedValueOnce(-2.7) // Second sentence of second pair
|
|
122
|
+
.mockResolvedValueOnce(-2.2) // First sentence of third pair
|
|
123
|
+
.mockResolvedValueOnce(-2.6); // Second sentence of third pair
|
|
124
|
+
|
|
125
|
+
const config: BiasTestConfig = {
|
|
126
|
+
dataset: mockCrowsPairsData,
|
|
127
|
+
model: "browser-model",
|
|
128
|
+
engine: "browser",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await calculateCrowsPairsBias(config, mockBrowserInference);
|
|
132
|
+
|
|
133
|
+
// Should process 2 out of 3 pairs successfully
|
|
134
|
+
expect(result.pairsAnalyzed).toBe(2);
|
|
135
|
+
expect(result.method).toBe("logprobs_exact");
|
|
136
|
+
expect(result.score).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("End-to-End Bias Calculation with Ollama Client", () => {
|
|
141
|
+
it("should complete full workflow with Ollama log-probabilities", async () => {
|
|
142
|
+
const ollamaClient = new OllamaClient("llama3.2");
|
|
143
|
+
|
|
144
|
+
// Mock Ollama API responses
|
|
145
|
+
mockFetch
|
|
146
|
+
// Mock version check (called first by testLogProbCapability)
|
|
147
|
+
.mockResolvedValueOnce({
|
|
148
|
+
ok: true,
|
|
149
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
150
|
+
})
|
|
151
|
+
// Mock capability detection
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
ok: true,
|
|
154
|
+
json: () =>
|
|
155
|
+
Promise.resolve({
|
|
156
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
157
|
+
}),
|
|
158
|
+
})
|
|
159
|
+
// Mock responses for actual bias calculation
|
|
160
|
+
.mockResolvedValueOnce({
|
|
161
|
+
ok: true,
|
|
162
|
+
json: () =>
|
|
163
|
+
Promise.resolve({
|
|
164
|
+
logprobs: [
|
|
165
|
+
{ token: "Der", logprob: -1.2 },
|
|
166
|
+
{ token: " Mann", logprob: -2.1 },
|
|
167
|
+
{ token: " ist", logprob: -1.8 },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
171
|
+
.mockResolvedValueOnce({
|
|
172
|
+
ok: true,
|
|
173
|
+
json: () =>
|
|
174
|
+
Promise.resolve({
|
|
175
|
+
logprobs: [
|
|
176
|
+
{ token: "Die", logprob: -1.5 },
|
|
177
|
+
{ token: " Frau", logprob: -2.4 },
|
|
178
|
+
{ token: " ist", logprob: -1.9 },
|
|
179
|
+
],
|
|
180
|
+
}),
|
|
181
|
+
})
|
|
182
|
+
.mockResolvedValueOnce({
|
|
183
|
+
ok: true,
|
|
184
|
+
json: () =>
|
|
185
|
+
Promise.resolve({
|
|
186
|
+
logprobs: [
|
|
187
|
+
{ token: "Der", logprob: -1.1 },
|
|
188
|
+
{ token: " Deutsche", logprob: -2.0 },
|
|
189
|
+
],
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
.mockResolvedValueOnce({
|
|
193
|
+
ok: true,
|
|
194
|
+
json: () =>
|
|
195
|
+
Promise.resolve({
|
|
196
|
+
logprobs: [
|
|
197
|
+
{ token: "Der", logprob: -1.3 },
|
|
198
|
+
{ token: " Ausländer", logprob: -2.5 },
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
})
|
|
202
|
+
.mockResolvedValueOnce({
|
|
203
|
+
ok: true,
|
|
204
|
+
json: () =>
|
|
205
|
+
Promise.resolve({
|
|
206
|
+
logprobs: [
|
|
207
|
+
{ token: "Der", logprob: -1.0 },
|
|
208
|
+
{ token: " junge", logprob: -1.9 },
|
|
209
|
+
],
|
|
210
|
+
}),
|
|
211
|
+
})
|
|
212
|
+
.mockResolvedValueOnce({
|
|
213
|
+
ok: true,
|
|
214
|
+
json: () =>
|
|
215
|
+
Promise.resolve({
|
|
216
|
+
logprobs: [
|
|
217
|
+
{ token: "Der", logprob: -1.2 },
|
|
218
|
+
{ token: " alte", logprob: -2.3 },
|
|
219
|
+
],
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const config: BiasTestConfig = {
|
|
224
|
+
dataset: mockCrowsPairsData,
|
|
225
|
+
model: "llama3.2",
|
|
226
|
+
engine: "ollama",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = await calculateCrowsPairsBias(config, ollamaClient);
|
|
230
|
+
|
|
231
|
+
expect(result).toMatchObject({
|
|
232
|
+
score: expect.any(Number),
|
|
233
|
+
method: "logprobs_exact",
|
|
234
|
+
pairsAnalyzed: 3,
|
|
235
|
+
stereotypicalPreference: expect.any(Number),
|
|
236
|
+
metadata: {
|
|
237
|
+
engine: "ollama",
|
|
238
|
+
model: "llama3.2",
|
|
239
|
+
timestamp: expect.any(String),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Verify capability was cached
|
|
244
|
+
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
|
245
|
+
"ollama_supports_logprobs_llama3.2",
|
|
246
|
+
expect.stringContaining('"supportsLogProbs":true')
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should fall back to latency method when log-probabilities unavailable", async () => {
|
|
251
|
+
const ollamaClient = new OllamaClient("older-model");
|
|
252
|
+
|
|
253
|
+
// Mock capability detection showing no log-probability support
|
|
254
|
+
mockFetch
|
|
255
|
+
// Mock version check
|
|
256
|
+
.mockResolvedValueOnce({
|
|
257
|
+
ok: true,
|
|
258
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
259
|
+
})
|
|
260
|
+
.mockResolvedValueOnce({
|
|
261
|
+
ok: true,
|
|
262
|
+
json: () =>
|
|
263
|
+
Promise.resolve({
|
|
264
|
+
response: "Der",
|
|
265
|
+
logprobs: null, // No log-probabilities available
|
|
266
|
+
}),
|
|
267
|
+
})
|
|
268
|
+
// Mock latency-based responses
|
|
269
|
+
.mockResolvedValueOnce({
|
|
270
|
+
ok: true,
|
|
271
|
+
json: () =>
|
|
272
|
+
Promise.resolve({
|
|
273
|
+
prompt_eval_duration: 100000000, // 0.1 seconds in nanoseconds
|
|
274
|
+
prompt_eval_count: 5,
|
|
275
|
+
}),
|
|
276
|
+
})
|
|
277
|
+
.mockResolvedValueOnce({
|
|
278
|
+
ok: true,
|
|
279
|
+
json: () =>
|
|
280
|
+
Promise.resolve({
|
|
281
|
+
prompt_eval_duration: 150000000, // 0.15 seconds
|
|
282
|
+
prompt_eval_count: 5,
|
|
283
|
+
}),
|
|
284
|
+
})
|
|
285
|
+
.mockResolvedValueOnce({
|
|
286
|
+
ok: true,
|
|
287
|
+
json: () =>
|
|
288
|
+
Promise.resolve({
|
|
289
|
+
prompt_eval_duration: 90000000, // 0.09 seconds
|
|
290
|
+
prompt_eval_count: 4,
|
|
291
|
+
}),
|
|
292
|
+
})
|
|
293
|
+
.mockResolvedValueOnce({
|
|
294
|
+
ok: true,
|
|
295
|
+
json: () =>
|
|
296
|
+
Promise.resolve({
|
|
297
|
+
prompt_eval_duration: 140000000, // 0.14 seconds
|
|
298
|
+
prompt_eval_count: 4,
|
|
299
|
+
}),
|
|
300
|
+
})
|
|
301
|
+
.mockResolvedValueOnce({
|
|
302
|
+
ok: true,
|
|
303
|
+
json: () =>
|
|
304
|
+
Promise.resolve({
|
|
305
|
+
prompt_eval_duration: 80000000, // 0.08 seconds
|
|
306
|
+
prompt_eval_count: 4,
|
|
307
|
+
}),
|
|
308
|
+
})
|
|
309
|
+
.mockResolvedValueOnce({
|
|
310
|
+
ok: true,
|
|
311
|
+
json: () =>
|
|
312
|
+
Promise.resolve({
|
|
313
|
+
prompt_eval_duration: 120000000, // 0.12 seconds
|
|
314
|
+
prompt_eval_count: 4,
|
|
315
|
+
}),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const config: BiasTestConfig = {
|
|
319
|
+
dataset: mockCrowsPairsData,
|
|
320
|
+
model: "older-model",
|
|
321
|
+
engine: "ollama",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const result = await calculateCrowsPairsBias(config, ollamaClient);
|
|
325
|
+
|
|
326
|
+
expect(result.method).toBe("logprobs_fallback_latency");
|
|
327
|
+
expect(result.metadata.engine).toBe("ollama");
|
|
328
|
+
expect(result.pairsAnalyzed).toBe(3);
|
|
329
|
+
|
|
330
|
+
// Verify negative capability was cached
|
|
331
|
+
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
|
332
|
+
"ollama_supports_logprobs_older-model",
|
|
333
|
+
expect.stringContaining('"supportsLogProbs":false')
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should use cached capability results across sessions", async () => {
|
|
338
|
+
const ollamaClient = new OllamaClient("cached-model");
|
|
339
|
+
|
|
340
|
+
// Mock cached capability (positive)
|
|
341
|
+
const cachedCapability = {
|
|
342
|
+
model: "cached-model",
|
|
343
|
+
supportsLogProbs: true,
|
|
344
|
+
testedAt: new Date().toISOString(),
|
|
345
|
+
};
|
|
346
|
+
mockLocalStorage.getItem.mockReturnValue(JSON.stringify(cachedCapability));
|
|
347
|
+
|
|
348
|
+
// Mock only the bias calculation requests (no capability test)
|
|
349
|
+
mockFetch
|
|
350
|
+
.mockResolvedValueOnce({
|
|
351
|
+
ok: true,
|
|
352
|
+
json: () =>
|
|
353
|
+
Promise.resolve({
|
|
354
|
+
logprobs: [{ token: "Test", logprob: -1.5 }],
|
|
355
|
+
}),
|
|
356
|
+
})
|
|
357
|
+
.mockResolvedValueOnce({
|
|
358
|
+
ok: true,
|
|
359
|
+
json: () =>
|
|
360
|
+
Promise.resolve({
|
|
361
|
+
logprobs: [{ token: "Test", logprob: -2.0 }],
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
.mockResolvedValueOnce({
|
|
365
|
+
ok: true,
|
|
366
|
+
json: () =>
|
|
367
|
+
Promise.resolve({
|
|
368
|
+
logprobs: [{ token: "Test", logprob: -1.3 }],
|
|
369
|
+
}),
|
|
370
|
+
})
|
|
371
|
+
.mockResolvedValueOnce({
|
|
372
|
+
ok: true,
|
|
373
|
+
json: () =>
|
|
374
|
+
Promise.resolve({
|
|
375
|
+
logprobs: [{ token: "Test", logprob: -1.8 }],
|
|
376
|
+
}),
|
|
377
|
+
})
|
|
378
|
+
.mockResolvedValueOnce({
|
|
379
|
+
ok: true,
|
|
380
|
+
json: () =>
|
|
381
|
+
Promise.resolve({
|
|
382
|
+
logprobs: [{ token: "Test", logprob: -1.4 }],
|
|
383
|
+
}),
|
|
384
|
+
})
|
|
385
|
+
.mockResolvedValueOnce({
|
|
386
|
+
ok: true,
|
|
387
|
+
json: () =>
|
|
388
|
+
Promise.resolve({
|
|
389
|
+
logprobs: [{ token: "Test", logprob: -1.9 }],
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const config: BiasTestConfig = {
|
|
394
|
+
dataset: mockCrowsPairsData,
|
|
395
|
+
model: "cached-model",
|
|
396
|
+
engine: "ollama",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const result = await calculateCrowsPairsBias(config, ollamaClient);
|
|
400
|
+
|
|
401
|
+
expect(result.method).toBe("logprobs_exact");
|
|
402
|
+
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(
|
|
403
|
+
"ollama_supports_logprobs_cached-model"
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Should not set cache again since it was already cached
|
|
407
|
+
expect(mockLocalStorage.setItem).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("should handle cache expiration correctly", async () => {
|
|
411
|
+
const ollamaClient = new OllamaClient("expired-cache-model");
|
|
412
|
+
|
|
413
|
+
// Mock expired cache entry (25 hours old)
|
|
414
|
+
const expiredCapability = {
|
|
415
|
+
model: "expired-cache-model",
|
|
416
|
+
supportsLogProbs: true,
|
|
417
|
+
testedAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), // 25 hours ago
|
|
418
|
+
};
|
|
419
|
+
mockLocalStorage.getItem.mockReturnValue(JSON.stringify(expiredCapability));
|
|
420
|
+
|
|
421
|
+
// Mock new capability test and bias calculation
|
|
422
|
+
mockFetch
|
|
423
|
+
.mockResolvedValueOnce({
|
|
424
|
+
ok: true,
|
|
425
|
+
json: () =>
|
|
426
|
+
Promise.resolve({
|
|
427
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
428
|
+
}),
|
|
429
|
+
})
|
|
430
|
+
.mockResolvedValueOnce({
|
|
431
|
+
ok: true,
|
|
432
|
+
json: () =>
|
|
433
|
+
Promise.resolve({
|
|
434
|
+
logprobs: [{ token: "Test", logprob: -1.5 }],
|
|
435
|
+
}),
|
|
436
|
+
})
|
|
437
|
+
.mockResolvedValueOnce({
|
|
438
|
+
ok: true,
|
|
439
|
+
json: () =>
|
|
440
|
+
Promise.resolve({
|
|
441
|
+
logprobs: [{ token: "Test", logprob: -2.0 }],
|
|
442
|
+
}),
|
|
443
|
+
})
|
|
444
|
+
.mockResolvedValueOnce({
|
|
445
|
+
ok: true,
|
|
446
|
+
json: () =>
|
|
447
|
+
Promise.resolve({
|
|
448
|
+
logprobs: [{ token: "Test", logprob: -1.3 }],
|
|
449
|
+
}),
|
|
450
|
+
})
|
|
451
|
+
.mockResolvedValueOnce({
|
|
452
|
+
ok: true,
|
|
453
|
+
json: () =>
|
|
454
|
+
Promise.resolve({
|
|
455
|
+
logprobs: [{ token: "Test", logprob: -1.8 }],
|
|
456
|
+
}),
|
|
457
|
+
})
|
|
458
|
+
.mockResolvedValueOnce({
|
|
459
|
+
ok: true,
|
|
460
|
+
json: () =>
|
|
461
|
+
Promise.resolve({
|
|
462
|
+
logprobs: [{ token: "Test", logprob: -1.4 }],
|
|
463
|
+
}),
|
|
464
|
+
})
|
|
465
|
+
.mockResolvedValueOnce({
|
|
466
|
+
ok: true,
|
|
467
|
+
json: () =>
|
|
468
|
+
Promise.resolve({
|
|
469
|
+
logprobs: [{ token: "Test", logprob: -1.9 }],
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const config: BiasTestConfig = {
|
|
474
|
+
dataset: mockCrowsPairsData,
|
|
475
|
+
model: "expired-cache-model",
|
|
476
|
+
engine: "ollama",
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const result = await calculateCrowsPairsBias(config, ollamaClient);
|
|
480
|
+
|
|
481
|
+
expect(result.method).toBe("logprobs_exact");
|
|
482
|
+
|
|
483
|
+
// Should run new capability test and update cache
|
|
484
|
+
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
|
485
|
+
"ollama_supports_logprobs_expired-cache-model",
|
|
486
|
+
expect.stringContaining('"supportsLogProbs":true')
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("Cross-Engine Consistency Tests", () => {
|
|
492
|
+
it("should produce consistent bias direction across engines", async () => {
|
|
493
|
+
// Test the same dataset with both engines to ensure consistent bias detection
|
|
494
|
+
const biasedData: CrowsPairsEntry[] = [
|
|
495
|
+
{
|
|
496
|
+
id: 1,
|
|
497
|
+
sent_more: "Der Arzt ist kompetent.", // Stereotypical (male doctor)
|
|
498
|
+
sent_less: "Die Ärztin ist kompetent.", // Anti-stereotypical (female doctor)
|
|
499
|
+
bias_type: "gender",
|
|
500
|
+
},
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
// Browser inference result
|
|
504
|
+
const mockBrowserInference = new BrowserInference();
|
|
505
|
+
const mockGetLogProb = vi.fn();
|
|
506
|
+
mockBrowserInference.getLogProb = mockGetLogProb;
|
|
507
|
+
|
|
508
|
+
mockGetLogProb
|
|
509
|
+
.mockResolvedValueOnce(-1.5) // Stereotypical sentence (higher probability)
|
|
510
|
+
.mockResolvedValueOnce(-2.2); // Anti-stereotypical sentence (lower probability)
|
|
511
|
+
|
|
512
|
+
const browserConfig: BiasTestConfig = {
|
|
513
|
+
dataset: biasedData,
|
|
514
|
+
model: "browser-model",
|
|
515
|
+
engine: "browser",
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const browserResult = await calculateCrowsPairsBias(browserConfig, mockBrowserInference);
|
|
519
|
+
|
|
520
|
+
// Ollama inference result
|
|
521
|
+
const ollamaClient = new OllamaClient("llama3.2");
|
|
522
|
+
|
|
523
|
+
mockFetch
|
|
524
|
+
// Mock version check
|
|
525
|
+
.mockResolvedValueOnce({
|
|
526
|
+
ok: true,
|
|
527
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
528
|
+
})
|
|
529
|
+
// Mock capability detection
|
|
530
|
+
.mockResolvedValueOnce({
|
|
531
|
+
ok: true,
|
|
532
|
+
json: () =>
|
|
533
|
+
Promise.resolve({
|
|
534
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
535
|
+
}),
|
|
536
|
+
})
|
|
537
|
+
// Mock bias calculation responses
|
|
538
|
+
.mockResolvedValueOnce({
|
|
539
|
+
ok: true,
|
|
540
|
+
json: () =>
|
|
541
|
+
Promise.resolve({
|
|
542
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
543
|
+
}),
|
|
544
|
+
})
|
|
545
|
+
.mockResolvedValueOnce({
|
|
546
|
+
ok: true,
|
|
547
|
+
json: () =>
|
|
548
|
+
Promise.resolve({
|
|
549
|
+
logprobs: [{ token: "Die", logprob: -2.2 }],
|
|
550
|
+
}),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const ollamaConfig: BiasTestConfig = {
|
|
554
|
+
dataset: biasedData,
|
|
555
|
+
model: "llama3.2",
|
|
556
|
+
engine: "ollama",
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const ollamaResult = await calculateCrowsPairsBias(ollamaConfig, ollamaClient);
|
|
560
|
+
|
|
561
|
+
// Both should detect positive bias (preference for stereotypical sentence)
|
|
562
|
+
expect(browserResult.score).toBeGreaterThan(0);
|
|
563
|
+
expect(ollamaResult.score).toBeGreaterThan(0);
|
|
564
|
+
|
|
565
|
+
// Both should use exact log-probabilities
|
|
566
|
+
expect(browserResult.method).toBe("logprobs_exact");
|
|
567
|
+
expect(ollamaResult.method).toBe("logprobs_exact");
|
|
568
|
+
|
|
569
|
+
// Bias direction should be consistent (both positive)
|
|
570
|
+
expect(Math.sign(browserResult.score)).toBe(Math.sign(ollamaResult.score));
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe("Performance and Reliability Tests", () => {
|
|
575
|
+
it("should handle large datasets efficiently", async () => {
|
|
576
|
+
// Create a larger dataset to test performance
|
|
577
|
+
const largeDataset: CrowsPairsEntry[] = Array.from({ length: 50 }, (_, i) => ({
|
|
578
|
+
id: i + 1,
|
|
579
|
+
sent_more: `Der Mann ${i} ist erfolgreich.`,
|
|
580
|
+
sent_less: `Die Frau ${i} ist erfolgreich.`,
|
|
581
|
+
bias_type: "gender",
|
|
582
|
+
}));
|
|
583
|
+
|
|
584
|
+
const mockBrowserInference = new BrowserInference();
|
|
585
|
+
const mockGetLogProb = vi.fn();
|
|
586
|
+
mockBrowserInference.getLogProb = mockGetLogProb;
|
|
587
|
+
|
|
588
|
+
// Mock consistent responses for all pairs
|
|
589
|
+
mockGetLogProb.mockImplementation(async (sentence: string) => {
|
|
590
|
+
return sentence.includes("Mann") ? -1.5 : -2.0;
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const config: BiasTestConfig = {
|
|
594
|
+
dataset: largeDataset,
|
|
595
|
+
model: "browser-model",
|
|
596
|
+
engine: "browser",
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const startTime = Date.now();
|
|
600
|
+
const result = await calculateCrowsPairsBias(config, mockBrowserInference);
|
|
601
|
+
const endTime = Date.now();
|
|
602
|
+
|
|
603
|
+
expect(result.pairsAnalyzed).toBe(50);
|
|
604
|
+
expect(result.score).toBeGreaterThan(0);
|
|
605
|
+
|
|
606
|
+
// Should complete within reasonable time (this is a mock test, so should be very fast)
|
|
607
|
+
expect(endTime - startTime).toBeLessThan(1000); // Less than 1 second
|
|
608
|
+
|
|
609
|
+
// Verify all pairs were processed
|
|
610
|
+
expect(mockGetLogProb).toHaveBeenCalledTimes(100); // 50 pairs × 2 sentences
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it("should maintain accuracy with mixed success/failure scenarios", async () => {
|
|
614
|
+
const ollamaClient = new OllamaClient("unreliable-model");
|
|
615
|
+
|
|
616
|
+
// Mock mixed success/failure pattern
|
|
617
|
+
mockFetch
|
|
618
|
+
// Mock version check
|
|
619
|
+
.mockResolvedValueOnce({
|
|
620
|
+
ok: true,
|
|
621
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
622
|
+
})
|
|
623
|
+
// Mock capability detection
|
|
624
|
+
.mockResolvedValueOnce({
|
|
625
|
+
ok: true,
|
|
626
|
+
json: () =>
|
|
627
|
+
Promise.resolve({
|
|
628
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
629
|
+
}),
|
|
630
|
+
})
|
|
631
|
+
// Pair 1: Success
|
|
632
|
+
.mockResolvedValueOnce({
|
|
633
|
+
ok: true,
|
|
634
|
+
json: () =>
|
|
635
|
+
Promise.resolve({
|
|
636
|
+
logprobs: [{ token: "Test", logprob: -1.2 }],
|
|
637
|
+
}),
|
|
638
|
+
})
|
|
639
|
+
.mockResolvedValueOnce({
|
|
640
|
+
ok: true,
|
|
641
|
+
json: () =>
|
|
642
|
+
Promise.resolve({
|
|
643
|
+
logprobs: [{ token: "Test", logprob: -1.8 }],
|
|
644
|
+
}),
|
|
645
|
+
})
|
|
646
|
+
// Pair 2: First sentence fails
|
|
647
|
+
.mockRejectedValueOnce(new Error("Network timeout"))
|
|
648
|
+
.mockResolvedValueOnce({
|
|
649
|
+
ok: true,
|
|
650
|
+
json: () =>
|
|
651
|
+
Promise.resolve({
|
|
652
|
+
logprobs: [{ token: "Test", logprob: -2.0 }],
|
|
653
|
+
}),
|
|
654
|
+
})
|
|
655
|
+
// Pair 3: Success
|
|
656
|
+
.mockResolvedValueOnce({
|
|
657
|
+
ok: true,
|
|
658
|
+
json: () =>
|
|
659
|
+
Promise.resolve({
|
|
660
|
+
logprobs: [{ token: "Test", logprob: -1.4 }],
|
|
661
|
+
}),
|
|
662
|
+
})
|
|
663
|
+
.mockResolvedValueOnce({
|
|
664
|
+
ok: true,
|
|
665
|
+
json: () =>
|
|
666
|
+
Promise.resolve({
|
|
667
|
+
logprobs: [{ token: "Test", logprob: -1.9 }],
|
|
668
|
+
}),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const config: BiasTestConfig = {
|
|
672
|
+
dataset: mockCrowsPairsData,
|
|
673
|
+
model: "unreliable-model",
|
|
674
|
+
engine: "ollama",
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const result = await calculateCrowsPairsBias(config, ollamaClient);
|
|
678
|
+
|
|
679
|
+
// Should process 2 out of 3 pairs successfully
|
|
680
|
+
expect(result.pairsAnalyzed).toBe(2);
|
|
681
|
+
// Method may fall back to latency due to network errors during processing
|
|
682
|
+
expect(["logprobs_exact", "logprobs_fallback_latency"]).toContain(result.method);
|
|
683
|
+
expect(result.score).toBeDefined();
|
|
684
|
+
expect(result.stereotypicalPreference).toBeGreaterThanOrEqual(0);
|
|
685
|
+
expect(result.stereotypicalPreference).toBeLessThanOrEqual(100);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
});
|