@aitytech/agentkits-memory 1.0.0 → 2.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/LICENSE +21 -0
- package/README.md +267 -149
- package/assets/agentkits-memory-add-memory.png +0 -0
- package/assets/agentkits-memory-memory-detail.png +0 -0
- package/assets/agentkits-memory-memory-list.png +0 -0
- package/assets/logo.svg +24 -0
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +14 -0
- package/dist/cli/web-viewer.d.ts.map +1 -0
- package/dist/cli/web-viewer.js +1769 -0
- package/dist/cli/web-viewer.js.map +1 -0
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +1956 -0
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
LocalEmbeddingsService,
|
|
4
|
+
createLocalEmbeddings,
|
|
5
|
+
createEmbeddingGenerator,
|
|
6
|
+
} from '../local-embeddings.js';
|
|
7
|
+
|
|
8
|
+
describe('LocalEmbeddingsService', () => {
|
|
9
|
+
let service: LocalEmbeddingsService;
|
|
10
|
+
|
|
11
|
+
describe('mock provider', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await service.shutdown();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create service with default config', () => {
|
|
21
|
+
expect(service).toBeDefined();
|
|
22
|
+
expect(service.getDimensions()).toBe(384);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should generate embedding for text', async () => {
|
|
26
|
+
const result = await service.embed('Hello world');
|
|
27
|
+
|
|
28
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
29
|
+
expect(result.embedding.length).toBe(384);
|
|
30
|
+
expect(result.cached).toBe(false);
|
|
31
|
+
expect(result.timeMs).toBeGreaterThanOrEqual(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return normalized embeddings', async () => {
|
|
35
|
+
const result = await service.embed('Test content');
|
|
36
|
+
|
|
37
|
+
// Calculate L2 norm
|
|
38
|
+
let norm = 0;
|
|
39
|
+
for (let i = 0; i < result.embedding.length; i++) {
|
|
40
|
+
norm += result.embedding[i] * result.embedding[i];
|
|
41
|
+
}
|
|
42
|
+
norm = Math.sqrt(norm);
|
|
43
|
+
|
|
44
|
+
// Should be normalized (L2 norm ≈ 1)
|
|
45
|
+
expect(norm).toBeCloseTo(1, 5);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should cache embeddings', async () => {
|
|
49
|
+
const text = 'Cache test';
|
|
50
|
+
|
|
51
|
+
const first = await service.embed(text);
|
|
52
|
+
expect(first.cached).toBe(false);
|
|
53
|
+
|
|
54
|
+
const second = await service.embed(text);
|
|
55
|
+
expect(second.cached).toBe(true);
|
|
56
|
+
|
|
57
|
+
// Same embedding
|
|
58
|
+
expect(Array.from(first.embedding)).toEqual(Array.from(second.embedding));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should generate deterministic embeddings', async () => {
|
|
62
|
+
const service2 = new LocalEmbeddingsService({
|
|
63
|
+
provider: 'mock',
|
|
64
|
+
cacheEnabled: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const text = 'Deterministic test';
|
|
68
|
+
const result1 = await service.embed(text);
|
|
69
|
+
|
|
70
|
+
// Clear cache by using new service
|
|
71
|
+
service.clearCache();
|
|
72
|
+
|
|
73
|
+
const result2 = await service2.embed(text);
|
|
74
|
+
|
|
75
|
+
// Same text should produce same embedding
|
|
76
|
+
expect(Array.from(result1.embedding)).toEqual(Array.from(result2.embedding));
|
|
77
|
+
|
|
78
|
+
await service2.shutdown();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle batch embeddings', async () => {
|
|
82
|
+
const texts = ['First', 'Second', 'Third'];
|
|
83
|
+
const results = await service.embedBatch(texts);
|
|
84
|
+
|
|
85
|
+
expect(results.length).toBe(3);
|
|
86
|
+
results.forEach((result, i) => {
|
|
87
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
88
|
+
expect(result.embedding.length).toBe(384);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should track statistics', async () => {
|
|
93
|
+
await service.embed('Text 1');
|
|
94
|
+
await service.embed('Text 2');
|
|
95
|
+
await service.embed('Text 1'); // Cache hit
|
|
96
|
+
|
|
97
|
+
const stats = service.getStats();
|
|
98
|
+
|
|
99
|
+
expect(stats.totalEmbeddings).toBe(2); // Only 2 unique
|
|
100
|
+
expect(stats.cacheHits).toBe(1);
|
|
101
|
+
expect(stats.cacheMisses).toBe(2);
|
|
102
|
+
expect(stats.modelLoaded).toBe(true);
|
|
103
|
+
expect(stats.provider).toBe('mock');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should clear cache', async () => {
|
|
107
|
+
await service.embed('Cache clear test');
|
|
108
|
+
service.clearCache();
|
|
109
|
+
|
|
110
|
+
const result = await service.embed('Cache clear test');
|
|
111
|
+
expect(result.cached).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should return embedding generator function', async () => {
|
|
115
|
+
const generator = service.getGenerator();
|
|
116
|
+
|
|
117
|
+
const embedding = await generator('Test content');
|
|
118
|
+
|
|
119
|
+
expect(embedding).toBeInstanceOf(Float32Array);
|
|
120
|
+
expect(embedding.length).toBe(384);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('createLocalEmbeddings factory', () => {
|
|
125
|
+
it('should create service with default config', () => {
|
|
126
|
+
const service = createLocalEmbeddings();
|
|
127
|
+
expect(service).toBeInstanceOf(LocalEmbeddingsService);
|
|
128
|
+
expect(service.getDimensions()).toBe(384);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should accept custom config', () => {
|
|
132
|
+
const service = createLocalEmbeddings({
|
|
133
|
+
dimensions: 768,
|
|
134
|
+
provider: 'mock',
|
|
135
|
+
maxCacheSize: 500,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(service.getDimensions()).toBe(768);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('createEmbeddingGenerator factory', () => {
|
|
143
|
+
it('should create embedding generator function', async () => {
|
|
144
|
+
const generator = await createEmbeddingGenerator({ provider: 'mock' });
|
|
145
|
+
|
|
146
|
+
const embedding = await generator('Hello world');
|
|
147
|
+
|
|
148
|
+
expect(embedding).toBeInstanceOf(Float32Array);
|
|
149
|
+
expect(embedding.length).toBe(384);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('cache disabled', () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
service = new LocalEmbeddingsService({
|
|
156
|
+
provider: 'mock',
|
|
157
|
+
cacheEnabled: false,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(async () => {
|
|
162
|
+
await service.shutdown();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should not cache when disabled', async () => {
|
|
166
|
+
const text = 'No cache test';
|
|
167
|
+
|
|
168
|
+
const first = await service.embed(text);
|
|
169
|
+
expect(first.cached).toBe(false);
|
|
170
|
+
|
|
171
|
+
const second = await service.embed(text);
|
|
172
|
+
expect(second.cached).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should still generate valid embeddings', async () => {
|
|
176
|
+
const result = await service.embed('Test');
|
|
177
|
+
|
|
178
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
179
|
+
expect(result.embedding.length).toBe(384);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('custom dimensions', () => {
|
|
184
|
+
it('should support 768 dimensions', async () => {
|
|
185
|
+
service = new LocalEmbeddingsService({
|
|
186
|
+
provider: 'mock',
|
|
187
|
+
dimensions: 768,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await service.embed('Test');
|
|
191
|
+
|
|
192
|
+
expect(result.embedding.length).toBe(768);
|
|
193
|
+
|
|
194
|
+
await service.shutdown();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should support 1024 dimensions', async () => {
|
|
198
|
+
service = new LocalEmbeddingsService({
|
|
199
|
+
provider: 'mock',
|
|
200
|
+
dimensions: 1024,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = await service.embed('Test');
|
|
204
|
+
|
|
205
|
+
expect(result.embedding.length).toBe(1024);
|
|
206
|
+
|
|
207
|
+
await service.shutdown();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Embedding Similarity', () => {
|
|
213
|
+
let service: LocalEmbeddingsService;
|
|
214
|
+
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
afterEach(async () => {
|
|
220
|
+
await service.shutdown();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should produce different embeddings for different texts', async () => {
|
|
224
|
+
const result1 = await service.embed('Hello world');
|
|
225
|
+
const result2 = await service.embed('Goodbye universe');
|
|
226
|
+
|
|
227
|
+
// At least some values should differ
|
|
228
|
+
let diffCount = 0;
|
|
229
|
+
for (let i = 0; i < result1.embedding.length; i++) {
|
|
230
|
+
if (result1.embedding[i] !== result2.embedding[i]) {
|
|
231
|
+
diffCount++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
expect(diffCount).toBeGreaterThan(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should produce same embedding for same text', async () => {
|
|
239
|
+
service.clearCache();
|
|
240
|
+
|
|
241
|
+
const result1 = await service.embed('Identical text');
|
|
242
|
+
service.clearCache();
|
|
243
|
+
const result2 = await service.embed('Identical text');
|
|
244
|
+
|
|
245
|
+
// All values should be the same
|
|
246
|
+
for (let i = 0; i < result1.embedding.length; i++) {
|
|
247
|
+
expect(result1.embedding[i]).toBe(result2.embedding[i]);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('LocalEmbeddingsService advanced', () => {
|
|
253
|
+
describe('cache eviction', () => {
|
|
254
|
+
it('should evict oldest entries when cache is full', async () => {
|
|
255
|
+
// Create service with tiny cache
|
|
256
|
+
const service = new LocalEmbeddingsService({
|
|
257
|
+
provider: 'mock',
|
|
258
|
+
maxCacheSize: 3,
|
|
259
|
+
cacheEnabled: true,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Fill the cache
|
|
263
|
+
await service.embed('Text 1');
|
|
264
|
+
await service.embed('Text 2');
|
|
265
|
+
await service.embed('Text 3');
|
|
266
|
+
|
|
267
|
+
// This should evict 'Text 1'
|
|
268
|
+
await service.embed('Text 4');
|
|
269
|
+
|
|
270
|
+
// 'Text 1' should no longer be cached
|
|
271
|
+
const result1 = await service.embed('Text 1');
|
|
272
|
+
expect(result1.cached).toBe(false);
|
|
273
|
+
|
|
274
|
+
// 'Text 4' should be cached
|
|
275
|
+
const result4 = await service.embed('Text 4');
|
|
276
|
+
expect(result4.cached).toBe(true);
|
|
277
|
+
|
|
278
|
+
await service.shutdown();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should update existing cache entries without evicting', async () => {
|
|
282
|
+
const service = new LocalEmbeddingsService({
|
|
283
|
+
provider: 'mock',
|
|
284
|
+
maxCacheSize: 3,
|
|
285
|
+
cacheEnabled: true,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await service.embed('Text 1');
|
|
289
|
+
await service.embed('Text 2');
|
|
290
|
+
await service.embed('Text 3');
|
|
291
|
+
|
|
292
|
+
// Access existing entry (should update, not evict)
|
|
293
|
+
const result = await service.embed('Text 2');
|
|
294
|
+
expect(result.cached).toBe(true);
|
|
295
|
+
|
|
296
|
+
await service.shutdown();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should move accessed entries to end of LRU', async () => {
|
|
300
|
+
const service = new LocalEmbeddingsService({
|
|
301
|
+
provider: 'mock',
|
|
302
|
+
maxCacheSize: 3,
|
|
303
|
+
cacheEnabled: true,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await service.embed('Text 1');
|
|
307
|
+
await service.embed('Text 2');
|
|
308
|
+
await service.embed('Text 3');
|
|
309
|
+
|
|
310
|
+
// Access 'Text 1' to move it to end
|
|
311
|
+
await service.embed('Text 1');
|
|
312
|
+
|
|
313
|
+
// Add new entry - should evict 'Text 2' (oldest after Text 1 access)
|
|
314
|
+
await service.embed('Text 4');
|
|
315
|
+
|
|
316
|
+
// 'Text 1' should still be cached
|
|
317
|
+
const result1 = await service.embed('Text 1');
|
|
318
|
+
expect(result1.cached).toBe(true);
|
|
319
|
+
|
|
320
|
+
// 'Text 2' should be evicted
|
|
321
|
+
const result2 = await service.embed('Text 2');
|
|
322
|
+
expect(result2.cached).toBe(false);
|
|
323
|
+
|
|
324
|
+
await service.shutdown();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('initialization', () => {
|
|
329
|
+
it('should handle double initialization', async () => {
|
|
330
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
331
|
+
|
|
332
|
+
await service.initialize();
|
|
333
|
+
await service.initialize(); // Should not throw
|
|
334
|
+
|
|
335
|
+
await service.shutdown();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle concurrent initialization', async () => {
|
|
339
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
340
|
+
|
|
341
|
+
// Start multiple initializations concurrently
|
|
342
|
+
const [result1, result2] = await Promise.all([
|
|
343
|
+
service.initialize(),
|
|
344
|
+
service.initialize(),
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
// Both should complete without error
|
|
348
|
+
expect(result1).toBeUndefined();
|
|
349
|
+
expect(result2).toBeUndefined();
|
|
350
|
+
|
|
351
|
+
await service.shutdown();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should initialize when using transformers provider without transformers installed', async () => {
|
|
355
|
+
// This tests the fallback to mock when transformers.js is not available
|
|
356
|
+
const service = new LocalEmbeddingsService({ provider: 'transformers' });
|
|
357
|
+
|
|
358
|
+
// Capture console.warn
|
|
359
|
+
const warnings: string[] = [];
|
|
360
|
+
const originalWarn = console.warn;
|
|
361
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
362
|
+
|
|
363
|
+
// The service will try to load transformers and fall back to mock
|
|
364
|
+
await service.initialize();
|
|
365
|
+
|
|
366
|
+
// Should have generated a warning about transformers not being available
|
|
367
|
+
// (only if transformers.js is not installed)
|
|
368
|
+
// If transformers IS installed, it will load successfully
|
|
369
|
+
|
|
370
|
+
const result = await service.embed('Test');
|
|
371
|
+
expect(result.embedding.length).toBeGreaterThan(0);
|
|
372
|
+
|
|
373
|
+
console.warn = originalWarn;
|
|
374
|
+
await service.shutdown();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('stats tracking', () => {
|
|
379
|
+
it('should track average time correctly', async () => {
|
|
380
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
381
|
+
|
|
382
|
+
await service.embed('Text 1');
|
|
383
|
+
await service.embed('Text 2');
|
|
384
|
+
|
|
385
|
+
const stats = service.getStats();
|
|
386
|
+
expect(stats.avgTimeMs).toBeGreaterThanOrEqual(0);
|
|
387
|
+
expect(stats.totalTimeMs).toBeGreaterThanOrEqual(0);
|
|
388
|
+
|
|
389
|
+
await service.shutdown();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should report zero average time when no embeddings', () => {
|
|
393
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
394
|
+
|
|
395
|
+
const stats = service.getStats();
|
|
396
|
+
expect(stats.avgTimeMs).toBe(0);
|
|
397
|
+
expect(stats.totalEmbeddings).toBe(0);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('shutdown', () => {
|
|
402
|
+
it('should clear state on shutdown', async () => {
|
|
403
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
404
|
+
|
|
405
|
+
await service.embed('Test');
|
|
406
|
+
await service.shutdown();
|
|
407
|
+
|
|
408
|
+
// After shutdown, should start fresh
|
|
409
|
+
const result = await service.embed('Test');
|
|
410
|
+
expect(result.cached).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe('configuration', () => {
|
|
415
|
+
it('should use custom model ID', () => {
|
|
416
|
+
const service = new LocalEmbeddingsService({
|
|
417
|
+
provider: 'mock',
|
|
418
|
+
modelId: 'custom/model',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Model ID is stored in config (accessed via getStats)
|
|
422
|
+
const stats = service.getStats();
|
|
423
|
+
expect(stats.provider).toBe('mock');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should use custom cache directory', () => {
|
|
427
|
+
const service = new LocalEmbeddingsService({
|
|
428
|
+
provider: 'mock',
|
|
429
|
+
cacheDir: '/custom/cache',
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(service.getDimensions()).toBe(384);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should handle showProgress option', async () => {
|
|
436
|
+
const service = new LocalEmbeddingsService({
|
|
437
|
+
provider: 'mock',
|
|
438
|
+
showProgress: true,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await service.initialize();
|
|
442
|
+
// Mock provider doesn't actually show progress, but config is accepted
|
|
443
|
+
|
|
444
|
+
await service.shutdown();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('edge cases', () => {
|
|
449
|
+
it('should handle empty string', async () => {
|
|
450
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
451
|
+
|
|
452
|
+
const result = await service.embed('');
|
|
453
|
+
|
|
454
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
455
|
+
expect(result.embedding.length).toBe(384);
|
|
456
|
+
|
|
457
|
+
await service.shutdown();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should handle very long text', async () => {
|
|
461
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
462
|
+
|
|
463
|
+
const longText = 'a'.repeat(10000);
|
|
464
|
+
const result = await service.embed(longText);
|
|
465
|
+
|
|
466
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
467
|
+
expect(result.embedding.length).toBe(384);
|
|
468
|
+
|
|
469
|
+
await service.shutdown();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should handle unicode text', async () => {
|
|
473
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
474
|
+
|
|
475
|
+
const unicodeText = '日本語テスト 中文测试 한국어 테스트 🎉';
|
|
476
|
+
const result = await service.embed(unicodeText);
|
|
477
|
+
|
|
478
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
479
|
+
expect(result.embedding.length).toBe(384);
|
|
480
|
+
|
|
481
|
+
await service.shutdown();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should handle special characters', async () => {
|
|
485
|
+
const service = new LocalEmbeddingsService({ provider: 'mock' });
|
|
486
|
+
|
|
487
|
+
const specialText = '!@#$%^&*()[]{}|\\;:\'",.<>?/`~';
|
|
488
|
+
const result = await service.embed(specialText);
|
|
489
|
+
|
|
490
|
+
expect(result.embedding).toBeInstanceOf(Float32Array);
|
|
491
|
+
|
|
492
|
+
await service.shutdown();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|