@framers/agentos-ext-ml-classifiers 0.1.0 → 0.2.1
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/CHANGELOG.md +18 -0
- package/dist/MLClassifierGuardrail.d.ts +88 -117
- package/dist/MLClassifierGuardrail.d.ts.map +1 -1
- package/dist/MLClassifierGuardrail.js +255 -264
- package/dist/MLClassifierGuardrail.js.map +1 -1
- package/dist/classifiers/InjectionClassifier.d.ts +1 -1
- package/dist/classifiers/InjectionClassifier.d.ts.map +1 -1
- package/dist/classifiers/JailbreakClassifier.d.ts +1 -1
- package/dist/classifiers/JailbreakClassifier.d.ts.map +1 -1
- package/dist/classifiers/ToxicityClassifier.d.ts +1 -1
- package/dist/classifiers/ToxicityClassifier.d.ts.map +1 -1
- package/dist/classifiers/WorkerClassifierProxy.d.ts +1 -1
- package/dist/classifiers/WorkerClassifierProxy.d.ts.map +1 -1
- package/dist/index.d.ts +16 -90
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -306
- package/dist/index.js.map +1 -1
- package/dist/keyword-classifier.d.ts +26 -0
- package/dist/keyword-classifier.d.ts.map +1 -0
- package/dist/keyword-classifier.js +113 -0
- package/dist/keyword-classifier.js.map +1 -0
- package/dist/llm-classifier.d.ts +27 -0
- package/dist/llm-classifier.d.ts.map +1 -0
- package/dist/llm-classifier.js +129 -0
- package/dist/llm-classifier.js.map +1 -0
- package/dist/tools/ClassifyContentTool.d.ts +53 -80
- package/dist/tools/ClassifyContentTool.d.ts.map +1 -1
- package/dist/tools/ClassifyContentTool.js +52 -103
- package/dist/tools/ClassifyContentTool.js.map +1 -1
- package/dist/types.d.ts +77 -277
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -55
- package/dist/types.js.map +1 -1
- package/package.json +10 -16
- package/src/MLClassifierGuardrail.ts +279 -316
- package/src/index.ts +35 -339
- package/src/keyword-classifier.ts +130 -0
- package/src/llm-classifier.ts +163 -0
- package/src/tools/ClassifyContentTool.ts +75 -132
- package/src/types.ts +78 -325
- package/test/ClassifierOrchestrator.spec.ts +365 -0
- package/test/ClassifyContentTool.spec.ts +226 -0
- package/test/InjectionClassifier.spec.ts +263 -0
- package/test/JailbreakClassifier.spec.ts +295 -0
- package/test/MLClassifierGuardrail.spec.ts +486 -0
- package/test/SlidingWindowBuffer.spec.ts +391 -0
- package/test/ToxicityClassifier.spec.ts +268 -0
- package/test/WorkerClassifierProxy.spec.ts +303 -0
- package/test/index.spec.ts +431 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.spec.ts
|
|
3
|
+
* @description Unit tests for the ML Classifier pack factory.
|
|
4
|
+
*
|
|
5
|
+
* Tests verify:
|
|
6
|
+
* - createMLClassifierGuardrail returns an ExtensionPack with name 'ml-classifiers'
|
|
7
|
+
* and version '1.0.0'
|
|
8
|
+
* - The pack provides exactly 2 descriptors: 1 guardrail + 1 tool
|
|
9
|
+
* - Guardrail descriptor has id 'ml-classifier-guardrail' and kind 'guardrail'
|
|
10
|
+
* - Tool descriptor has id 'classify_content' and kind 'tool'
|
|
11
|
+
* - createExtensionPack bridges context.options to createMLClassifierGuardrail
|
|
12
|
+
* - Disabled / selective classifiers work correctly
|
|
13
|
+
* - onActivate rebuilds components with the shared registry
|
|
14
|
+
* - onDeactivate disposes orchestrator and clears buffer
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
18
|
+
import {
|
|
19
|
+
createMLClassifierGuardrail,
|
|
20
|
+
createExtensionPack,
|
|
21
|
+
} from '../src/index';
|
|
22
|
+
import { SharedServiceRegistry } from '@framers/agentos';
|
|
23
|
+
import {
|
|
24
|
+
EXTENSION_KIND_GUARDRAIL,
|
|
25
|
+
EXTENSION_KIND_TOOL,
|
|
26
|
+
} from '@framers/agentos';
|
|
27
|
+
import type { ExtensionPackContext } from '@framers/agentos';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Mocks — prevent real model downloads and ONNX/WASM loading
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mock ToxicityClassifier — lightweight stand-in that avoids the real
|
|
35
|
+
* `@huggingface/transformers` import during unit tests.
|
|
36
|
+
*/
|
|
37
|
+
vi.mock(
|
|
38
|
+
'../src/classifiers/ToxicityClassifier',
|
|
39
|
+
() => ({
|
|
40
|
+
ToxicityClassifier: vi.fn().mockImplementation(() => ({
|
|
41
|
+
id: 'agentos:ml-classifiers:toxicity-pipeline',
|
|
42
|
+
displayName: 'Toxicity Classifier (mock)',
|
|
43
|
+
description: 'Mock toxicity classifier.',
|
|
44
|
+
modelId: 'unitary/toxic-bert',
|
|
45
|
+
isLoaded: false,
|
|
46
|
+
classify: vi.fn().mockResolvedValue({ bestClass: 'benign', confidence: 0, allScores: [] }),
|
|
47
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
})),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mock InjectionClassifier.
|
|
54
|
+
*/
|
|
55
|
+
vi.mock(
|
|
56
|
+
'../src/classifiers/InjectionClassifier',
|
|
57
|
+
() => ({
|
|
58
|
+
InjectionClassifier: vi.fn().mockImplementation(() => ({
|
|
59
|
+
id: 'agentos:ml-classifiers:injection-pipeline',
|
|
60
|
+
displayName: 'Injection Classifier (mock)',
|
|
61
|
+
description: 'Mock injection classifier.',
|
|
62
|
+
modelId: 'protectai/deberta-v3-small-prompt-injection-v2',
|
|
63
|
+
isLoaded: false,
|
|
64
|
+
classify: vi.fn().mockResolvedValue({ bestClass: 'SAFE', confidence: 0.1, allScores: [] }),
|
|
65
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
66
|
+
})),
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mock JailbreakClassifier.
|
|
72
|
+
*/
|
|
73
|
+
vi.mock(
|
|
74
|
+
'../src/classifiers/JailbreakClassifier',
|
|
75
|
+
() => ({
|
|
76
|
+
JailbreakClassifier: vi.fn().mockImplementation(() => ({
|
|
77
|
+
id: 'agentos:ml-classifiers:jailbreak-pipeline',
|
|
78
|
+
displayName: 'Jailbreak Classifier (mock)',
|
|
79
|
+
description: 'Mock jailbreak classifier.',
|
|
80
|
+
modelId: 'meta-llama/PromptGuard-86M',
|
|
81
|
+
isLoaded: false,
|
|
82
|
+
classify: vi.fn().mockResolvedValue({ bestClass: 'benign', confidence: 0, allScores: [] }),
|
|
83
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
84
|
+
})),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Import the mocked constructors so tests can assert on them.
|
|
89
|
+
import { ToxicityClassifier } from '../src/classifiers/ToxicityClassifier';
|
|
90
|
+
import { InjectionClassifier } from '../src/classifiers/InjectionClassifier';
|
|
91
|
+
import { JailbreakClassifier } from '../src/classifiers/JailbreakClassifier';
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Tests
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
describe('createMLClassifierGuardrail', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.clearAllMocks();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
// 1. Pack identity
|
|
104
|
+
// -------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe('pack identity', () => {
|
|
107
|
+
it('returns an ExtensionPack with name "ml-classifiers"', () => {
|
|
108
|
+
const pack = createMLClassifierGuardrail();
|
|
109
|
+
expect(pack.name).toBe('ml-classifiers');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns an ExtensionPack with version "1.0.0"', () => {
|
|
113
|
+
const pack = createMLClassifierGuardrail();
|
|
114
|
+
expect(pack.version).toBe('1.0.0');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
// 2. Descriptors shape
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
describe('descriptors', () => {
|
|
123
|
+
it('provides exactly 2 descriptors', () => {
|
|
124
|
+
const pack = createMLClassifierGuardrail();
|
|
125
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('has a guardrail descriptor with id "ml-classifier-guardrail"', () => {
|
|
129
|
+
const pack = createMLClassifierGuardrail();
|
|
130
|
+
const guardrailDescriptor = pack.descriptors.find((d) => d.id === 'ml-classifier-guardrail');
|
|
131
|
+
|
|
132
|
+
expect(guardrailDescriptor).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('guardrail descriptor has kind "guardrail"', () => {
|
|
136
|
+
const pack = createMLClassifierGuardrail();
|
|
137
|
+
const guardrailDescriptor = pack.descriptors.find((d) => d.id === 'ml-classifier-guardrail');
|
|
138
|
+
|
|
139
|
+
expect(guardrailDescriptor?.kind).toBe(EXTENSION_KIND_GUARDRAIL);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('guardrail descriptor has priority 5', () => {
|
|
143
|
+
const pack = createMLClassifierGuardrail();
|
|
144
|
+
const guardrailDescriptor = pack.descriptors.find((d) => d.id === 'ml-classifier-guardrail');
|
|
145
|
+
|
|
146
|
+
expect(guardrailDescriptor?.priority).toBe(5);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('guardrail descriptor has a non-null payload', () => {
|
|
150
|
+
const pack = createMLClassifierGuardrail();
|
|
151
|
+
const guardrailDescriptor = pack.descriptors.find((d) => d.id === 'ml-classifier-guardrail');
|
|
152
|
+
|
|
153
|
+
expect(guardrailDescriptor?.payload).toBeDefined();
|
|
154
|
+
expect(guardrailDescriptor?.payload).not.toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('has a tool descriptor with id "classify_content"', () => {
|
|
158
|
+
const pack = createMLClassifierGuardrail();
|
|
159
|
+
const toolDescriptor = pack.descriptors.find((d) => d.id === 'classify_content');
|
|
160
|
+
|
|
161
|
+
expect(toolDescriptor).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('tool descriptor has kind "tool"', () => {
|
|
165
|
+
const pack = createMLClassifierGuardrail();
|
|
166
|
+
const toolDescriptor = pack.descriptors.find((d) => d.id === 'classify_content');
|
|
167
|
+
|
|
168
|
+
expect(toolDescriptor?.kind).toBe(EXTENSION_KIND_TOOL);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('tool descriptor has priority 0', () => {
|
|
172
|
+
const pack = createMLClassifierGuardrail();
|
|
173
|
+
const toolDescriptor = pack.descriptors.find((d) => d.id === 'classify_content');
|
|
174
|
+
|
|
175
|
+
expect(toolDescriptor?.priority).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('tool descriptor has a non-null payload', () => {
|
|
179
|
+
const pack = createMLClassifierGuardrail();
|
|
180
|
+
const toolDescriptor = pack.descriptors.find((d) => d.id === 'classify_content');
|
|
181
|
+
|
|
182
|
+
expect(toolDescriptor?.payload).toBeDefined();
|
|
183
|
+
expect(toolDescriptor?.payload).not.toBeNull();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// -------------------------------------------------------------------------
|
|
188
|
+
// 3. Built-in classifier instantiation (zero-config)
|
|
189
|
+
// -------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
describe('zero-config classifier instantiation', () => {
|
|
192
|
+
it('instantiates all three built-in classifiers when no classifiers option is given', () => {
|
|
193
|
+
createMLClassifierGuardrail();
|
|
194
|
+
|
|
195
|
+
// Each built-in classifier should have been constructed once.
|
|
196
|
+
expect(ToxicityClassifier).toHaveBeenCalledOnce();
|
|
197
|
+
expect(InjectionClassifier).toHaveBeenCalledOnce();
|
|
198
|
+
expect(JailbreakClassifier).toHaveBeenCalledOnce();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('instantiates all three built-in classifiers when classifiers is an empty array', () => {
|
|
202
|
+
createMLClassifierGuardrail({ classifiers: [] });
|
|
203
|
+
|
|
204
|
+
expect(ToxicityClassifier).toHaveBeenCalledOnce();
|
|
205
|
+
expect(InjectionClassifier).toHaveBeenCalledOnce();
|
|
206
|
+
expect(JailbreakClassifier).toHaveBeenCalledOnce();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// -------------------------------------------------------------------------
|
|
211
|
+
// 4. Selective / disabled classifiers
|
|
212
|
+
// -------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe('selective classifiers', () => {
|
|
215
|
+
it('only instantiates ToxicityClassifier when classifiers: ["toxicity"]', () => {
|
|
216
|
+
createMLClassifierGuardrail({ classifiers: ['toxicity'] });
|
|
217
|
+
|
|
218
|
+
expect(ToxicityClassifier).toHaveBeenCalledOnce();
|
|
219
|
+
expect(InjectionClassifier).not.toHaveBeenCalled();
|
|
220
|
+
expect(JailbreakClassifier).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('only instantiates InjectionClassifier when classifiers: ["injection"]', () => {
|
|
224
|
+
createMLClassifierGuardrail({ classifiers: ['injection'] });
|
|
225
|
+
|
|
226
|
+
expect(ToxicityClassifier).not.toHaveBeenCalled();
|
|
227
|
+
expect(InjectionClassifier).toHaveBeenCalledOnce();
|
|
228
|
+
expect(JailbreakClassifier).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('only instantiates JailbreakClassifier when classifiers: ["jailbreak"]', () => {
|
|
232
|
+
createMLClassifierGuardrail({ classifiers: ['jailbreak'] });
|
|
233
|
+
|
|
234
|
+
expect(ToxicityClassifier).not.toHaveBeenCalled();
|
|
235
|
+
expect(InjectionClassifier).not.toHaveBeenCalled();
|
|
236
|
+
expect(JailbreakClassifier).toHaveBeenCalledOnce();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('instantiates toxicity and jailbreak but not injection when specified', () => {
|
|
240
|
+
createMLClassifierGuardrail({ classifiers: ['toxicity', 'jailbreak'] });
|
|
241
|
+
|
|
242
|
+
expect(ToxicityClassifier).toHaveBeenCalledOnce();
|
|
243
|
+
expect(InjectionClassifier).not.toHaveBeenCalled();
|
|
244
|
+
expect(JailbreakClassifier).toHaveBeenCalledOnce();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('still provides 2 descriptors when only 1 classifier is enabled', () => {
|
|
248
|
+
const pack = createMLClassifierGuardrail({ classifiers: ['toxicity'] });
|
|
249
|
+
|
|
250
|
+
// The guardrail and tool are always present regardless of classifier count.
|
|
251
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
// 5. Custom classifiers
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
describe('customClassifiers option', () => {
|
|
260
|
+
it('includes custom classifiers alongside built-in ones', () => {
|
|
261
|
+
const customClassifier = {
|
|
262
|
+
id: 'custom:sarcasm',
|
|
263
|
+
displayName: 'Sarcasm Detector',
|
|
264
|
+
description: 'Detects sarcasm.',
|
|
265
|
+
modelId: 'my-org/sarcasm-bert',
|
|
266
|
+
isLoaded: false,
|
|
267
|
+
classify: vi.fn().mockResolvedValue({ bestClass: 'benign', confidence: 0, allScores: [] }),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Should not throw when a custom classifier is provided.
|
|
271
|
+
const pack = createMLClassifierGuardrail({
|
|
272
|
+
classifiers: ['toxicity'],
|
|
273
|
+
customClassifiers: [customClassifier],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Pack structure must remain consistent.
|
|
277
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
// 6. onActivate lifecycle hook
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
describe('onActivate lifecycle hook', () => {
|
|
286
|
+
it('rebuilds components when onActivate is called with a shared registry', () => {
|
|
287
|
+
const pack = createMLClassifierGuardrail();
|
|
288
|
+
|
|
289
|
+
// Record the number of classifier constructions at pack-creation time.
|
|
290
|
+
const constructsBefore =
|
|
291
|
+
(ToxicityClassifier as ReturnType<typeof vi.fn>).mock.calls.length +
|
|
292
|
+
(InjectionClassifier as ReturnType<typeof vi.fn>).mock.calls.length +
|
|
293
|
+
(JailbreakClassifier as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
294
|
+
|
|
295
|
+
// Activate with a shared registry.
|
|
296
|
+
const sharedRegistry = new SharedServiceRegistry();
|
|
297
|
+
pack.onActivate!({ services: sharedRegistry });
|
|
298
|
+
|
|
299
|
+
const constructsAfter =
|
|
300
|
+
(ToxicityClassifier as ReturnType<typeof vi.fn>).mock.calls.length +
|
|
301
|
+
(InjectionClassifier as ReturnType<typeof vi.fn>).mock.calls.length +
|
|
302
|
+
(JailbreakClassifier as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
303
|
+
|
|
304
|
+
// Activation must have rebuilt the classifiers (3 more constructions).
|
|
305
|
+
expect(constructsAfter).toBe(constructsBefore + 3);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('descriptors still reflect the rebuilt components after onActivate', () => {
|
|
309
|
+
const pack = createMLClassifierGuardrail();
|
|
310
|
+
const sharedRegistry = new SharedServiceRegistry();
|
|
311
|
+
pack.onActivate!({ services: sharedRegistry });
|
|
312
|
+
|
|
313
|
+
// Descriptors getter must return fresh references.
|
|
314
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('does not throw when onActivate is called without services', () => {
|
|
318
|
+
const pack = createMLClassifierGuardrail();
|
|
319
|
+
|
|
320
|
+
// Context without a services field should be handled gracefully.
|
|
321
|
+
expect(() => pack.onActivate!({})).not.toThrow();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// -------------------------------------------------------------------------
|
|
326
|
+
// 7. onDeactivate lifecycle hook
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
describe('onDeactivate lifecycle hook', () => {
|
|
330
|
+
it('resolves without throwing', async () => {
|
|
331
|
+
const pack = createMLClassifierGuardrail();
|
|
332
|
+
await expect(pack.onDeactivate!()).resolves.toBeUndefined();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// -------------------------------------------------------------------------
|
|
337
|
+
// 8. Options passthrough
|
|
338
|
+
// -------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
describe('options passthrough', () => {
|
|
341
|
+
it('accepts and applies streaming mode options without throwing', () => {
|
|
342
|
+
expect(() =>
|
|
343
|
+
createMLClassifierGuardrail({
|
|
344
|
+
streamingMode: true,
|
|
345
|
+
chunkSize: 150,
|
|
346
|
+
contextSize: 30,
|
|
347
|
+
maxEvaluations: 50,
|
|
348
|
+
guardrailScope: 'output',
|
|
349
|
+
}),
|
|
350
|
+
).not.toThrow();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('accepts custom thresholds without throwing', () => {
|
|
354
|
+
expect(() =>
|
|
355
|
+
createMLClassifierGuardrail({
|
|
356
|
+
thresholds: {
|
|
357
|
+
blockThreshold: 0.95,
|
|
358
|
+
flagThreshold: 0.75,
|
|
359
|
+
warnThreshold: 0.5,
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
).not.toThrow();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// createExtensionPack (manifest factory bridge)
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
describe('createExtensionPack', () => {
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
vi.clearAllMocks();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('returns a pack with name "ml-classifiers"', () => {
|
|
377
|
+
const context: ExtensionPackContext = {};
|
|
378
|
+
const pack = createExtensionPack(context);
|
|
379
|
+
|
|
380
|
+
expect(pack.name).toBe('ml-classifiers');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('returns a pack with version "1.0.0"', () => {
|
|
384
|
+
const context: ExtensionPackContext = {};
|
|
385
|
+
const pack = createExtensionPack(context);
|
|
386
|
+
|
|
387
|
+
expect(pack.version).toBe('1.0.0');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('provides 2 descriptors with empty context', () => {
|
|
391
|
+
const pack = createExtensionPack({});
|
|
392
|
+
|
|
393
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('bridges context.options to createMLClassifierGuardrail — classifiers subset', () => {
|
|
397
|
+
const context: ExtensionPackContext = {
|
|
398
|
+
options: {
|
|
399
|
+
classifiers: ['toxicity'],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
createExtensionPack(context);
|
|
404
|
+
|
|
405
|
+
// Only ToxicityClassifier should have been instantiated.
|
|
406
|
+
expect(ToxicityClassifier).toHaveBeenCalledOnce();
|
|
407
|
+
expect(InjectionClassifier).not.toHaveBeenCalled();
|
|
408
|
+
expect(JailbreakClassifier).not.toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('bridges context.options to createMLClassifierGuardrail — thresholds', () => {
|
|
412
|
+
const context: ExtensionPackContext = {
|
|
413
|
+
options: {
|
|
414
|
+
thresholds: { blockThreshold: 0.99 },
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const pack = createExtensionPack(context);
|
|
419
|
+
|
|
420
|
+
// Pack must still be well-formed.
|
|
421
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('works when context.options is undefined', () => {
|
|
425
|
+
const context: ExtensionPackContext = { options: undefined };
|
|
426
|
+
const pack = createExtensionPack(context);
|
|
427
|
+
|
|
428
|
+
expect(pack.name).toBe('ml-classifiers');
|
|
429
|
+
expect(pack.descriptors).toHaveLength(2);
|
|
430
|
+
});
|
|
431
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
20
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
// CI layout: agentos cloned into packages/agentos/ inside this repo
|
|
6
|
+
const ciPath = path.resolve(__dirname, '../../../../packages/agentos/src');
|
|
7
|
+
// Monorepo layout: agentos is a sibling at packages/agentos/
|
|
8
|
+
const monoPath = path.resolve(__dirname, '../../../../../agentos/src');
|
|
9
|
+
|
|
10
|
+
const agentosPath = fs.existsSync(ciPath) ? ciPath : fs.existsSync(monoPath) ? monoPath : null;
|
|
11
|
+
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
test: {
|
|
14
|
+
globals: true,
|
|
15
|
+
environment: 'node',
|
|
16
|
+
include: ['test/**/*.spec.ts'],
|
|
17
|
+
testTimeout: 10000,
|
|
18
|
+
},
|
|
19
|
+
resolve: agentosPath ? {
|
|
20
|
+
alias: {
|
|
21
|
+
'@framers/agentos': agentosPath,
|
|
22
|
+
},
|
|
23
|
+
} : {},
|
|
24
|
+
});
|