@dangao/bun-server 1.12.1 → 2.0.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/README.md +32 -0
- package/dist/ai/ai-module.d.ts +24 -0
- package/dist/ai/ai-module.d.ts.map +1 -0
- package/dist/ai/decorators.d.ts +25 -0
- package/dist/ai/decorators.d.ts.map +1 -0
- package/dist/ai/errors.d.ts +39 -0
- package/dist/ai/errors.d.ts.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/providers/anthropic-provider.d.ts +23 -0
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/providers/google-provider.d.ts +20 -0
- package/dist/ai/providers/google-provider.d.ts.map +1 -0
- package/dist/ai/providers/ollama-provider.d.ts +17 -0
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
- package/dist/ai/providers/openai-provider.d.ts +28 -0
- package/dist/ai/providers/openai-provider.d.ts.map +1 -0
- package/dist/ai/service.d.ts +40 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/tools/tool-executor.d.ts +15 -0
- package/dist/ai/tools/tool-executor.d.ts.map +1 -0
- package/dist/ai/tools/tool-registry.d.ts +39 -0
- package/dist/ai/tools/tool-registry.d.ts.map +1 -0
- package/dist/ai/types.d.ts +134 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai-guard/ai-guard-module.d.ts +18 -0
- package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
- package/dist/ai-guard/decorators.d.ts +16 -0
- package/dist/ai-guard/decorators.d.ts.map +1 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
- package/dist/ai-guard/index.d.ts +8 -0
- package/dist/ai-guard/index.d.ts.map +1 -0
- package/dist/ai-guard/service.d.ts +21 -0
- package/dist/ai-guard/service.d.ts.map +1 -0
- package/dist/ai-guard/types.d.ts +59 -0
- package/dist/ai-guard/types.d.ts.map +1 -0
- package/dist/conversation/conversation-module.d.ts +25 -0
- package/dist/conversation/conversation-module.d.ts.map +1 -0
- package/dist/conversation/decorators.d.ts +28 -0
- package/dist/conversation/decorators.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +8 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/service.d.ts +43 -0
- package/dist/conversation/service.d.ts.map +1 -0
- package/dist/conversation/stores/database-store.d.ts +46 -0
- package/dist/conversation/stores/database-store.d.ts.map +1 -0
- package/dist/conversation/stores/memory-store.d.ts +17 -0
- package/dist/conversation/stores/memory-store.d.ts.map +1 -0
- package/dist/conversation/stores/redis-store.d.ts +39 -0
- package/dist/conversation/stores/redis-store.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +64 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/embedding/embedding-module.d.ts +20 -0
- package/dist/embedding/embedding-module.d.ts.map +1 -0
- package/dist/embedding/index.d.ts +6 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/service.d.ts +27 -0
- package/dist/embedding/service.d.ts.map +1 -0
- package/dist/embedding/types.d.ts +25 -0
- package/dist/embedding/types.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2638 -1
- package/dist/mcp/decorators.d.ts +42 -0
- package/dist/mcp/decorators.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/mcp-module.d.ts +22 -0
- package/dist/mcp/mcp-module.d.ts.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/types.d.ts +60 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/prompt-module.d.ts +23 -0
- package/dist/prompt/prompt-module.d.ts.map +1 -0
- package/dist/prompt/service.d.ts +47 -0
- package/dist/prompt/service.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts +36 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -0
- package/dist/prompt/stores/memory-store.d.ts +17 -0
- package/dist/prompt/stores/memory-store.d.ts.map +1 -0
- package/dist/prompt/types.d.ts +68 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
- package/dist/rag/chunkers/text-chunker.d.ts +11 -0
- package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
- package/dist/rag/decorators.d.ts +24 -0
- package/dist/rag/decorators.d.ts.map +1 -0
- package/dist/rag/index.d.ts +7 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/rag-module.d.ts +23 -0
- package/dist/rag/rag-module.d.ts.map +1 -0
- package/dist/rag/service.d.ts +36 -0
- package/dist/rag/service.d.ts.map +1 -0
- package/dist/rag/types.d.ts +56 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/vector-store/index.d.ts +6 -0
- package/dist/vector-store/index.d.ts.map +1 -0
- package/dist/vector-store/stores/memory-store.d.ts +17 -0
- package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
- package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
- package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
- package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
- package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
- package/dist/vector-store/types.d.ts +60 -0
- package/dist/vector-store/types.d.ts.map +1 -0
- package/dist/vector-store/vector-store-module.d.ts +20 -0
- package/dist/vector-store/vector-store-module.d.ts.map +1 -0
- package/docs/ai.md +500 -0
- package/docs/best-practices.md +83 -8
- package/docs/database.md +23 -0
- package/docs/guide.md +90 -27
- package/docs/migration.md +81 -7
- package/docs/security.md +23 -0
- package/docs/zh/ai.md +441 -0
- package/docs/zh/best-practices.md +43 -0
- package/docs/zh/database.md +23 -0
- package/docs/zh/guide.md +40 -1
- package/docs/zh/migration.md +39 -0
- package/docs/zh/security.md +23 -0
- package/package.json +3 -3
- package/src/ai/ai-module.ts +62 -0
- package/src/ai/decorators.ts +30 -0
- package/src/ai/errors.ts +71 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/providers/anthropic-provider.ts +190 -0
- package/src/ai/providers/google-provider.ts +179 -0
- package/src/ai/providers/ollama-provider.ts +126 -0
- package/src/ai/providers/openai-provider.ts +242 -0
- package/src/ai/service.ts +155 -0
- package/src/ai/tools/tool-executor.ts +38 -0
- package/src/ai/tools/tool-registry.ts +91 -0
- package/src/ai/types.ts +145 -0
- package/src/ai-guard/ai-guard-module.ts +50 -0
- package/src/ai-guard/decorators.ts +21 -0
- package/src/ai-guard/detectors/content-moderator.ts +80 -0
- package/src/ai-guard/detectors/injection-detector.ts +48 -0
- package/src/ai-guard/detectors/pii-detector.ts +64 -0
- package/src/ai-guard/index.ts +7 -0
- package/src/ai-guard/service.ts +100 -0
- package/src/ai-guard/types.ts +61 -0
- package/src/conversation/conversation-module.ts +63 -0
- package/src/conversation/decorators.ts +47 -0
- package/src/conversation/index.ts +7 -0
- package/src/conversation/service.ts +133 -0
- package/src/conversation/stores/database-store.ts +125 -0
- package/src/conversation/stores/memory-store.ts +57 -0
- package/src/conversation/stores/redis-store.ts +101 -0
- package/src/conversation/types.ts +68 -0
- package/src/embedding/embedding-module.ts +52 -0
- package/src/embedding/index.ts +5 -0
- package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
- package/src/embedding/providers/openai-embedding-provider.ts +47 -0
- package/src/embedding/service.ts +55 -0
- package/src/embedding/types.ts +27 -0
- package/src/index.ts +10 -0
- package/src/mcp/decorators.ts +60 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/mcp-module.ts +58 -0
- package/src/mcp/registry.ts +72 -0
- package/src/mcp/server.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/prompt/index.ts +5 -0
- package/src/prompt/prompt-module.ts +61 -0
- package/src/prompt/service.ts +93 -0
- package/src/prompt/stores/file-store.ts +135 -0
- package/src/prompt/stores/memory-store.ts +82 -0
- package/src/prompt/types.ts +84 -0
- package/src/rag/chunkers/markdown-chunker.ts +40 -0
- package/src/rag/chunkers/text-chunker.ts +30 -0
- package/src/rag/decorators.ts +26 -0
- package/src/rag/index.ts +6 -0
- package/src/rag/rag-module.ts +78 -0
- package/src/rag/service.ts +134 -0
- package/src/rag/types.ts +47 -0
- package/src/vector-store/index.ts +5 -0
- package/src/vector-store/stores/memory-store.ts +69 -0
- package/src/vector-store/stores/pinecone-store.ts +123 -0
- package/src/vector-store/stores/qdrant-store.ts +147 -0
- package/src/vector-store/types.ts +77 -0
- package/src/vector-store/vector-store-module.ts +50 -0
- package/tests/ai/ai-module.test.ts +46 -0
- package/tests/ai/ai-service.test.ts +91 -0
- package/tests/ai/tool-registry.test.ts +57 -0
- package/tests/ai-guard/ai-guard-module.test.ts +23 -0
- package/tests/ai-guard/content-moderator.test.ts +65 -0
- package/tests/ai-guard/pii-detector.test.ts +41 -0
- package/tests/conversation/conversation-module.test.ts +26 -0
- package/tests/conversation/conversation-service.test.ts +64 -0
- package/tests/conversation/memory-store.test.ts +68 -0
- package/tests/embedding/embedding-service.test.ts +55 -0
- package/tests/mcp/mcp-server.test.ts +85 -0
- package/tests/prompt/prompt-module.test.ts +30 -0
- package/tests/prompt/prompt-service.test.ts +74 -0
- package/tests/rag/chunkers.test.ts +58 -0
- package/tests/rag/rag-service.test.ts +66 -0
- package/tests/vector-store/memory-vector-store.test.ts +84 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
- package/tests/perf/optimization.test.ts +0 -182
- package/tests/perf/regression.test.ts +0 -120
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { PiiDetector } from '../../src/ai-guard/detectors/pii-detector';
|
|
3
|
+
|
|
4
|
+
describe('PiiDetector', () => {
|
|
5
|
+
const detector = new PiiDetector();
|
|
6
|
+
|
|
7
|
+
test('should detect email addresses', () => {
|
|
8
|
+
const result = detector.detect('Contact me at alice@example.com for info.');
|
|
9
|
+
expect(result.detected).toBe(true);
|
|
10
|
+
expect(result.types).toContain('email');
|
|
11
|
+
expect(result.sanitized).toContain('[EMAIL]');
|
|
12
|
+
expect(result.sanitized).not.toContain('alice@example.com');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should detect phone numbers', () => {
|
|
16
|
+
const result = detector.detect('Call me at 555-123-4567');
|
|
17
|
+
expect(result.detected).toBe(true);
|
|
18
|
+
expect(result.types).toContain('phone');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should detect SSNs', () => {
|
|
22
|
+
const result = detector.detect('My SSN is 123-45-6789');
|
|
23
|
+
expect(result.detected).toBe(true);
|
|
24
|
+
expect(result.types).toContain('ssn');
|
|
25
|
+
expect(result.sanitized).toContain('[SSN]');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should return original text if no PII found', () => {
|
|
29
|
+
const text = 'This is a safe message with no personal information.';
|
|
30
|
+
const result = detector.detect(text);
|
|
31
|
+
expect(result.detected).toBe(false);
|
|
32
|
+
expect(result.sanitized).toBe(text);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should not redact when redact=false', () => {
|
|
36
|
+
const text = 'Email: test@test.com';
|
|
37
|
+
const result = detector.detect(text, false);
|
|
38
|
+
expect(result.detected).toBe(true);
|
|
39
|
+
expect(result.sanitized).toBe(text);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { ConversationModule } from '../../src/conversation/conversation-module';
|
|
3
|
+
import { CONVERSATION_SERVICE_TOKEN } from '../../src/conversation/types';
|
|
4
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
5
|
+
|
|
6
|
+
describe('ConversationModule', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
ConversationModule.reset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should register providers on forRoot()', () => {
|
|
12
|
+
ConversationModule.forRoot({ maxMessages: 20 });
|
|
13
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, ConversationModule);
|
|
14
|
+
expect(metadata).toBeDefined();
|
|
15
|
+
expect(metadata.exports).toContain(CONVERSATION_SERVICE_TOKEN);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should use MemoryConversationStore by default', () => {
|
|
19
|
+
ConversationModule.forRoot();
|
|
20
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, ConversationModule);
|
|
21
|
+
const optionsProvider = metadata.providers.find(
|
|
22
|
+
(p: { provide: symbol }) => p.provide === Symbol.for('@dangao/bun-server:conversation:options') || true,
|
|
23
|
+
);
|
|
24
|
+
expect(optionsProvider).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { ConversationService } from '../../src/conversation/service';
|
|
3
|
+
import { MemoryConversationStore } from '../../src/conversation/stores/memory-store';
|
|
4
|
+
|
|
5
|
+
function createService(opts = {}) {
|
|
6
|
+
const store = new MemoryConversationStore();
|
|
7
|
+
return new ConversationService({ store, maxMessages: 10, autoTrim: true, ...opts } as never);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('ConversationService', () => {
|
|
11
|
+
test('should create a conversation', async () => {
|
|
12
|
+
const service = createService();
|
|
13
|
+
const conv = await service.create({ userId: 'u1' });
|
|
14
|
+
expect(conv.id).toBeDefined();
|
|
15
|
+
expect(conv.metadata['userId']).toBe('u1');
|
|
16
|
+
expect(conv.messages).toHaveLength(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should append messages', async () => {
|
|
20
|
+
const service = createService();
|
|
21
|
+
const conv = await service.create();
|
|
22
|
+
await service.appendMessage(conv.id, { role: 'user', content: 'Hello' });
|
|
23
|
+
await service.appendMessage(conv.id, { role: 'assistant', content: 'Hi!' });
|
|
24
|
+
|
|
25
|
+
const history = await service.getHistory(conv.id);
|
|
26
|
+
expect(history).toHaveLength(2);
|
|
27
|
+
expect(history[0]!.content).toBe('Hello');
|
|
28
|
+
expect(history[1]!.content).toBe('Hi!');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should auto-trim when maxMessages exceeded', async () => {
|
|
32
|
+
const service = createService({ maxMessages: 3, autoTrim: true });
|
|
33
|
+
const conv = await service.create();
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < 5; i++) {
|
|
36
|
+
await service.appendMessage(conv.id, { role: 'user', content: `Message ${i}` });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const history = await service.getHistory(conv.id);
|
|
40
|
+
expect(history.length).toBeLessThanOrEqual(3);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should delete a conversation', async () => {
|
|
44
|
+
const service = createService();
|
|
45
|
+
const conv = await service.create();
|
|
46
|
+
const deleted = await service.delete(conv.id);
|
|
47
|
+
expect(deleted).toBe(true);
|
|
48
|
+
expect(await service.get(conv.id)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should list conversation ids', async () => {
|
|
52
|
+
const service = createService();
|
|
53
|
+
await service.create();
|
|
54
|
+
await service.create();
|
|
55
|
+
const ids = await service.list();
|
|
56
|
+
expect(ids.length).toBe(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should return null for missing conversation', async () => {
|
|
60
|
+
const service = createService();
|
|
61
|
+
const result = await service.get('non-existent');
|
|
62
|
+
expect(result).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { MemoryConversationStore } from '../../src/conversation/stores/memory-store';
|
|
3
|
+
|
|
4
|
+
describe('MemoryConversationStore', () => {
|
|
5
|
+
test('should create and retrieve conversation', async () => {
|
|
6
|
+
const store = new MemoryConversationStore();
|
|
7
|
+
const conv = await store.create({ tag: 'test' });
|
|
8
|
+
|
|
9
|
+
expect(conv.id).toBeDefined();
|
|
10
|
+
expect(conv.messages).toHaveLength(0);
|
|
11
|
+
|
|
12
|
+
const retrieved = await store.get(conv.id);
|
|
13
|
+
expect(retrieved).not.toBeNull();
|
|
14
|
+
expect(retrieved!.id).toBe(conv.id);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should append messages', async () => {
|
|
18
|
+
const store = new MemoryConversationStore();
|
|
19
|
+
const conv = await store.create();
|
|
20
|
+
await store.appendMessage(conv.id, { role: 'user', content: 'Hello' });
|
|
21
|
+
|
|
22
|
+
const retrieved = await store.get(conv.id);
|
|
23
|
+
expect(retrieved!.messages).toHaveLength(1);
|
|
24
|
+
expect(retrieved!.messages[0]!.content).toBe('Hello');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should trim old messages', async () => {
|
|
28
|
+
const store = new MemoryConversationStore();
|
|
29
|
+
const conv = await store.create();
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < 5; i++) {
|
|
32
|
+
await store.appendMessage(conv.id, { role: 'user', content: `msg${i}` });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await store.trim(conv.id, 3);
|
|
36
|
+
const retrieved = await store.get(conv.id);
|
|
37
|
+
expect(retrieved!.messages).toHaveLength(3);
|
|
38
|
+
// Should keep the most recent
|
|
39
|
+
expect(retrieved!.messages[0]!.content).toBe('msg2');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should delete conversation', async () => {
|
|
43
|
+
const store = new MemoryConversationStore();
|
|
44
|
+
const conv = await store.create();
|
|
45
|
+
const deleted = await store.delete(conv.id);
|
|
46
|
+
expect(deleted).toBe(true);
|
|
47
|
+
expect(await store.get(conv.id)).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should list all conversations', async () => {
|
|
51
|
+
const store = new MemoryConversationStore();
|
|
52
|
+
await store.create();
|
|
53
|
+
await store.create();
|
|
54
|
+
await store.create();
|
|
55
|
+
const ids = await store.list();
|
|
56
|
+
expect(ids).toHaveLength(3);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should return null for missing id', async () => {
|
|
60
|
+
const store = new MemoryConversationStore();
|
|
61
|
+
expect(await store.get('missing')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should throw on appendMessage to missing conversation', async () => {
|
|
65
|
+
const store = new MemoryConversationStore();
|
|
66
|
+
expect(store.appendMessage('missing', { role: 'user', content: 'hi' })).rejects.toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { EmbeddingService } from '../../src/embedding/service';
|
|
3
|
+
|
|
4
|
+
class MockEmbeddingProvider {
|
|
5
|
+
readonly name = 'mock';
|
|
6
|
+
readonly dimensions = 4;
|
|
7
|
+
|
|
8
|
+
async embed(text: string): Promise<number[]> {
|
|
9
|
+
return [text.length / 100, 0.2, 0.3, 0.4];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
13
|
+
return texts.map((t) => [t.length / 100, 0.2, 0.3, 0.4]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createService(batchSize = 100): EmbeddingService {
|
|
18
|
+
return new EmbeddingService({
|
|
19
|
+
provider: { name: 'mock', provider: MockEmbeddingProvider as never, config: {} },
|
|
20
|
+
batchSize,
|
|
21
|
+
} as never);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('EmbeddingService', () => {
|
|
25
|
+
test('should embed a single text', async () => {
|
|
26
|
+
const service = createService();
|
|
27
|
+
const vec = await service.embed('hello');
|
|
28
|
+
expect(vec).toHaveLength(4);
|
|
29
|
+
expect(Array.isArray(vec)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should embedBatch multiple texts', async () => {
|
|
33
|
+
const service = createService();
|
|
34
|
+
const vecs = await service.embedBatch(['hello', 'world', 'foo']);
|
|
35
|
+
expect(vecs).toHaveLength(3);
|
|
36
|
+
expect(vecs[0]).toHaveLength(4);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should report correct dimensions', () => {
|
|
40
|
+
const service = createService();
|
|
41
|
+
expect(service.dimensions).toBe(4);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should report provider name', () => {
|
|
45
|
+
const service = createService();
|
|
46
|
+
expect(service.providerName).toBe('mock');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should handle batch larger than batchSize', async () => {
|
|
50
|
+
const service = createService(2);
|
|
51
|
+
const texts = ['a', 'b', 'c', 'd', 'e'];
|
|
52
|
+
const vecs = await service.embedBatch(texts);
|
|
53
|
+
expect(vecs).toHaveLength(5);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { McpServer } from '../../src/mcp/server';
|
|
3
|
+
import { McpRegistry } from '../../src/mcp/registry';
|
|
4
|
+
import { McpTool } from '../../src/mcp/decorators';
|
|
5
|
+
import type { McpServerInfo } from '../../src/mcp/types';
|
|
6
|
+
|
|
7
|
+
const serverInfo: McpServerInfo = { name: 'test-server', version: '1.0.0' };
|
|
8
|
+
|
|
9
|
+
function createServer(): { server: McpServer; registry: McpRegistry } {
|
|
10
|
+
const registry = new McpRegistry();
|
|
11
|
+
const server = new McpServer(registry, serverInfo);
|
|
12
|
+
return { server, registry };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('McpServer', () => {
|
|
16
|
+
test('should handle initialize request', async () => {
|
|
17
|
+
const { server } = createServer();
|
|
18
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
19
|
+
|
|
20
|
+
expect(response.error).toBeUndefined();
|
|
21
|
+
expect((response.result as Record<string, unknown>)['protocolVersion']).toBeDefined();
|
|
22
|
+
expect((response.result as Record<string, unknown>)['serverInfo']).toMatchObject(serverInfo);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should list tools', async () => {
|
|
26
|
+
const { server, registry } = createServer();
|
|
27
|
+
|
|
28
|
+
class MyTools {
|
|
29
|
+
@McpTool({ name: 'echo', description: 'Echo input', inputSchema: { type: 'object' } })
|
|
30
|
+
async echo({ text }: { text: string }) { return text; }
|
|
31
|
+
}
|
|
32
|
+
registry.scan(new MyTools());
|
|
33
|
+
|
|
34
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
|
|
35
|
+
const tools = (response.result as { tools: unknown[] }).tools;
|
|
36
|
+
expect(tools).toHaveLength(1);
|
|
37
|
+
expect((tools[0] as Record<string, unknown>)['name']).toBe('echo');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should call a tool', async () => {
|
|
41
|
+
const { server, registry } = createServer();
|
|
42
|
+
|
|
43
|
+
class EchoTool {
|
|
44
|
+
@McpTool({ name: 'echo', description: 'Echo', inputSchema: {} })
|
|
45
|
+
async echo(args: { text: string }) { return `Echo: ${args.text}`; }
|
|
46
|
+
}
|
|
47
|
+
registry.scan(new EchoTool());
|
|
48
|
+
|
|
49
|
+
const response = await server.handle({
|
|
50
|
+
jsonrpc: '2.0',
|
|
51
|
+
id: 3,
|
|
52
|
+
method: 'tools/call',
|
|
53
|
+
params: { name: 'echo', arguments: { text: 'Hello' } },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(response.error).toBeUndefined();
|
|
57
|
+
const content = (response.result as { content: Array<{ text: string }> }).content;
|
|
58
|
+
expect(content[0]!.text).toContain('Hello');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should return error for unknown method', async () => {
|
|
62
|
+
const { server } = createServer();
|
|
63
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 4, method: 'unknown/method' });
|
|
64
|
+
expect(response.error).toBeDefined();
|
|
65
|
+
expect(response.error!.code).toBe(-32601);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should return error for unknown tool call', async () => {
|
|
69
|
+
const { server } = createServer();
|
|
70
|
+
const response = await server.handle({
|
|
71
|
+
jsonrpc: '2.0',
|
|
72
|
+
id: 5,
|
|
73
|
+
method: 'tools/call',
|
|
74
|
+
params: { name: 'non-existent', arguments: {} },
|
|
75
|
+
});
|
|
76
|
+
expect(response.error).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should handle ping', async () => {
|
|
80
|
+
const { server } = createServer();
|
|
81
|
+
const response = await server.handle({ jsonrpc: '2.0', id: 6, method: 'ping' });
|
|
82
|
+
expect(response.error).toBeUndefined();
|
|
83
|
+
expect(response.result).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import { PromptModule } from '../../src/prompt/prompt-module';
|
|
3
|
+
import { PROMPT_SERVICE_TOKEN } from '../../src/prompt/types';
|
|
4
|
+
import { MODULE_METADATA_KEY } from '../../src/di/module';
|
|
5
|
+
|
|
6
|
+
describe('PromptModule', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
PromptModule.reset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should register providers on forRoot()', () => {
|
|
12
|
+
PromptModule.forRoot();
|
|
13
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
14
|
+
expect(metadata).toBeDefined();
|
|
15
|
+
expect(metadata.exports).toContain(PROMPT_SERVICE_TOKEN);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should use InMemoryPromptStore by default', () => {
|
|
19
|
+
PromptModule.forRoot();
|
|
20
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
21
|
+
expect(metadata.providers.length).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('reset() should clear module metadata', () => {
|
|
25
|
+
PromptModule.forRoot();
|
|
26
|
+
PromptModule.reset();
|
|
27
|
+
const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, PromptModule);
|
|
28
|
+
expect(metadata).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { PromptService } from '../../src/prompt/service';
|
|
3
|
+
import { InMemoryPromptStore } from '../../src/prompt/stores/memory-store';
|
|
4
|
+
|
|
5
|
+
function createService(): PromptService {
|
|
6
|
+
return new PromptService({ store: new InMemoryPromptStore() } as never);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('PromptService', () => {
|
|
10
|
+
test('should create and retrieve a template', async () => {
|
|
11
|
+
const service = createService();
|
|
12
|
+
const created = await service.create({ name: 'greeting', content: 'Hello, {{name}}!' });
|
|
13
|
+
|
|
14
|
+
expect(created.id).toBeDefined();
|
|
15
|
+
expect(created.version).toBe(1);
|
|
16
|
+
expect(created.variables).toContain('name');
|
|
17
|
+
|
|
18
|
+
const fetched = await service.get(created.id);
|
|
19
|
+
expect(fetched.content).toBe('Hello, {{name}}!');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should render template with variables', async () => {
|
|
23
|
+
const service = createService();
|
|
24
|
+
const template = await service.create({ name: 't', content: 'Hi {{name}}, welcome to {{app}}!' });
|
|
25
|
+
const rendered = await service.render(template.id, { name: 'Alice', app: 'MyApp' });
|
|
26
|
+
expect(rendered).toBe('Hi Alice, welcome to MyApp!');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should leave unresolved variables as-is', async () => {
|
|
30
|
+
const service = createService();
|
|
31
|
+
const template = await service.create({ name: 't', content: 'Hello {{name}}, from {{sender}}!' });
|
|
32
|
+
const rendered = await service.render(template.id, { name: 'Bob' });
|
|
33
|
+
expect(rendered).toBe('Hello Bob, from {{sender}}!');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should increment version on update', async () => {
|
|
37
|
+
const service = createService();
|
|
38
|
+
const template = await service.create({ name: 't', content: 'v1 {{x}}' });
|
|
39
|
+
const updated = await service.update(template.id, { content: 'v2 {{x}} {{y}}' });
|
|
40
|
+
expect(updated.version).toBe(2);
|
|
41
|
+
expect(updated.variables).toContain('y');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should retrieve specific version', async () => {
|
|
45
|
+
const service = createService();
|
|
46
|
+
const template = await service.create({ name: 't', content: 'v1' });
|
|
47
|
+
await service.update(template.id, { content: 'v2' });
|
|
48
|
+
|
|
49
|
+
const v1 = await service.getVersion(template.id, 1);
|
|
50
|
+
expect(v1.content).toBe('v1');
|
|
51
|
+
const v2 = await service.getVersion(template.id, 2);
|
|
52
|
+
expect(v2.content).toBe('v2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should list all templates', async () => {
|
|
56
|
+
const service = createService();
|
|
57
|
+
await service.create({ name: 'a', content: 'A' });
|
|
58
|
+
await service.create({ name: 'b', content: 'B' });
|
|
59
|
+
const list = await service.list();
|
|
60
|
+
expect(list).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should delete template', async () => {
|
|
64
|
+
const service = createService();
|
|
65
|
+
const template = await service.create({ name: 'd', content: 'delete me' });
|
|
66
|
+
await service.delete(template.id);
|
|
67
|
+
expect(service.get(template.id)).rejects.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should throw 404 for missing template', async () => {
|
|
71
|
+
const service = createService();
|
|
72
|
+
expect(service.get('non-existent')).rejects.toThrow('not found');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { TextChunker } from '../../src/rag/chunkers/text-chunker';
|
|
3
|
+
import { MarkdownChunker } from '../../src/rag/chunkers/markdown-chunker';
|
|
4
|
+
|
|
5
|
+
describe('TextChunker', () => {
|
|
6
|
+
test('should split text into chunks of specified size', () => {
|
|
7
|
+
const chunker = new TextChunker(10, 0);
|
|
8
|
+
const chunks = chunker.chunk('Hello World this is a test string');
|
|
9
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
10
|
+
for (const chunk of chunks) {
|
|
11
|
+
expect(chunk.content.length).toBeLessThanOrEqual(10);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should include overlap between chunks', () => {
|
|
16
|
+
const chunker = new TextChunker(20, 5);
|
|
17
|
+
const text = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
18
|
+
const chunks = chunker.chunk(text);
|
|
19
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should return single chunk for short text', () => {
|
|
23
|
+
const chunker = new TextChunker(512, 50);
|
|
24
|
+
const chunks = chunker.chunk('Short text');
|
|
25
|
+
expect(chunks).toHaveLength(1);
|
|
26
|
+
expect(chunks[0]!.content).toBe('Short text');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should not return empty chunks', () => {
|
|
30
|
+
const chunker = new TextChunker(10, 0);
|
|
31
|
+
const chunks = chunker.chunk(' \n\n ');
|
|
32
|
+
expect(chunks).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('MarkdownChunker', () => {
|
|
37
|
+
test('should split by headings', () => {
|
|
38
|
+
const chunker = new MarkdownChunker(1000);
|
|
39
|
+
const markdown = `# Title\n\nIntro text.\n\n## Section 1\n\nContent 1.\n\n## Section 2\n\nContent 2.`;
|
|
40
|
+
const chunks = chunker.chunk(markdown);
|
|
41
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should handle text without headings', () => {
|
|
45
|
+
const chunker = new MarkdownChunker(1000);
|
|
46
|
+
const text = 'Just plain text without any headings.';
|
|
47
|
+
const chunks = chunker.chunk(text);
|
|
48
|
+
expect(chunks).toHaveLength(1);
|
|
49
|
+
expect(chunks[0]!.content).toBe(text);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should split large sections into paragraphs', () => {
|
|
53
|
+
const chunker = new MarkdownChunker(50);
|
|
54
|
+
const markdown = `## Big Section\n\nParagraph one with some content.\n\nParagraph two with more content that makes it too long.`;
|
|
55
|
+
const chunks = chunker.chunk(markdown);
|
|
56
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { RagService } from '../../src/rag/service';
|
|
3
|
+
import { MemoryVectorStore } from '../../src/vector-store/stores/memory-store';
|
|
4
|
+
import { EmbeddingService } from '../../src/embedding/service';
|
|
5
|
+
|
|
6
|
+
class MockEmbeddingProvider {
|
|
7
|
+
readonly name = 'mock';
|
|
8
|
+
readonly dimensions = 4;
|
|
9
|
+
|
|
10
|
+
async embed(text: string): Promise<number[]> {
|
|
11
|
+
const codes = text.split('').slice(0, 4).map((c) => c.charCodeAt(0) / 255);
|
|
12
|
+
while (codes.length < 4) codes.push(0);
|
|
13
|
+
return codes;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
17
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createRagService(): RagService {
|
|
22
|
+
const options = { collection: 'test', chunkSize: 100, chunkOverlap: 10, topK: 3, minScore: 0 };
|
|
23
|
+
const embeddingService = new EmbeddingService({
|
|
24
|
+
provider: { name: 'mock', provider: MockEmbeddingProvider as never, config: {} },
|
|
25
|
+
} as never);
|
|
26
|
+
const vectorStore = new MemoryVectorStore();
|
|
27
|
+
|
|
28
|
+
return new RagService(options as never, embeddingService, vectorStore);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('RagService', () => {
|
|
32
|
+
test('should ingest text and return chunk count', async () => {
|
|
33
|
+
const service = createRagService();
|
|
34
|
+
const count = await service.ingest({ type: 'text', content: 'Hello world, this is a test document for RAG.' });
|
|
35
|
+
expect(count).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should retrieve context after ingestion', async () => {
|
|
39
|
+
const service = createRagService();
|
|
40
|
+
await service.ingest({ type: 'text', content: 'The sky is blue and the grass is green.' });
|
|
41
|
+
const context = await service.retrieve('sky');
|
|
42
|
+
expect(context.chunks.length).toBeGreaterThanOrEqual(0);
|
|
43
|
+
expect(typeof context.formatted).toBe('string');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should return empty context for empty store', async () => {
|
|
47
|
+
const service = createRagService();
|
|
48
|
+
const context = await service.retrieve('anything');
|
|
49
|
+
expect(context.chunks).toHaveLength(0);
|
|
50
|
+
expect(context.formatted).toBe('');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should build context prompt string', async () => {
|
|
54
|
+
const service = createRagService();
|
|
55
|
+
await service.ingest({ type: 'text', content: 'Water is H2O.' });
|
|
56
|
+
const prompt = await service.buildContextPrompt('water');
|
|
57
|
+
expect(typeof prompt).toBe('string');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should ingest multiple documents', async () => {
|
|
61
|
+
const service = createRagService();
|
|
62
|
+
const count1 = await service.ingest({ type: 'text', content: 'Document one content.' });
|
|
63
|
+
const count2 = await service.ingest({ type: 'text', content: 'Document two content.' });
|
|
64
|
+
expect(count1 + count2).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { MemoryVectorStore } from '../../src/vector-store/stores/memory-store';
|
|
3
|
+
import { cosineSimilarity } from '../../src/vector-store/types';
|
|
4
|
+
|
|
5
|
+
describe('MemoryVectorStore', () => {
|
|
6
|
+
test('should upsert and retrieve a document', async () => {
|
|
7
|
+
const store = new MemoryVectorStore();
|
|
8
|
+
await store.upsert({ id: 'doc1', vector: [1, 0, 0], content: 'Test', collection: 'col' });
|
|
9
|
+
const doc = await store.get('doc1', 'col');
|
|
10
|
+
expect(doc).not.toBeNull();
|
|
11
|
+
expect(doc!.content).toBe('Test');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should return null for missing document', async () => {
|
|
15
|
+
const store = new MemoryVectorStore();
|
|
16
|
+
expect(await store.get('missing')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should search by cosine similarity', async () => {
|
|
20
|
+
const store = new MemoryVectorStore();
|
|
21
|
+
await store.upsert({ id: 'a', vector: [1, 0, 0], content: 'Horizontal', collection: 'test' });
|
|
22
|
+
await store.upsert({ id: 'b', vector: [0, 1, 0], content: 'Vertical', collection: 'test' });
|
|
23
|
+
await store.upsert({ id: 'c', vector: [0.9, 0.1, 0], content: 'Mostly horizontal', collection: 'test' });
|
|
24
|
+
|
|
25
|
+
const results = await store.search([1, 0, 0], { topK: 2, collection: 'test' });
|
|
26
|
+
expect(results).toHaveLength(2);
|
|
27
|
+
// Most similar to [1,0,0] should be doc 'a' first
|
|
28
|
+
expect(results[0]!.document.id).toBe('a');
|
|
29
|
+
expect(results[0]!.score).toBeCloseTo(1.0, 2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should filter by collection', async () => {
|
|
33
|
+
const store = new MemoryVectorStore();
|
|
34
|
+
await store.upsert({ id: '1', vector: [1, 0], content: 'A', collection: 'col1' });
|
|
35
|
+
await store.upsert({ id: '2', vector: [1, 0], content: 'B', collection: 'col2' });
|
|
36
|
+
|
|
37
|
+
const results = await store.search([1, 0], { collection: 'col1' });
|
|
38
|
+
expect(results).toHaveLength(1);
|
|
39
|
+
expect(results[0]!.document.content).toBe('A');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should delete a document', async () => {
|
|
43
|
+
const store = new MemoryVectorStore();
|
|
44
|
+
await store.upsert({ id: 'del', vector: [1, 0], content: 'Delete me' });
|
|
45
|
+
const deleted = await store.delete('del');
|
|
46
|
+
expect(deleted).toBe(true);
|
|
47
|
+
expect(await store.get('del')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should delete entire collection', async () => {
|
|
51
|
+
const store = new MemoryVectorStore();
|
|
52
|
+
await store.upsert({ id: '1', vector: [1, 0], content: 'A', collection: 'purge' });
|
|
53
|
+
await store.upsert({ id: '2', vector: [0, 1], content: 'B', collection: 'purge' });
|
|
54
|
+
await store.deleteCollection('purge');
|
|
55
|
+
expect(await store.count('purge')).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should count documents', async () => {
|
|
59
|
+
const store = new MemoryVectorStore();
|
|
60
|
+
await store.upsert({ id: '1', vector: [1], content: 'A', collection: 'x' });
|
|
61
|
+
await store.upsert({ id: '2', vector: [2], content: 'B', collection: 'x' });
|
|
62
|
+
await store.upsert({ id: '3', vector: [3], content: 'C', collection: 'y' });
|
|
63
|
+
expect(await store.count()).toBe(3);
|
|
64
|
+
expect(await store.count('x')).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('cosineSimilarity', () => {
|
|
69
|
+
test('identical vectors have similarity 1.0', () => {
|
|
70
|
+
expect(cosineSimilarity([1, 0, 0], [1, 0, 0])).toBeCloseTo(1.0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('orthogonal vectors have similarity 0.0', () => {
|
|
74
|
+
expect(cosineSimilarity([1, 0], [0, 1])).toBeCloseTo(0.0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('opposite vectors have similarity -1.0', () => {
|
|
78
|
+
expect(cosineSimilarity([1, 0], [-1, 0])).toBeCloseTo(-1.0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('zero vectors return 0', () => {
|
|
82
|
+
expect(cosineSimilarity([0, 0], [1, 1])).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|