@qwickapps/qwickbrain-proxy 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/.github/workflows/publish.yml +92 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +45 -0
- package/README.md +165 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +142 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +23 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/schema.d.ts +551 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.js +202 -0
- package/dist/lib/__tests__/cache-manager.test.js.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.js +188 -0
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts +2 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.js +205 -0
- package/dist/lib/__tests__/proxy-server.test.js.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts +2 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js +233 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +25 -0
- package/dist/lib/cache-manager.d.ts.map +1 -0
- package/dist/lib/cache-manager.js +149 -0
- package/dist/lib/cache-manager.js.map +1 -0
- package/dist/lib/connection-manager.d.ts +26 -0
- package/dist/lib/connection-manager.d.ts.map +1 -0
- package/dist/lib/connection-manager.js +130 -0
- package/dist/lib/connection-manager.js.map +1 -0
- package/dist/lib/proxy-server.d.ts +19 -0
- package/dist/lib/proxy-server.d.ts.map +1 -0
- package/dist/lib/proxy-server.js +258 -0
- package/dist/lib/proxy-server.js.map +1 -0
- package/dist/lib/qwickbrain-client.d.ts +24 -0
- package/dist/lib/qwickbrain-client.d.ts.map +1 -0
- package/dist/lib/qwickbrain-client.js +197 -0
- package/dist/lib/qwickbrain-client.js.map +1 -0
- package/dist/types/config.d.ts +186 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +42 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/mcp.d.ts +223 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +78 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/drizzle/0000_fat_rafael_vega.sql +41 -0
- package/drizzle/0001_goofy_invisible_woman.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +276 -0
- package/drizzle/meta/0001_snapshot.json +295 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +12 -0
- package/package.json +65 -0
- package/src/bin/cli.ts +158 -0
- package/src/db/client.ts +34 -0
- package/src/db/schema.ts +68 -0
- package/src/index.ts +6 -0
- package/src/lib/__tests__/cache-manager.test.ts +264 -0
- package/src/lib/__tests__/connection-manager.test.ts +255 -0
- package/src/lib/__tests__/proxy-server.test.ts +261 -0
- package/src/lib/__tests__/qwickbrain-client.test.ts +310 -0
- package/src/lib/cache-manager.ts +201 -0
- package/src/lib/connection-manager.ts +156 -0
- package/src/lib/proxy-server.ts +320 -0
- package/src/lib/qwickbrain-client.ts +260 -0
- package/src/types/config.ts +47 -0
- package/src/types/mcp.ts +97 -0
- package/src/version.ts +11 -0
- package/test/fixtures/test-mcp.json +5 -0
- package/test-mcp-client.js +67 -0
- package/test-proxy.sh +25 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { createDatabase, runMigrations } from '../../db/client.js';
|
|
6
|
+
import { CacheManager } from '../cache-manager.js';
|
|
7
|
+
describe('CacheManager', () => {
|
|
8
|
+
let tmpDir;
|
|
9
|
+
let cacheManager;
|
|
10
|
+
let db;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create temporary directory for test database
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'cache-test-'));
|
|
14
|
+
const dbResult = createDatabase(tmpDir);
|
|
15
|
+
db = dbResult.db;
|
|
16
|
+
// Run migrations to create tables
|
|
17
|
+
runMigrations(db);
|
|
18
|
+
const config = {
|
|
19
|
+
dir: tmpDir,
|
|
20
|
+
ttl: {
|
|
21
|
+
workflows: 3600,
|
|
22
|
+
rules: 3600,
|
|
23
|
+
documents: 1800,
|
|
24
|
+
memories: 900,
|
|
25
|
+
},
|
|
26
|
+
preload: [],
|
|
27
|
+
};
|
|
28
|
+
cacheManager = new CacheManager(db, config);
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
// Clean up temporary directory
|
|
32
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
describe('setDocument and getDocument', () => {
|
|
35
|
+
it('should store and retrieve a document', async () => {
|
|
36
|
+
await cacheManager.setDocument('workflow', 'test-workflow', 'content here');
|
|
37
|
+
const cached = await cacheManager.getDocument('workflow', 'test-workflow');
|
|
38
|
+
expect(cached).not.toBeNull();
|
|
39
|
+
expect(cached?.data.content).toBe('content here');
|
|
40
|
+
expect(cached?.data.doc_type).toBe('workflow');
|
|
41
|
+
expect(cached?.data.name).toBe('test-workflow');
|
|
42
|
+
expect(cached?.isExpired).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('should store document with metadata', async () => {
|
|
45
|
+
const metadata = { author: 'test', version: 1 };
|
|
46
|
+
await cacheManager.setDocument('frd', 'test-frd', 'frd content', undefined, metadata);
|
|
47
|
+
const cached = await cacheManager.getDocument('frd', 'test-frd');
|
|
48
|
+
expect(cached?.data.metadata).toEqual(metadata);
|
|
49
|
+
});
|
|
50
|
+
it('should store document with project scope', async () => {
|
|
51
|
+
await cacheManager.setDocument('design', 'test-design', 'design content', 'my-project');
|
|
52
|
+
const cached = await cacheManager.getDocument('design', 'test-design', 'my-project');
|
|
53
|
+
expect(cached).not.toBeNull();
|
|
54
|
+
expect(cached?.data.project).toBe('my-project');
|
|
55
|
+
});
|
|
56
|
+
it('should distinguish between global and project-scoped documents', async () => {
|
|
57
|
+
await cacheManager.setDocument('rule', 'test-rule', 'global rule');
|
|
58
|
+
await cacheManager.setDocument('rule', 'test-rule', 'project rule', 'my-project');
|
|
59
|
+
const globalDoc = await cacheManager.getDocument('rule', 'test-rule');
|
|
60
|
+
const projectDoc = await cacheManager.getDocument('rule', 'test-rule', 'my-project');
|
|
61
|
+
expect(globalDoc?.data.content).toBe('global rule');
|
|
62
|
+
expect(globalDoc?.data.project).toBe('');
|
|
63
|
+
expect(projectDoc?.data.content).toBe('project rule');
|
|
64
|
+
expect(projectDoc?.data.project).toBe('my-project');
|
|
65
|
+
});
|
|
66
|
+
it('should return null for non-existent document', async () => {
|
|
67
|
+
const cached = await cacheManager.getDocument('workflow', 'non-existent');
|
|
68
|
+
expect(cached).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
it('should update existing document on conflict', async () => {
|
|
71
|
+
await cacheManager.setDocument('workflow', 'test', 'version 1');
|
|
72
|
+
await cacheManager.setDocument('workflow', 'test', 'version 2');
|
|
73
|
+
const cached = await cacheManager.getDocument('workflow', 'test');
|
|
74
|
+
expect(cached?.data.content).toBe('version 2');
|
|
75
|
+
});
|
|
76
|
+
it('should mark document as expired after TTL', async () => {
|
|
77
|
+
// Override config to have very short TTL for testing
|
|
78
|
+
const shortTTLConfig = {
|
|
79
|
+
dir: tmpDir,
|
|
80
|
+
ttl: {
|
|
81
|
+
workflows: 0, // Expire immediately
|
|
82
|
+
rules: 0,
|
|
83
|
+
documents: 0,
|
|
84
|
+
memories: 0,
|
|
85
|
+
},
|
|
86
|
+
preload: [],
|
|
87
|
+
};
|
|
88
|
+
const shortCacheManager = new CacheManager(db, shortTTLConfig);
|
|
89
|
+
await shortCacheManager.setDocument('workflow', 'test', 'content');
|
|
90
|
+
// Wait to ensure timestamp difference
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
92
|
+
const cached = await shortCacheManager.getDocument('workflow', 'test');
|
|
93
|
+
expect(cached).not.toBeNull();
|
|
94
|
+
expect(cached?.isExpired).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('setMemory and getMemory', () => {
|
|
98
|
+
it('should store and retrieve a memory', async () => {
|
|
99
|
+
await cacheManager.setMemory('test-memory', 'memory content');
|
|
100
|
+
const cached = await cacheManager.getMemory('test-memory');
|
|
101
|
+
expect(cached).not.toBeNull();
|
|
102
|
+
expect(cached?.data.content).toBe('memory content');
|
|
103
|
+
expect(cached?.data.name).toBe('test-memory');
|
|
104
|
+
expect(cached?.isExpired).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('should store memory with metadata', async () => {
|
|
107
|
+
const metadata = { lastUpdated: Date.now() };
|
|
108
|
+
await cacheManager.setMemory('test-memory', 'content', undefined, metadata);
|
|
109
|
+
const cached = await cacheManager.getMemory('test-memory');
|
|
110
|
+
expect(cached?.data.metadata).toEqual(metadata);
|
|
111
|
+
});
|
|
112
|
+
it('should store memory with project scope', async () => {
|
|
113
|
+
await cacheManager.setMemory('patterns', 'project patterns', 'my-project');
|
|
114
|
+
const cached = await cacheManager.getMemory('patterns', 'my-project');
|
|
115
|
+
expect(cached).not.toBeNull();
|
|
116
|
+
expect(cached?.data.project).toBe('my-project');
|
|
117
|
+
});
|
|
118
|
+
it('should distinguish between global and project-scoped memories', async () => {
|
|
119
|
+
await cacheManager.setMemory('context', 'global context');
|
|
120
|
+
await cacheManager.setMemory('context', 'project context', 'my-project');
|
|
121
|
+
const globalMem = await cacheManager.getMemory('context');
|
|
122
|
+
const projectMem = await cacheManager.getMemory('context', 'my-project');
|
|
123
|
+
expect(globalMem?.data.content).toBe('global context');
|
|
124
|
+
expect(projectMem?.data.content).toBe('project context');
|
|
125
|
+
});
|
|
126
|
+
it('should return null for non-existent memory', async () => {
|
|
127
|
+
const cached = await cacheManager.getMemory('non-existent');
|
|
128
|
+
expect(cached).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
it('should mark memory as expired after TTL', async () => {
|
|
131
|
+
const shortTTLConfig = {
|
|
132
|
+
dir: tmpDir,
|
|
133
|
+
ttl: {
|
|
134
|
+
workflows: 0,
|
|
135
|
+
rules: 0,
|
|
136
|
+
documents: 0,
|
|
137
|
+
memories: 0,
|
|
138
|
+
},
|
|
139
|
+
preload: [],
|
|
140
|
+
};
|
|
141
|
+
const shortCacheManager = new CacheManager(db, shortTTLConfig);
|
|
142
|
+
await shortCacheManager.setMemory('test', 'content');
|
|
143
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
144
|
+
const cached = await shortCacheManager.getMemory('test');
|
|
145
|
+
expect(cached).not.toBeNull();
|
|
146
|
+
expect(cached?.isExpired).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('cleanupExpiredEntries', () => {
|
|
150
|
+
it('should delete expired documents and memories', async () => {
|
|
151
|
+
const shortTTLConfig = {
|
|
152
|
+
dir: tmpDir,
|
|
153
|
+
ttl: {
|
|
154
|
+
workflows: 0,
|
|
155
|
+
rules: 0,
|
|
156
|
+
documents: 0,
|
|
157
|
+
memories: 0,
|
|
158
|
+
},
|
|
159
|
+
preload: [],
|
|
160
|
+
};
|
|
161
|
+
const shortCacheManager = new CacheManager(db, shortTTLConfig);
|
|
162
|
+
// Add some items that will immediately expire
|
|
163
|
+
await shortCacheManager.setDocument('workflow', 'test1', 'content1');
|
|
164
|
+
await shortCacheManager.setDocument('rule', 'test2', 'content2');
|
|
165
|
+
await shortCacheManager.setMemory('memory1', 'content3');
|
|
166
|
+
// Wait to ensure they're expired (need sufficient time for clock to advance)
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
168
|
+
// Clean up
|
|
169
|
+
const result = await shortCacheManager.cleanupExpiredEntries();
|
|
170
|
+
expect(result.documentsDeleted).toBe(2);
|
|
171
|
+
expect(result.memoriesDeleted).toBe(1);
|
|
172
|
+
// Verify they're gone
|
|
173
|
+
const doc1 = await shortCacheManager.getDocument('workflow', 'test1');
|
|
174
|
+
const mem1 = await shortCacheManager.getMemory('memory1');
|
|
175
|
+
expect(doc1).toBeNull();
|
|
176
|
+
expect(mem1).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
it('should not delete non-expired items', async () => {
|
|
179
|
+
await cacheManager.setDocument('workflow', 'test', 'content');
|
|
180
|
+
await cacheManager.setMemory('memory', 'content');
|
|
181
|
+
const result = await cacheManager.cleanupExpiredEntries();
|
|
182
|
+
expect(result.documentsDeleted).toBe(0);
|
|
183
|
+
expect(result.memoriesDeleted).toBe(0);
|
|
184
|
+
// Verify they're still there
|
|
185
|
+
const doc = await cacheManager.getDocument('workflow', 'test');
|
|
186
|
+
const mem = await cacheManager.getMemory('memory');
|
|
187
|
+
expect(doc).not.toBeNull();
|
|
188
|
+
expect(mem).not.toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('cache age calculation', () => {
|
|
192
|
+
it('should calculate age correctly', async () => {
|
|
193
|
+
await cacheManager.setDocument('workflow', 'test', 'content');
|
|
194
|
+
// Wait 100ms
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
196
|
+
const cached = await cacheManager.getDocument('workflow', 'test');
|
|
197
|
+
expect(cached?.age).toBeGreaterThanOrEqual(0);
|
|
198
|
+
expect(cached?.age).toBeLessThan(1); // Less than 1 second
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=cache-manager.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-manager.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/cache-manager.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-manager.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/connection-manager.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { ConnectionManager } from '../connection-manager.js';
|
|
3
|
+
describe('ConnectionManager', () => {
|
|
4
|
+
let mockClient;
|
|
5
|
+
let connectionManager;
|
|
6
|
+
let config;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
config = {
|
|
9
|
+
healthCheckInterval: 100, // Short interval for testing
|
|
10
|
+
timeout: 5000,
|
|
11
|
+
maxReconnectAttempts: 3,
|
|
12
|
+
reconnectBackoff: {
|
|
13
|
+
initial: 50,
|
|
14
|
+
multiplier: 2,
|
|
15
|
+
max: 500,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
mockClient = {
|
|
19
|
+
healthCheck: vi.fn().mockResolvedValue(true),
|
|
20
|
+
};
|
|
21
|
+
connectionManager = new ConnectionManager(mockClient, config);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
connectionManager.stop();
|
|
25
|
+
});
|
|
26
|
+
describe('getState', () => {
|
|
27
|
+
it('should return initial state as disconnected', () => {
|
|
28
|
+
expect(connectionManager.getState()).toBe('disconnected');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('start and healthCheck', () => {
|
|
32
|
+
it('should transition to connected on successful health check', async () => {
|
|
33
|
+
await connectionManager.start();
|
|
34
|
+
expect(connectionManager.getState()).toBe('connected');
|
|
35
|
+
expect(mockClient.healthCheck).toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
it('should transition to reconnecting after failed health check', async () => {
|
|
38
|
+
mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Connection failed'));
|
|
39
|
+
await connectionManager.start();
|
|
40
|
+
// After health check fails, it immediately schedules reconnect
|
|
41
|
+
expect(connectionManager.getState()).toBe('reconnecting');
|
|
42
|
+
});
|
|
43
|
+
it('should emit stateChange event on state transition', async () => {
|
|
44
|
+
const stateChangeListener = vi.fn();
|
|
45
|
+
connectionManager.on('stateChange', stateChangeListener);
|
|
46
|
+
await connectionManager.start();
|
|
47
|
+
expect(stateChangeListener).toHaveBeenCalledWith({
|
|
48
|
+
from: 'disconnected',
|
|
49
|
+
to: 'connected',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it('should emit connected event with latency', async () => {
|
|
53
|
+
const connectedListener = vi.fn();
|
|
54
|
+
connectionManager.on('connected', connectedListener);
|
|
55
|
+
await connectionManager.start();
|
|
56
|
+
expect(connectedListener).toHaveBeenCalled();
|
|
57
|
+
const call = connectedListener.mock.calls[0][0];
|
|
58
|
+
expect(call).toHaveProperty('latencyMs');
|
|
59
|
+
expect(typeof call.latencyMs).toBe('number');
|
|
60
|
+
});
|
|
61
|
+
it('should emit disconnected event on failure', async () => {
|
|
62
|
+
mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
|
|
63
|
+
const disconnectedListener = vi.fn();
|
|
64
|
+
connectionManager.on('disconnected', disconnectedListener);
|
|
65
|
+
await connectionManager.start();
|
|
66
|
+
expect(disconnectedListener).toHaveBeenCalled();
|
|
67
|
+
expect(disconnectedListener.mock.calls[0][0]).toHaveProperty('error');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('stop', () => {
|
|
71
|
+
it('should transition to offline state', () => {
|
|
72
|
+
connectionManager.stop();
|
|
73
|
+
expect(connectionManager.getState()).toBe('offline');
|
|
74
|
+
});
|
|
75
|
+
it('should stop health check timer', async () => {
|
|
76
|
+
await connectionManager.start();
|
|
77
|
+
const healthCheckCount = mockClient.healthCheck.mock.calls.length;
|
|
78
|
+
connectionManager.stop();
|
|
79
|
+
// Wait longer than health check interval
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
81
|
+
// Health check should not have been called again
|
|
82
|
+
expect(mockClient.healthCheck.mock.calls.length).toBe(healthCheckCount);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('reconnection logic', () => {
|
|
86
|
+
it('should attempt to reconnect on health check failure', async () => {
|
|
87
|
+
let callCount = 0;
|
|
88
|
+
mockClient.healthCheck = vi.fn().mockImplementation(() => {
|
|
89
|
+
callCount++;
|
|
90
|
+
if (callCount === 1) {
|
|
91
|
+
return Promise.reject(new Error('Failed'));
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve(true);
|
|
94
|
+
});
|
|
95
|
+
const reconnectingListener = vi.fn();
|
|
96
|
+
connectionManager.on('reconnecting', reconnectingListener);
|
|
97
|
+
await connectionManager.start();
|
|
98
|
+
// Wait for reconnect attempt
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
100
|
+
expect(reconnectingListener).toHaveBeenCalled();
|
|
101
|
+
expect(reconnectingListener.mock.calls[0][0]).toHaveProperty('attempt');
|
|
102
|
+
expect(reconnectingListener.mock.calls[0][0]).toHaveProperty('delay');
|
|
103
|
+
});
|
|
104
|
+
it('should use exponential backoff for reconnection', async () => {
|
|
105
|
+
mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
|
|
106
|
+
const reconnectingListener = vi.fn();
|
|
107
|
+
connectionManager.on('reconnecting', reconnectingListener);
|
|
108
|
+
await connectionManager.start();
|
|
109
|
+
// Wait for multiple reconnect attempts
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
111
|
+
const delays = reconnectingListener.mock.calls.map(call => call[0].delay);
|
|
112
|
+
// Delays should increase exponentially
|
|
113
|
+
for (let i = 1; i < delays.length; i++) {
|
|
114
|
+
expect(delays[i]).toBeGreaterThan(delays[i - 1]);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('should emit maxReconnectAttemptsReached after max attempts', async () => {
|
|
118
|
+
mockClient.healthCheck = vi.fn().mockRejectedValue(new Error('Failed'));
|
|
119
|
+
const maxAttemptsListener = vi.fn();
|
|
120
|
+
connectionManager.on('maxReconnectAttemptsReached', maxAttemptsListener);
|
|
121
|
+
await connectionManager.start();
|
|
122
|
+
// Wait for all reconnect attempts
|
|
123
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
124
|
+
expect(maxAttemptsListener).toHaveBeenCalled();
|
|
125
|
+
expect(connectionManager.getState()).toBe('offline');
|
|
126
|
+
});
|
|
127
|
+
it('should reset reconnect attempts on successful connection', async () => {
|
|
128
|
+
let callCount = 0;
|
|
129
|
+
mockClient.healthCheck = vi.fn().mockImplementation(() => {
|
|
130
|
+
callCount++;
|
|
131
|
+
// Fail first two, then succeed
|
|
132
|
+
if (callCount <= 2) {
|
|
133
|
+
return Promise.reject(new Error('Failed'));
|
|
134
|
+
}
|
|
135
|
+
return Promise.resolve(true);
|
|
136
|
+
});
|
|
137
|
+
await connectionManager.start();
|
|
138
|
+
// Wait for reconnection
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
140
|
+
expect(connectionManager.getState()).toBe('connected');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('execute', () => {
|
|
144
|
+
it('should execute function when connected', async () => {
|
|
145
|
+
await connectionManager.start();
|
|
146
|
+
const testFn = vi.fn().mockResolvedValue('result');
|
|
147
|
+
const result = await connectionManager.execute(testFn);
|
|
148
|
+
expect(result).toBe('result');
|
|
149
|
+
expect(testFn).toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
it('should throw error when not connected', async () => {
|
|
152
|
+
const testFn = vi.fn();
|
|
153
|
+
await expect(connectionManager.execute(testFn)).rejects.toThrow('QwickBrain unavailable');
|
|
154
|
+
expect(testFn).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
it('should record failure if execution throws', async () => {
|
|
157
|
+
await connectionManager.start();
|
|
158
|
+
const testFn = vi.fn().mockRejectedValue(new Error('Execution failed'));
|
|
159
|
+
await expect(connectionManager.execute(testFn)).rejects.toThrow('Execution failed');
|
|
160
|
+
// Should transition to reconnecting after failure
|
|
161
|
+
expect(connectionManager.getState()).toBe('reconnecting');
|
|
162
|
+
});
|
|
163
|
+
it('should wait for state transitions to complete (race condition test)', async () => {
|
|
164
|
+
await connectionManager.start();
|
|
165
|
+
// Simulate rapid state changes
|
|
166
|
+
connectionManager.setState('disconnected');
|
|
167
|
+
connectionManager.setState('connected');
|
|
168
|
+
const testFn = vi.fn().mockResolvedValue('result');
|
|
169
|
+
// This should wait for state transitions to settle
|
|
170
|
+
const result = await connectionManager.execute(testFn);
|
|
171
|
+
expect(result).toBe('result');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('recordFailure', () => {
|
|
175
|
+
it('should transition to reconnecting when connected', () => {
|
|
176
|
+
connectionManager['state'] = 'connected';
|
|
177
|
+
connectionManager.recordFailure();
|
|
178
|
+
// After failure, it immediately schedules reconnect
|
|
179
|
+
expect(connectionManager.getState()).toBe('reconnecting');
|
|
180
|
+
});
|
|
181
|
+
it('should not affect offline state', () => {
|
|
182
|
+
connectionManager.stop();
|
|
183
|
+
connectionManager.recordFailure();
|
|
184
|
+
expect(connectionManager.getState()).toBe('offline');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=connection-manager.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-manager.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/connection-manager.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-server.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/proxy-server.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { createDatabase, runMigrations } from '../../db/client.js';
|
|
6
|
+
import { ProxyServer } from '../proxy-server.js';
|
|
7
|
+
// Mock MCP SDK
|
|
8
|
+
vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
|
|
9
|
+
Client: vi.fn().mockImplementation(() => ({
|
|
10
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
callTool: vi.fn(),
|
|
13
|
+
listTools: vi.fn().mockResolvedValue({ tools: [] }),
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
17
|
+
Server: vi.fn().mockImplementation(() => ({
|
|
18
|
+
setRequestHandler: vi.fn(),
|
|
19
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
24
|
+
StdioServerTransport: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('@modelcontextprotocol/sdk/client/sse.js', () => ({
|
|
27
|
+
SSEClientTransport: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
describe('ProxyServer', () => {
|
|
30
|
+
let tmpDir;
|
|
31
|
+
let config;
|
|
32
|
+
let db;
|
|
33
|
+
let server;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'proxy-test-'));
|
|
36
|
+
const dbResult = createDatabase(tmpDir);
|
|
37
|
+
db = dbResult.db;
|
|
38
|
+
// Run migrations to create tables
|
|
39
|
+
runMigrations(db);
|
|
40
|
+
config = {
|
|
41
|
+
qwickbrain: {
|
|
42
|
+
mode: 'sse',
|
|
43
|
+
url: 'http://test.local:3000',
|
|
44
|
+
},
|
|
45
|
+
cache: {
|
|
46
|
+
dir: tmpDir,
|
|
47
|
+
ttl: {
|
|
48
|
+
workflows: 3600,
|
|
49
|
+
rules: 3600,
|
|
50
|
+
documents: 1800,
|
|
51
|
+
memories: 900,
|
|
52
|
+
},
|
|
53
|
+
preload: [],
|
|
54
|
+
},
|
|
55
|
+
connection: {
|
|
56
|
+
healthCheckInterval: 30000,
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
maxReconnectAttempts: 5,
|
|
59
|
+
reconnectBackoff: {
|
|
60
|
+
initial: 1000,
|
|
61
|
+
multiplier: 2,
|
|
62
|
+
max: 30000,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
sync: {
|
|
66
|
+
interval: 60000,
|
|
67
|
+
batchSize: 10,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
server = new ProxyServer(db, config);
|
|
71
|
+
});
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await server.stop();
|
|
74
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
describe('initialization', () => {
|
|
77
|
+
it('should initialize with all components', () => {
|
|
78
|
+
expect(server['server']).toBeDefined();
|
|
79
|
+
expect(server['connectionManager']).toBeDefined();
|
|
80
|
+
expect(server['cacheManager']).toBeDefined();
|
|
81
|
+
expect(server['qwickbrainClient']).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
it('should set up MCP request handlers', () => {
|
|
84
|
+
const setRequestHandlerMock = server['server'].setRequestHandler;
|
|
85
|
+
// Should have set up handlers for ListTools and CallTool
|
|
86
|
+
expect(setRequestHandlerMock).toHaveBeenCalled();
|
|
87
|
+
expect(setRequestHandlerMock.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('cache cleanup on startup', () => {
|
|
91
|
+
it('should clean up expired cache entries on start', async () => {
|
|
92
|
+
// Add some expired entries
|
|
93
|
+
const shortTTLConfig = { ...config };
|
|
94
|
+
shortTTLConfig.cache.ttl = {
|
|
95
|
+
workflows: 0,
|
|
96
|
+
rules: 0,
|
|
97
|
+
documents: 0,
|
|
98
|
+
memories: 0,
|
|
99
|
+
};
|
|
100
|
+
const tempServer = new ProxyServer(db, shortTTLConfig);
|
|
101
|
+
await tempServer['cacheManager'].setDocument('workflow', 'test', 'content');
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
103
|
+
// Verify entry exists before cleanup
|
|
104
|
+
const beforeCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
|
|
105
|
+
expect(beforeCleanup).not.toBeNull();
|
|
106
|
+
await tempServer.start();
|
|
107
|
+
// Entry should be removed after cleanup
|
|
108
|
+
const afterCleanup = await tempServer['cacheManager'].getDocument('workflow', 'test');
|
|
109
|
+
expect(afterCleanup).toBeNull();
|
|
110
|
+
await tempServer.stop();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('graceful degradation', () => {
|
|
114
|
+
it('should serve stale cache when disconnected', async () => {
|
|
115
|
+
// Create server with short TTL to make cache expire
|
|
116
|
+
const shortTTLConfig = { ...config };
|
|
117
|
+
shortTTLConfig.cache.ttl = {
|
|
118
|
+
workflows: 0,
|
|
119
|
+
rules: 0,
|
|
120
|
+
documents: 0,
|
|
121
|
+
memories: 0,
|
|
122
|
+
};
|
|
123
|
+
const tempServer = new ProxyServer(db, shortTTLConfig);
|
|
124
|
+
// Add item to cache
|
|
125
|
+
await tempServer['cacheManager'].setDocument('workflow', 'test', 'cached content');
|
|
126
|
+
// Wait for cache to expire
|
|
127
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
128
|
+
// Simulate disconnected state
|
|
129
|
+
tempServer['connectionManager']['state'] = 'disconnected';
|
|
130
|
+
const result = await tempServer['handleGetDocument']('workflow', 'test');
|
|
131
|
+
expect(result.data.content).toBe('cached content');
|
|
132
|
+
expect(result._metadata.source).toBe('stale_cache');
|
|
133
|
+
await tempServer.stop();
|
|
134
|
+
});
|
|
135
|
+
it('should return error when no cache available and disconnected', async () => {
|
|
136
|
+
// Simulate disconnected state with no cache
|
|
137
|
+
server['connectionManager']['state'] = 'disconnected';
|
|
138
|
+
const result = await server['handleGetDocument']('workflow', 'non-existent');
|
|
139
|
+
expect(result.error).toBeDefined();
|
|
140
|
+
expect(result.error?.code).toBe('UNAVAILABLE');
|
|
141
|
+
expect(result.error?.message).toContain('QwickBrain unavailable');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('metadata', () => {
|
|
145
|
+
it('should include correct metadata for cache hit', async () => {
|
|
146
|
+
await server['cacheManager'].setDocument('workflow', 'test', 'content');
|
|
147
|
+
// Simulate connected state
|
|
148
|
+
server['connectionManager']['state'] = 'connected';
|
|
149
|
+
const result = await server['handleGetDocument']('workflow', 'test');
|
|
150
|
+
expect(result._metadata.source).toBe('cache');
|
|
151
|
+
expect(result._metadata.age_seconds).toBeDefined();
|
|
152
|
+
expect(typeof result._metadata.age_seconds).toBe('number');
|
|
153
|
+
});
|
|
154
|
+
it('should create metadata with correct source', () => {
|
|
155
|
+
const cacheMeta = server['createMetadata']('cache', 100);
|
|
156
|
+
expect(cacheMeta).toEqual({
|
|
157
|
+
source: 'cache',
|
|
158
|
+
age_seconds: 100,
|
|
159
|
+
status: server['connectionManager'].getState(),
|
|
160
|
+
});
|
|
161
|
+
const liveMeta = server['createMetadata']('live');
|
|
162
|
+
expect(liveMeta).toEqual({
|
|
163
|
+
source: 'live',
|
|
164
|
+
status: server['connectionManager'].getState(),
|
|
165
|
+
});
|
|
166
|
+
const staleMeta = server['createMetadata']('stale_cache', 500);
|
|
167
|
+
expect(staleMeta).toEqual({
|
|
168
|
+
source: 'stale_cache',
|
|
169
|
+
age_seconds: 500,
|
|
170
|
+
status: server['connectionManager'].getState(),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('error handling', () => {
|
|
175
|
+
it('should handle QwickBrain errors gracefully', async () => {
|
|
176
|
+
// Mock client to throw error
|
|
177
|
+
server['qwickbrainClient'].getDocument = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
178
|
+
// Simulate connected state
|
|
179
|
+
server['connectionManager']['state'] = 'connected';
|
|
180
|
+
const result = await server['handleGetDocument']('workflow', 'test');
|
|
181
|
+
// Should fall back to error response when remote fails and no cache
|
|
182
|
+
expect(result.error).toBeDefined();
|
|
183
|
+
expect(result.error?.code).toBe('UNAVAILABLE');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('project scoping', () => {
|
|
187
|
+
it('should handle project-scoped documents', async () => {
|
|
188
|
+
await server['cacheManager'].setDocument('design', 'test-design', 'design content', 'my-project');
|
|
189
|
+
server['connectionManager']['state'] = 'connected';
|
|
190
|
+
const result = await server['handleGetDocument']('design', 'test-design', 'my-project');
|
|
191
|
+
expect(result.data.content).toBe('design content');
|
|
192
|
+
expect(result.data.project).toBe('my-project');
|
|
193
|
+
});
|
|
194
|
+
it('should distinguish global vs project-scoped items', async () => {
|
|
195
|
+
await server['cacheManager'].setDocument('rule', 'test', 'global rule');
|
|
196
|
+
await server['cacheManager'].setDocument('rule', 'test', 'project rule', 'proj1');
|
|
197
|
+
server['connectionManager']['state'] = 'connected';
|
|
198
|
+
const globalResult = await server['handleGetDocument']('rule', 'test');
|
|
199
|
+
const projectResult = await server['handleGetDocument']('rule', 'test', 'proj1');
|
|
200
|
+
expect(globalResult.data.content).toBe('global rule');
|
|
201
|
+
expect(projectResult.data.content).toBe('project rule');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
//# sourceMappingURL=proxy-server.test.js.map
|