@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,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { counterfactualFairness } from "../src/legal-checks/bias-metrics";
|
|
3
|
+
import { classifyAnnexIIIRisk } from "../src/legal-checks/risk-classifier";
|
|
4
|
+
import type { QuizAnswer } from "../src/types";
|
|
5
|
+
|
|
6
|
+
describe("legal-checks", () => {
|
|
7
|
+
describe("classifyAnnexIIIRisk", () => {
|
|
8
|
+
it("flags high-risk if any Annex III category is 'yes'", () => {
|
|
9
|
+
const answers: QuizAnswer[] = [
|
|
10
|
+
{ questionId: "annexIII-employment-workers", value: "yes" },
|
|
11
|
+
{ questionId: "annexIII-education-training", value: "no" },
|
|
12
|
+
];
|
|
13
|
+
const res = classifyAnnexIIIRisk(answers);
|
|
14
|
+
expect(res.level).toBe("high");
|
|
15
|
+
expect(res.matchedCategories).toContain("employment-workers");
|
|
16
|
+
expect(res.legalBasis.join(" ")).toContain("Annex III");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("flags unacceptable if prohibited screening is 'yes' (red-flag)", () => {
|
|
20
|
+
const answers: QuizAnswer[] = [{ questionId: "art5-subliminal", value: "yes" }];
|
|
21
|
+
const res = classifyAnnexIIIRisk(answers);
|
|
22
|
+
expect(res.level).toBe("unacceptable");
|
|
23
|
+
expect(res.prohibitedFlags.length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("counterfactualFairness", () => {
|
|
28
|
+
it("computes flip-rate and passes when flips are below threshold", () => {
|
|
29
|
+
const pairs = [
|
|
30
|
+
{
|
|
31
|
+
id: "1",
|
|
32
|
+
protectedAttribute: "gender",
|
|
33
|
+
groupA: "female",
|
|
34
|
+
groupB: "male",
|
|
35
|
+
predictionA: 1,
|
|
36
|
+
predictionB: 1,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "2",
|
|
40
|
+
protectedAttribute: "gender",
|
|
41
|
+
groupA: "female",
|
|
42
|
+
groupB: "male",
|
|
43
|
+
predictionA: 0,
|
|
44
|
+
predictionB: 0,
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const m = counterfactualFairness(pairs, 0.05);
|
|
49
|
+
expect(m.value).toBe(0);
|
|
50
|
+
expect(m.passed).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
OllamaClient,
|
|
4
|
+
detectOllama,
|
|
5
|
+
listOllamaModels,
|
|
6
|
+
ollamaGenerate,
|
|
7
|
+
} from "../src/inference/ollama-client";
|
|
8
|
+
import type { ModelCapabilityCache } from "../src/types";
|
|
9
|
+
|
|
10
|
+
// Mock fetch globally
|
|
11
|
+
const mockFetch = vi.fn();
|
|
12
|
+
global.fetch = mockFetch;
|
|
13
|
+
|
|
14
|
+
// Mock localStorage
|
|
15
|
+
const mockLocalStorage = {
|
|
16
|
+
getItem: vi.fn(),
|
|
17
|
+
setItem: vi.fn(),
|
|
18
|
+
removeItem: vi.fn(),
|
|
19
|
+
clear: vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Mock global objects for Node.js environment
|
|
23
|
+
Object.defineProperty(globalThis, "localStorage", {
|
|
24
|
+
value: mockLocalStorage,
|
|
25
|
+
writable: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
Object.defineProperty(globalThis, "performance", {
|
|
29
|
+
value: {
|
|
30
|
+
now: vi.fn(() => 1000), // Fixed timestamp for consistent tests
|
|
31
|
+
},
|
|
32
|
+
writable: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("OllamaClient", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("detectOllama", () => {
|
|
46
|
+
it("should return true when Ollama is accessible", async () => {
|
|
47
|
+
mockFetch.mockResolvedValueOnce({
|
|
48
|
+
ok: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await detectOllama();
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
expect(mockFetch).toHaveBeenCalledWith("http://localhost:11434/api/tags", {
|
|
54
|
+
signal: expect.any(AbortSignal),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return false when Ollama is not accessible", async () => {
|
|
59
|
+
mockFetch.mockRejectedValueOnce(new Error("Connection failed"));
|
|
60
|
+
|
|
61
|
+
const result = await detectOllama();
|
|
62
|
+
expect(result).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should return false when response is not ok", async () => {
|
|
66
|
+
mockFetch.mockResolvedValueOnce({
|
|
67
|
+
ok: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await detectOllama();
|
|
71
|
+
expect(result).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("listOllamaModels", () => {
|
|
76
|
+
it("should return list of models when successful", async () => {
|
|
77
|
+
const mockModels = [
|
|
78
|
+
{ name: "llama3.2:latest", size: 1000000, digest: "abc123", modified_at: "2024-01-01" },
|
|
79
|
+
{ name: "mistral:latest", size: 2000000, digest: "def456", modified_at: "2024-01-02" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
mockFetch.mockResolvedValueOnce({
|
|
83
|
+
ok: true,
|
|
84
|
+
json: () => Promise.resolve({ models: mockModels }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await listOllamaModels();
|
|
88
|
+
expect(result).toEqual(mockModels);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should return empty array when request fails", async () => {
|
|
92
|
+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
93
|
+
|
|
94
|
+
const result = await listOllamaModels();
|
|
95
|
+
expect(result).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return empty array when no models field in response", async () => {
|
|
99
|
+
mockFetch.mockResolvedValueOnce({
|
|
100
|
+
ok: true,
|
|
101
|
+
json: () => Promise.resolve({}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await listOllamaModels();
|
|
105
|
+
expect(result).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("ollamaGenerate", () => {
|
|
110
|
+
it("should generate text successfully", async () => {
|
|
111
|
+
const mockResponse = {
|
|
112
|
+
model: "llama3.2",
|
|
113
|
+
response: "Generated text",
|
|
114
|
+
done: true,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
mockFetch.mockResolvedValueOnce({
|
|
118
|
+
ok: true,
|
|
119
|
+
json: () => Promise.resolve(mockResponse),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await ollamaGenerate({
|
|
123
|
+
model: "llama3.2",
|
|
124
|
+
prompt: "Test prompt",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual(mockResponse);
|
|
128
|
+
expect(mockFetch).toHaveBeenCalledWith("http://localhost:11434/api/generate", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
model: "llama3.2",
|
|
133
|
+
prompt: "Test prompt",
|
|
134
|
+
stream: false,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should throw error when request fails", async () => {
|
|
140
|
+
mockFetch.mockResolvedValueOnce({
|
|
141
|
+
ok: false,
|
|
142
|
+
status: 500,
|
|
143
|
+
statusText: "Internal Server Error",
|
|
144
|
+
text: () => Promise.resolve("Server error"),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
ollamaGenerate({
|
|
149
|
+
model: "llama3.2",
|
|
150
|
+
prompt: "Test prompt",
|
|
151
|
+
})
|
|
152
|
+
).rejects.toThrow("Ollama generate failed (500): Server error");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("OllamaClient class", () => {
|
|
157
|
+
let client: OllamaClient;
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
client = new OllamaClient("llama3.2");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("detectLogProbSupport", () => {
|
|
164
|
+
it("should return cached result when available", async () => {
|
|
165
|
+
const cachedCapability: ModelCapabilityCache = {
|
|
166
|
+
model: "llama3.2",
|
|
167
|
+
supportsLogProbs: true,
|
|
168
|
+
testedAt: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
mockLocalStorage.getItem.mockReturnValue(JSON.stringify(cachedCapability));
|
|
172
|
+
|
|
173
|
+
const result = await client.detectLogProbSupport();
|
|
174
|
+
expect(result).toBe(true);
|
|
175
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should test capability when no cache exists", async () => {
|
|
179
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
180
|
+
|
|
181
|
+
// Mock version check (optional, called first)
|
|
182
|
+
mockFetch.mockResolvedValueOnce({
|
|
183
|
+
ok: true,
|
|
184
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Mock successful log-probability test
|
|
188
|
+
mockFetch.mockResolvedValueOnce({
|
|
189
|
+
ok: true,
|
|
190
|
+
json: () =>
|
|
191
|
+
Promise.resolve({
|
|
192
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await client.detectLogProbSupport();
|
|
197
|
+
expect(result).toBe(true);
|
|
198
|
+
|
|
199
|
+
// Verify test request was made
|
|
200
|
+
expect(mockFetch).toHaveBeenCalledWith("http://localhost:11434/api/generate", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
model: "llama3.2",
|
|
205
|
+
prompt: "Der",
|
|
206
|
+
stream: false,
|
|
207
|
+
logprobs: true,
|
|
208
|
+
options: {
|
|
209
|
+
num_predict: 1,
|
|
210
|
+
temperature: 0,
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Verify result was cached
|
|
216
|
+
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
|
217
|
+
"ollama_supports_logprobs_llama3.2",
|
|
218
|
+
expect.stringContaining('"supportsLogProbs":true')
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should return false when log-probabilities are not supported", async () => {
|
|
223
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
224
|
+
|
|
225
|
+
// Mock version check
|
|
226
|
+
mockFetch.mockResolvedValueOnce({
|
|
227
|
+
ok: true,
|
|
228
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Mock response without log-probabilities
|
|
232
|
+
mockFetch.mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: () =>
|
|
235
|
+
Promise.resolve({
|
|
236
|
+
response: "Der",
|
|
237
|
+
logprobs: null,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await client.detectLogProbSupport();
|
|
242
|
+
expect(result).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should return false when test request fails", async () => {
|
|
246
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
247
|
+
|
|
248
|
+
// Mock version check
|
|
249
|
+
mockFetch.mockResolvedValueOnce({
|
|
250
|
+
ok: true,
|
|
251
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Mock failed test request
|
|
255
|
+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
256
|
+
|
|
257
|
+
const result = await client.detectLogProbSupport();
|
|
258
|
+
expect(result).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should ignore expired cache entries", async () => {
|
|
262
|
+
const expiredCapability: ModelCapabilityCache = {
|
|
263
|
+
model: "llama3.2",
|
|
264
|
+
supportsLogProbs: true,
|
|
265
|
+
testedAt: new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(), // 25 hours ago
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
mockLocalStorage.getItem.mockReturnValue(JSON.stringify(expiredCapability));
|
|
269
|
+
|
|
270
|
+
// Mock version check
|
|
271
|
+
mockFetch.mockResolvedValueOnce({
|
|
272
|
+
ok: true,
|
|
273
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Mock new test
|
|
277
|
+
mockFetch.mockResolvedValueOnce({
|
|
278
|
+
ok: true,
|
|
279
|
+
json: () =>
|
|
280
|
+
Promise.resolve({
|
|
281
|
+
logprobs: [{ token: "Der", logprob: -1.5 }],
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = await client.detectLogProbSupport();
|
|
286
|
+
expect(result).toBe(true);
|
|
287
|
+
expect(mockFetch).toHaveBeenCalled(); // Should run new test
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("getLogProbWithFallback", () => {
|
|
292
|
+
it("should use exact log-probabilities when supported", async () => {
|
|
293
|
+
// Mock capability detection
|
|
294
|
+
mockLocalStorage.getItem.mockReturnValue(
|
|
295
|
+
JSON.stringify({
|
|
296
|
+
model: "llama3.2",
|
|
297
|
+
supportsLogProbs: true,
|
|
298
|
+
testedAt: new Date().toISOString(),
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Mock log-probability response
|
|
303
|
+
mockFetch.mockResolvedValueOnce({
|
|
304
|
+
ok: true,
|
|
305
|
+
json: () =>
|
|
306
|
+
Promise.resolve({
|
|
307
|
+
logprobs: [
|
|
308
|
+
{ token: "Hello", logprob: -1.2 },
|
|
309
|
+
{ token: " world", logprob: -2.1 },
|
|
310
|
+
],
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await client.getLogProbWithFallback("Hello world");
|
|
315
|
+
|
|
316
|
+
expect(result.method).toBe("logprobs_exact");
|
|
317
|
+
expect(result.logprob).toBeCloseTo(-1.65, 2); // Average of -1.2 and -2.1
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should fallback to latency method when log-probabilities fail", async () => {
|
|
321
|
+
// Mock capability detection showing support
|
|
322
|
+
mockLocalStorage.getItem.mockReturnValue(
|
|
323
|
+
JSON.stringify({
|
|
324
|
+
model: "llama3.2",
|
|
325
|
+
supportsLogProbs: true,
|
|
326
|
+
testedAt: new Date().toISOString(),
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Mock log-probability request failure, then successful fallback
|
|
331
|
+
mockFetch
|
|
332
|
+
.mockResolvedValueOnce({
|
|
333
|
+
ok: false,
|
|
334
|
+
statusText: "Log-probabilities not available",
|
|
335
|
+
})
|
|
336
|
+
.mockResolvedValueOnce({
|
|
337
|
+
ok: true,
|
|
338
|
+
json: () =>
|
|
339
|
+
Promise.resolve({
|
|
340
|
+
prompt_eval_duration: 1000000000, // 1 second in nanoseconds
|
|
341
|
+
prompt_eval_count: 2, // 2 tokens
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const result = await client.getLogProbWithFallback("Hello world");
|
|
346
|
+
|
|
347
|
+
expect(result.method).toBe("logprobs_fallback_latency");
|
|
348
|
+
expect(result.logprob).toBeDefined();
|
|
349
|
+
expect(typeof result.logprob).toBe("number");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("should use latency method when model doesn't support log-probabilities", async () => {
|
|
353
|
+
// Mock capability detection showing no support
|
|
354
|
+
mockLocalStorage.getItem.mockReturnValue(
|
|
355
|
+
JSON.stringify({
|
|
356
|
+
model: "llama3.2",
|
|
357
|
+
supportsLogProbs: false,
|
|
358
|
+
testedAt: new Date().toISOString(),
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// Mock latency-based response
|
|
363
|
+
mockFetch.mockResolvedValueOnce({
|
|
364
|
+
ok: true,
|
|
365
|
+
json: () =>
|
|
366
|
+
Promise.resolve({
|
|
367
|
+
prompt_eval_duration: 500000000, // 0.5 seconds in nanoseconds
|
|
368
|
+
prompt_eval_count: 1,
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const result = await client.getLogProbWithFallback("Test");
|
|
373
|
+
|
|
374
|
+
expect(result.method).toBe("logprobs_fallback_latency");
|
|
375
|
+
expect(result.logprob).toBeDefined();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should handle total request time fallback", async () => {
|
|
379
|
+
mockLocalStorage.getItem.mockReturnValue(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
model: "llama3.2",
|
|
382
|
+
supportsLogProbs: false,
|
|
383
|
+
testedAt: new Date().toISOString(),
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Mock response without prompt_eval metrics
|
|
388
|
+
mockFetch.mockResolvedValueOnce({
|
|
389
|
+
ok: true,
|
|
390
|
+
json: () =>
|
|
391
|
+
Promise.resolve({
|
|
392
|
+
response: "",
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Mock performance.now to simulate request time
|
|
397
|
+
const mockPerformance = vi.spyOn(globalThis.performance, "now");
|
|
398
|
+
mockPerformance
|
|
399
|
+
.mockReturnValueOnce(1000) // Start time
|
|
400
|
+
.mockReturnValueOnce(1500); // End time (500ms later)
|
|
401
|
+
|
|
402
|
+
const result = await client.getLogProbWithFallback("Test prompt");
|
|
403
|
+
|
|
404
|
+
expect(result.method).toBe("logprobs_fallback_latency");
|
|
405
|
+
expect(result.logprob).toBeDefined();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should throw error when all methods fail", async () => {
|
|
409
|
+
mockLocalStorage.getItem.mockReturnValue(null);
|
|
410
|
+
|
|
411
|
+
// Mock version check (succeeds)
|
|
412
|
+
mockFetch.mockResolvedValueOnce({
|
|
413
|
+
ok: true,
|
|
414
|
+
json: () => Promise.resolve({ version: "0.1.26" }),
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Mock capability test to fail
|
|
418
|
+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
419
|
+
|
|
420
|
+
// Mock the fallback latency request to also fail
|
|
421
|
+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
422
|
+
|
|
423
|
+
await expect(client.getLogProbWithFallback("Test")).rejects.toThrow("Network error");
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { RISK_QUIZ_QUESTIONS, classifyRisk, getQuizQuestions } from "../src/risk-engine";
|
|
3
|
+
import type { QuizAnswer } from "../src/types";
|
|
4
|
+
|
|
5
|
+
describe("Risk Engine", () => {
|
|
6
|
+
describe("getQuizQuestions", () => {
|
|
7
|
+
it("should return all quiz questions", () => {
|
|
8
|
+
const questions = getQuizQuestions();
|
|
9
|
+
expect(questions).toHaveLength(6);
|
|
10
|
+
expect(questions).toEqual(RISK_QUIZ_QUESTIONS);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should have valid question structure", () => {
|
|
14
|
+
const questions = getQuizQuestions();
|
|
15
|
+
for (const question of questions) {
|
|
16
|
+
expect(question.id).toBeDefined();
|
|
17
|
+
expect(question.question).toBeDefined();
|
|
18
|
+
expect(question.options.length).toBeGreaterThan(0);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("classifyRisk - Prohibited Practices", () => {
|
|
24
|
+
it("should classify subliminal manipulation as unacceptable", () => {
|
|
25
|
+
const answers: QuizAnswer[] = [
|
|
26
|
+
{ questionId: "purpose", value: "other" },
|
|
27
|
+
{ questionId: "subliminal", value: "yes" },
|
|
28
|
+
{ questionId: "vulnerability", value: "no" },
|
|
29
|
+
{ questionId: "social-scoring", value: "no" },
|
|
30
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
31
|
+
{ questionId: "human-oversight", value: "full" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const result = classifyRisk(answers);
|
|
35
|
+
expect(result.level).toBe("unacceptable");
|
|
36
|
+
expect(result.flags.some((f) => f.articleReference === "Article 5(1)(a)")).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should classify vulnerability exploitation as unacceptable", () => {
|
|
40
|
+
const answers: QuizAnswer[] = [
|
|
41
|
+
{ questionId: "purpose", value: "other" },
|
|
42
|
+
{ questionId: "subliminal", value: "no" },
|
|
43
|
+
{ questionId: "vulnerability", value: "yes" },
|
|
44
|
+
{ questionId: "social-scoring", value: "no" },
|
|
45
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
46
|
+
{ questionId: "human-oversight", value: "full" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const result = classifyRisk(answers);
|
|
50
|
+
expect(result.level).toBe("unacceptable");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should classify social scoring as unacceptable", () => {
|
|
54
|
+
const answers: QuizAnswer[] = [
|
|
55
|
+
{ questionId: "purpose", value: "other" },
|
|
56
|
+
{ questionId: "subliminal", value: "no" },
|
|
57
|
+
{ questionId: "vulnerability", value: "no" },
|
|
58
|
+
{ questionId: "social-scoring", value: "yes" },
|
|
59
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
60
|
+
{ questionId: "human-oversight", value: "full" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const result = classifyRisk(answers);
|
|
64
|
+
expect(result.level).toBe("unacceptable");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("classifyRisk - High Risk", () => {
|
|
69
|
+
it("should classify biometric identification system as high risk", () => {
|
|
70
|
+
const answers: QuizAnswer[] = [
|
|
71
|
+
{ questionId: "purpose", value: "biometric" },
|
|
72
|
+
{ questionId: "subliminal", value: "no" },
|
|
73
|
+
{ questionId: "vulnerability", value: "no" },
|
|
74
|
+
{ questionId: "social-scoring", value: "no" },
|
|
75
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
76
|
+
{ questionId: "human-oversight", value: "full" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const result = classifyRisk(answers);
|
|
80
|
+
expect(result.level).toBe("high");
|
|
81
|
+
expect(result.category).toBe("biometric-identification");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should classify employment AI as high risk", () => {
|
|
85
|
+
const answers: QuizAnswer[] = [
|
|
86
|
+
{ questionId: "purpose", value: "employment" },
|
|
87
|
+
{ questionId: "subliminal", value: "no" },
|
|
88
|
+
{ questionId: "vulnerability", value: "no" },
|
|
89
|
+
{ questionId: "social-scoring", value: "no" },
|
|
90
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
91
|
+
{ questionId: "human-oversight", value: "limited" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const result = classifyRisk(answers);
|
|
95
|
+
expect(result.level).toBe("high");
|
|
96
|
+
expect(result.category).toBe("employment-workers");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("classifyRisk - Limited Risk", () => {
|
|
101
|
+
it("should classify chatbot as limited risk", () => {
|
|
102
|
+
const answers: QuizAnswer[] = [
|
|
103
|
+
{ questionId: "purpose", value: "chatbot" },
|
|
104
|
+
{ questionId: "subliminal", value: "no" },
|
|
105
|
+
{ questionId: "vulnerability", value: "no" },
|
|
106
|
+
{ questionId: "social-scoring", value: "no" },
|
|
107
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
108
|
+
{ questionId: "human-oversight", value: "full" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const result = classifyRisk(answers);
|
|
112
|
+
expect(result.level).toBe("limited");
|
|
113
|
+
expect(result.legalBasis).toContain("Article 50 - Transparency Obligations");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("classifyRisk - Minimal Risk", () => {
|
|
118
|
+
it("should classify general purpose system with good oversight as minimal", () => {
|
|
119
|
+
const answers: QuizAnswer[] = [
|
|
120
|
+
{ questionId: "purpose", value: "other" },
|
|
121
|
+
{ questionId: "subliminal", value: "no" },
|
|
122
|
+
{ questionId: "vulnerability", value: "no" },
|
|
123
|
+
{ questionId: "social-scoring", value: "no" },
|
|
124
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
125
|
+
{ questionId: "human-oversight", value: "full" },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const result = classifyRisk(answers);
|
|
129
|
+
expect(result.level).toBe("minimal");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Risk Score Calculation", () => {
|
|
134
|
+
it("should return score between 0 and 100", () => {
|
|
135
|
+
const answers: QuizAnswer[] = [
|
|
136
|
+
{ questionId: "purpose", value: "other" },
|
|
137
|
+
{ questionId: "subliminal", value: "no" },
|
|
138
|
+
{ questionId: "vulnerability", value: "no" },
|
|
139
|
+
{ questionId: "social-scoring", value: "no" },
|
|
140
|
+
{ questionId: "biometric-realtime", value: "no" },
|
|
141
|
+
{ questionId: "human-oversight", value: "full" },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const result = classifyRisk(answers);
|
|
145
|
+
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
146
|
+
expect(result.score).toBeLessThanOrEqual(100);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: ["tests/**/*.test.ts"],
|
|
8
|
+
// Work around rare worker-pool teardown issues (tinypool) on some Node/tooling combos.
|
|
9
|
+
// Core tests are lightweight; single-thread execution improves stability and reproducibility.
|
|
10
|
+
pool: "threads",
|
|
11
|
+
poolOptions: {
|
|
12
|
+
threads: {
|
|
13
|
+
singleThread: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|