mcp-rubber-duck 1.5.1 → 1.6.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/.claude/agents/pricing-updater.md +111 -0
- package/.claude/commands/update-pricing.md +22 -0
- package/.releaserc.json +4 -0
- package/CHANGELOG.md +14 -0
- package/dist/config/types.d.ts +72 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +8 -0
- package/dist/config/types.js.map +1 -1
- package/dist/data/default-pricing.d.ts +18 -0
- package/dist/data/default-pricing.d.ts.map +1 -0
- package/dist/data/default-pricing.js +307 -0
- package/dist/data/default-pricing.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +2 -1
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +20 -2
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +3 -1
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +12 -1
- package/dist/providers/manager.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +35 -4
- package/dist/server.js.map +1 -1
- package/dist/services/pricing.d.ts +56 -0
- package/dist/services/pricing.d.ts.map +1 -0
- package/dist/services/pricing.js +124 -0
- package/dist/services/pricing.js.map +1 -0
- package/dist/services/usage.d.ts +48 -0
- package/dist/services/usage.d.ts.map +1 -0
- package/dist/services/usage.js +243 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/tools/get-usage-stats.d.ts +8 -0
- package/dist/tools/get-usage-stats.d.ts.map +1 -0
- package/dist/tools/get-usage-stats.js +92 -0
- package/dist/tools/get-usage-stats.js.map +1 -0
- package/package.json +1 -1
- package/src/config/types.ts +51 -0
- package/src/data/default-pricing.ts +368 -0
- package/src/providers/enhanced-manager.ts +41 -4
- package/src/providers/manager.ts +22 -1
- package/src/server.ts +42 -4
- package/src/services/pricing.ts +155 -0
- package/src/services/usage.ts +293 -0
- package/src/tools/get-usage-stats.ts +109 -0
- package/tests/approval.test.ts +440 -0
- package/tests/cache.test.ts +240 -0
- package/tests/config.test.ts +468 -0
- package/tests/consensus.test.ts +10 -0
- package/tests/conversation.test.ts +86 -0
- package/tests/duck-debate.test.ts +105 -1
- package/tests/duck-iterate.test.ts +30 -0
- package/tests/duck-judge.test.ts +93 -0
- package/tests/duck-vote.test.ts +46 -0
- package/tests/health.test.ts +129 -0
- package/tests/pricing.test.ts +335 -0
- package/tests/providers.test.ts +591 -0
- package/tests/safe-logger.test.ts +314 -0
- package/tests/tools/approve-mcp-request.test.ts +239 -0
- package/tests/tools/ask-duck.test.ts +159 -0
- package/tests/tools/chat-duck.test.ts +191 -0
- package/tests/tools/compare-ducks.test.ts +190 -0
- package/tests/tools/duck-council.test.ts +219 -0
- package/tests/tools/get-pending-approvals.test.ts +195 -0
- package/tests/tools/get-usage-stats.test.ts +236 -0
- package/tests/tools/list-ducks.test.ts +144 -0
- package/tests/tools/list-models.test.ts +163 -0
- package/tests/tools/mcp-status.test.ts +330 -0
- package/tests/usage.test.ts +661 -0
package/tests/config.test.ts
CHANGED
|
@@ -4,6 +4,474 @@ import { ConfigManager } from '../src/config/config';
|
|
|
4
4
|
// Mock logger to avoid console noise during tests
|
|
5
5
|
jest.mock('../src/utils/logger');
|
|
6
6
|
|
|
7
|
+
describe('ConfigManager - Default Providers', () => {
|
|
8
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalEnv = { ...process.env };
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
// Clear all provider keys
|
|
14
|
+
delete process.env.OPENAI_API_KEY;
|
|
15
|
+
delete process.env.GEMINI_API_KEY;
|
|
16
|
+
delete process.env.GROQ_API_KEY;
|
|
17
|
+
delete process.env.OLLAMA_BASE_URL;
|
|
18
|
+
delete process.env.ENABLE_OLLAMA;
|
|
19
|
+
Object.keys(process.env).forEach(key => {
|
|
20
|
+
if (key.startsWith('CUSTOM_')) delete process.env[key];
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
process.env = originalEnv;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should create OpenAI provider when API key is set', () => {
|
|
29
|
+
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
30
|
+
|
|
31
|
+
const configManager = new ConfigManager();
|
|
32
|
+
const providers = configManager.getAllProviders();
|
|
33
|
+
|
|
34
|
+
expect(providers.openai).toBeDefined();
|
|
35
|
+
expect(providers.openai.api_key).toBe('sk-test-key');
|
|
36
|
+
expect(providers.openai.base_url).toBe('https://api.openai.com/v1');
|
|
37
|
+
expect(providers.openai.nickname).toBe('GPT Duck');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should use custom OpenAI model and nickname from env', () => {
|
|
41
|
+
process.env.OPENAI_API_KEY = 'sk-test-key';
|
|
42
|
+
process.env.OPENAI_DEFAULT_MODEL = 'gpt-4-turbo';
|
|
43
|
+
process.env.OPENAI_NICKNAME = 'Custom GPT Duck';
|
|
44
|
+
|
|
45
|
+
const configManager = new ConfigManager();
|
|
46
|
+
const providers = configManager.getAllProviders();
|
|
47
|
+
|
|
48
|
+
expect(providers.openai.default_model).toBe('gpt-4-turbo');
|
|
49
|
+
expect(providers.openai.nickname).toBe('Custom GPT Duck');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should create Gemini provider when API key is set', () => {
|
|
53
|
+
process.env.GEMINI_API_KEY = 'gemini-test-key';
|
|
54
|
+
|
|
55
|
+
const configManager = new ConfigManager();
|
|
56
|
+
const providers = configManager.getAllProviders();
|
|
57
|
+
|
|
58
|
+
expect(providers.gemini).toBeDefined();
|
|
59
|
+
expect(providers.gemini.api_key).toBe('gemini-test-key');
|
|
60
|
+
expect(providers.gemini.nickname).toBe('Gemini Duck');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use custom Gemini model and nickname from env', () => {
|
|
64
|
+
process.env.GEMINI_API_KEY = 'gemini-test-key';
|
|
65
|
+
process.env.GEMINI_DEFAULT_MODEL = 'gemini-pro';
|
|
66
|
+
process.env.GEMINI_NICKNAME = 'Custom Gemini Duck';
|
|
67
|
+
|
|
68
|
+
const configManager = new ConfigManager();
|
|
69
|
+
const providers = configManager.getAllProviders();
|
|
70
|
+
|
|
71
|
+
expect(providers.gemini.default_model).toBe('gemini-pro');
|
|
72
|
+
expect(providers.gemini.nickname).toBe('Custom Gemini Duck');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should create Groq provider when API key is set', () => {
|
|
76
|
+
process.env.GROQ_API_KEY = 'gsk-test-key';
|
|
77
|
+
|
|
78
|
+
const configManager = new ConfigManager();
|
|
79
|
+
const providers = configManager.getAllProviders();
|
|
80
|
+
|
|
81
|
+
expect(providers.groq).toBeDefined();
|
|
82
|
+
expect(providers.groq.api_key).toBe('gsk-test-key');
|
|
83
|
+
expect(providers.groq.nickname).toBe('Groq Duck');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should use custom Groq model and nickname from env', () => {
|
|
87
|
+
process.env.GROQ_API_KEY = 'gsk-test-key';
|
|
88
|
+
process.env.GROQ_DEFAULT_MODEL = 'custom-groq-model';
|
|
89
|
+
process.env.GROQ_NICKNAME = 'Fast Groq Duck';
|
|
90
|
+
|
|
91
|
+
const configManager = new ConfigManager();
|
|
92
|
+
const providers = configManager.getAllProviders();
|
|
93
|
+
|
|
94
|
+
expect(providers.groq.default_model).toBe('custom-groq-model');
|
|
95
|
+
expect(providers.groq.nickname).toBe('Fast Groq Duck');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should create Ollama provider when OLLAMA_BASE_URL is set', () => {
|
|
99
|
+
process.env.OPENAI_API_KEY = 'dummy'; // Need at least one provider
|
|
100
|
+
process.env.OLLAMA_BASE_URL = 'http://localhost:11434/v1';
|
|
101
|
+
|
|
102
|
+
const configManager = new ConfigManager();
|
|
103
|
+
const providers = configManager.getAllProviders();
|
|
104
|
+
|
|
105
|
+
expect(providers.ollama).toBeDefined();
|
|
106
|
+
expect(providers.ollama.api_key).toBe('not-needed');
|
|
107
|
+
expect(providers.ollama.base_url).toBe('http://localhost:11434/v1');
|
|
108
|
+
expect(providers.ollama.nickname).toBe('Local Duck');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should create Ollama provider when ENABLE_OLLAMA is true', () => {
|
|
112
|
+
process.env.OPENAI_API_KEY = 'dummy'; // Need at least one provider
|
|
113
|
+
process.env.ENABLE_OLLAMA = 'true';
|
|
114
|
+
|
|
115
|
+
const configManager = new ConfigManager();
|
|
116
|
+
const providers = configManager.getAllProviders();
|
|
117
|
+
|
|
118
|
+
expect(providers.ollama).toBeDefined();
|
|
119
|
+
expect(providers.ollama.base_url).toBe('http://localhost:11434/v1');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use custom Ollama model and nickname from env', () => {
|
|
123
|
+
process.env.OPENAI_API_KEY = 'dummy';
|
|
124
|
+
process.env.ENABLE_OLLAMA = 'true';
|
|
125
|
+
process.env.OLLAMA_DEFAULT_MODEL = 'mistral';
|
|
126
|
+
process.env.OLLAMA_NICKNAME = 'My Local Duck';
|
|
127
|
+
|
|
128
|
+
const configManager = new ConfigManager();
|
|
129
|
+
const providers = configManager.getAllProviders();
|
|
130
|
+
|
|
131
|
+
expect(providers.ollama.default_model).toBe('mistral');
|
|
132
|
+
expect(providers.ollama.nickname).toBe('My Local Duck');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('ConfigManager - MCP Bridge Config', () => {
|
|
137
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
originalEnv = { ...process.env };
|
|
141
|
+
jest.clearAllMocks();
|
|
142
|
+
// Clear MCP-related env vars
|
|
143
|
+
Object.keys(process.env).forEach(key => {
|
|
144
|
+
if (key.startsWith('MCP_')) delete process.env[key];
|
|
145
|
+
if (key.startsWith('CUSTOM_')) delete process.env[key];
|
|
146
|
+
});
|
|
147
|
+
delete process.env.OPENAI_API_KEY;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
process.env = originalEnv;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should enable MCP bridge when MCP_BRIDGE_ENABLED=true', () => {
|
|
155
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
156
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
157
|
+
|
|
158
|
+
const configManager = new ConfigManager();
|
|
159
|
+
expect(configManager.getConfig().mcp_bridge?.enabled).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should disable MCP bridge when MCP_BRIDGE_ENABLED=false', () => {
|
|
163
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
164
|
+
process.env.MCP_BRIDGE_ENABLED = 'false';
|
|
165
|
+
|
|
166
|
+
const configManager = new ConfigManager();
|
|
167
|
+
expect(configManager.getConfig().mcp_bridge?.enabled).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should set approval mode from environment', () => {
|
|
171
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
172
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
173
|
+
process.env.MCP_APPROVAL_MODE = 'always';
|
|
174
|
+
|
|
175
|
+
const configManager = new ConfigManager();
|
|
176
|
+
expect(configManager.getConfig().mcp_bridge?.approval_mode).toBe('always');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should set approval timeout from environment', () => {
|
|
180
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
181
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
182
|
+
process.env.MCP_APPROVAL_TIMEOUT = '120';
|
|
183
|
+
|
|
184
|
+
const configManager = new ConfigManager();
|
|
185
|
+
expect(configManager.getConfig().mcp_bridge?.approval_timeout).toBe(120);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should parse trusted tools from environment', () => {
|
|
189
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
190
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
191
|
+
process.env.MCP_TRUSTED_TOOLS = 'tool1, tool2, tool3';
|
|
192
|
+
|
|
193
|
+
const configManager = new ConfigManager();
|
|
194
|
+
expect(configManager.getConfig().mcp_bridge?.trusted_tools).toEqual(['tool1', 'tool2', 'tool3']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should parse trusted tools by server from environment', () => {
|
|
198
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
199
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
200
|
+
process.env.MCP_TRUSTED_TOOLS_FILESYSTEM = 'read_file,write_file';
|
|
201
|
+
|
|
202
|
+
const configManager = new ConfigManager();
|
|
203
|
+
const trustedByServer = configManager.getConfig().mcp_bridge?.trusted_tools_by_server;
|
|
204
|
+
expect(trustedByServer?.filesystem).toEqual(['read_file', 'write_file']);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should handle wildcard trusted tools for server', () => {
|
|
208
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
209
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
210
|
+
process.env.MCP_TRUSTED_TOOLS_INTERNAL_API = '*';
|
|
211
|
+
|
|
212
|
+
const configManager = new ConfigManager();
|
|
213
|
+
const trustedByServer = configManager.getConfig().mcp_bridge?.trusted_tools_by_server;
|
|
214
|
+
expect(trustedByServer?.['internal-api']).toEqual(['*']);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should auto-enable MCP bridge when MCP_SERVER_ env vars exist', () => {
|
|
218
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
219
|
+
process.env.MCP_SERVER_TEST_TYPE = 'stdio';
|
|
220
|
+
process.env.MCP_SERVER_TEST_COMMAND = '/usr/bin/test-server';
|
|
221
|
+
|
|
222
|
+
const configManager = new ConfigManager();
|
|
223
|
+
expect(configManager.getConfig().mcp_bridge?.enabled).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should set approval mode to trusted', () => {
|
|
227
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
228
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
229
|
+
process.env.MCP_APPROVAL_MODE = 'trusted';
|
|
230
|
+
|
|
231
|
+
const configManager = new ConfigManager();
|
|
232
|
+
expect(configManager.getConfig().mcp_bridge?.approval_mode).toBe('trusted');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should set approval mode to never', () => {
|
|
236
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
237
|
+
process.env.MCP_BRIDGE_ENABLED = 'true';
|
|
238
|
+
process.env.MCP_APPROVAL_MODE = 'never';
|
|
239
|
+
|
|
240
|
+
const configManager = new ConfigManager();
|
|
241
|
+
expect(configManager.getConfig().mcp_bridge?.approval_mode).toBe('never');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('ConfigManager - MCP Server Config from Environment', () => {
|
|
246
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
247
|
+
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
originalEnv = { ...process.env };
|
|
250
|
+
jest.clearAllMocks();
|
|
251
|
+
Object.keys(process.env).forEach(key => {
|
|
252
|
+
if (key.startsWith('MCP_')) delete process.env[key];
|
|
253
|
+
});
|
|
254
|
+
delete process.env.OPENAI_API_KEY;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
afterEach(() => {
|
|
258
|
+
process.env = originalEnv;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should configure stdio MCP server from environment', () => {
|
|
262
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
263
|
+
process.env.MCP_SERVER_FILESYSTEM_TYPE = 'stdio';
|
|
264
|
+
process.env.MCP_SERVER_FILESYSTEM_COMMAND = 'npx @modelcontextprotocol/server-filesystem';
|
|
265
|
+
process.env.MCP_SERVER_FILESYSTEM_ARGS = '/home/user,/tmp';
|
|
266
|
+
|
|
267
|
+
const configManager = new ConfigManager();
|
|
268
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
269
|
+
|
|
270
|
+
expect(servers).toBeDefined();
|
|
271
|
+
expect(servers?.length).toBeGreaterThan(0);
|
|
272
|
+
const fsServer = servers?.find(s => s.name === 'filesystem');
|
|
273
|
+
expect(fsServer).toBeDefined();
|
|
274
|
+
expect(fsServer?.type).toBe('stdio');
|
|
275
|
+
expect(fsServer?.command).toBe('npx @modelcontextprotocol/server-filesystem');
|
|
276
|
+
expect(fsServer?.args).toEqual(['/home/user', '/tmp']);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should configure http MCP server from environment', () => {
|
|
280
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
281
|
+
process.env.MCP_SERVER_MYAPI_TYPE = 'http';
|
|
282
|
+
process.env.MCP_SERVER_MYAPI_URL = 'https://api.example.com/mcp';
|
|
283
|
+
process.env.MCP_SERVER_MYAPI_API_KEY = 'server-api-key';
|
|
284
|
+
|
|
285
|
+
const configManager = new ConfigManager();
|
|
286
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
287
|
+
|
|
288
|
+
const httpServer = servers?.find(s => s.name === 'myapi');
|
|
289
|
+
expect(httpServer).toBeDefined();
|
|
290
|
+
expect(httpServer?.type).toBe('http');
|
|
291
|
+
expect(httpServer?.url).toBe('https://api.example.com/mcp');
|
|
292
|
+
expect(httpServer?.apiKey).toBe('server-api-key');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should skip stdio server without command', () => {
|
|
296
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
297
|
+
process.env.MCP_SERVER_NOCOMMAND_TYPE = 'stdio';
|
|
298
|
+
// Missing COMMAND
|
|
299
|
+
|
|
300
|
+
const configManager = new ConfigManager();
|
|
301
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
302
|
+
|
|
303
|
+
const nocommandServer = servers?.find(s => s.name === 'nocommand');
|
|
304
|
+
expect(nocommandServer).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should skip http server without URL', () => {
|
|
308
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
309
|
+
process.env.MCP_SERVER_NOURL_TYPE = 'http';
|
|
310
|
+
// Missing URL
|
|
311
|
+
|
|
312
|
+
const configManager = new ConfigManager();
|
|
313
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
314
|
+
|
|
315
|
+
const nourlServer = servers?.find(s => s.name === 'nourl');
|
|
316
|
+
expect(nourlServer).toBeUndefined();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should handle disabled MCP server', () => {
|
|
320
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
321
|
+
process.env.MCP_SERVER_DISABLED_TYPE = 'stdio';
|
|
322
|
+
process.env.MCP_SERVER_DISABLED_COMMAND = 'some-command';
|
|
323
|
+
process.env.MCP_SERVER_DISABLED_ENABLED = 'false';
|
|
324
|
+
|
|
325
|
+
const configManager = new ConfigManager();
|
|
326
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
327
|
+
|
|
328
|
+
const disabledServer = servers?.find(s => s.name === 'disabled');
|
|
329
|
+
expect(disabledServer?.enabled).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should configure retry settings from environment', () => {
|
|
333
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
334
|
+
process.env.MCP_SERVER_RETRYTEST_TYPE = 'stdio';
|
|
335
|
+
process.env.MCP_SERVER_RETRYTEST_COMMAND = 'test-command';
|
|
336
|
+
process.env.MCP_SERVER_RETRYTEST_RETRY_ATTEMPTS = '5';
|
|
337
|
+
process.env.MCP_SERVER_RETRYTEST_RETRY_DELAY = '2000';
|
|
338
|
+
|
|
339
|
+
const configManager = new ConfigManager();
|
|
340
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
341
|
+
|
|
342
|
+
const retryServer = servers?.find(s => s.name === 'retrytest');
|
|
343
|
+
expect(retryServer?.retryAttempts).toBe(5);
|
|
344
|
+
expect(retryServer?.retryDelay).toBe(2000);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should convert server names to lowercase with hyphens', () => {
|
|
348
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
349
|
+
process.env.MCP_SERVER_MY_FANCY_SERVER_TYPE = 'stdio';
|
|
350
|
+
process.env.MCP_SERVER_MY_FANCY_SERVER_COMMAND = 'fancy-command';
|
|
351
|
+
|
|
352
|
+
const configManager = new ConfigManager();
|
|
353
|
+
const servers = configManager.getConfig().mcp_bridge?.mcp_servers;
|
|
354
|
+
|
|
355
|
+
const fancyServer = servers?.find(s => s.name === 'my-fancy-server');
|
|
356
|
+
expect(fancyServer).toBeDefined();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('ConfigManager - Public Methods', () => {
|
|
361
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
362
|
+
|
|
363
|
+
beforeEach(() => {
|
|
364
|
+
originalEnv = { ...process.env };
|
|
365
|
+
jest.clearAllMocks();
|
|
366
|
+
delete process.env.OPENAI_API_KEY;
|
|
367
|
+
delete process.env.GEMINI_API_KEY;
|
|
368
|
+
Object.keys(process.env).forEach(key => {
|
|
369
|
+
if (key.startsWith('CUSTOM_')) delete process.env[key];
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
afterEach(() => {
|
|
374
|
+
process.env = originalEnv;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('getProvider', () => {
|
|
378
|
+
it('should return specific provider by name', () => {
|
|
379
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
380
|
+
process.env.GEMINI_API_KEY = 'gemini-key';
|
|
381
|
+
|
|
382
|
+
const configManager = new ConfigManager();
|
|
383
|
+
const provider = configManager.getProvider('openai');
|
|
384
|
+
|
|
385
|
+
expect(provider).toBeDefined();
|
|
386
|
+
expect(provider.api_key).toBe('openai-key');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should return undefined for non-existent provider', () => {
|
|
390
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
391
|
+
|
|
392
|
+
const configManager = new ConfigManager();
|
|
393
|
+
const provider = configManager.getProvider('nonexistent');
|
|
394
|
+
|
|
395
|
+
expect(provider).toBeUndefined();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('getDefaultProvider', () => {
|
|
400
|
+
it('should return the default provider', () => {
|
|
401
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
402
|
+
process.env.DEFAULT_PROVIDER = 'openai';
|
|
403
|
+
|
|
404
|
+
const configManager = new ConfigManager();
|
|
405
|
+
const provider = configManager.getDefaultProvider();
|
|
406
|
+
|
|
407
|
+
expect(provider).toBeDefined();
|
|
408
|
+
expect(provider.api_key).toBe('openai-key');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should use first provider as default when none specified', () => {
|
|
412
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
413
|
+
// No DEFAULT_PROVIDER set
|
|
414
|
+
|
|
415
|
+
const configManager = new ConfigManager();
|
|
416
|
+
const provider = configManager.getDefaultProvider();
|
|
417
|
+
|
|
418
|
+
expect(provider).toBeDefined();
|
|
419
|
+
expect(provider.api_key).toBe('openai-key');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('updateConfig', () => {
|
|
424
|
+
it('should update config with partial updates', () => {
|
|
425
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
426
|
+
|
|
427
|
+
const configManager = new ConfigManager();
|
|
428
|
+
const originalTemp = configManager.getConfig().default_temperature;
|
|
429
|
+
|
|
430
|
+
configManager.updateConfig({ default_temperature: 0.99 });
|
|
431
|
+
|
|
432
|
+
expect(configManager.getConfig().default_temperature).toBe(0.99);
|
|
433
|
+
expect(configManager.getConfig().default_temperature).not.toBe(originalTemp);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should preserve existing config when updating', () => {
|
|
437
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
438
|
+
|
|
439
|
+
const configManager = new ConfigManager();
|
|
440
|
+
configManager.updateConfig({ log_level: 'debug' });
|
|
441
|
+
|
|
442
|
+
expect(configManager.getConfig().log_level).toBe('debug');
|
|
443
|
+
expect(configManager.getConfig().providers.openai).toBeDefined();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('environment overrides', () => {
|
|
448
|
+
it('should override default_provider from environment', () => {
|
|
449
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
450
|
+
process.env.GEMINI_API_KEY = 'gemini-key';
|
|
451
|
+
process.env.DEFAULT_PROVIDER = 'gemini';
|
|
452
|
+
|
|
453
|
+
const configManager = new ConfigManager();
|
|
454
|
+
expect(configManager.getConfig().default_provider).toBe('gemini');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should override default_temperature from environment', () => {
|
|
458
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
459
|
+
process.env.DEFAULT_TEMPERATURE = '0.9';
|
|
460
|
+
|
|
461
|
+
const configManager = new ConfigManager();
|
|
462
|
+
expect(configManager.getConfig().default_temperature).toBe(0.9);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should override log_level from environment', () => {
|
|
466
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
467
|
+
process.env.LOG_LEVEL = 'debug';
|
|
468
|
+
|
|
469
|
+
const configManager = new ConfigManager();
|
|
470
|
+
expect(configManager.getConfig().log_level).toBe('debug');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
7
475
|
describe('ConfigManager - Custom Providers', () => {
|
|
8
476
|
let originalEnv: NodeJS.ProcessEnv;
|
|
9
477
|
|
package/tests/consensus.test.ts
CHANGED
|
@@ -121,6 +121,16 @@ describe('ConsensusService', () => {
|
|
|
121
121
|
expect(result.choice).toBe('');
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it('should handle malformed JSON that looks like JSON', () => {
|
|
125
|
+
// This matches the JSON regex but fails JSON.parse
|
|
126
|
+
const response = '{"choice": Option A, confidence: 80}'; // Invalid JSON - missing quotes
|
|
127
|
+
|
|
128
|
+
const result = service.parseVote(response, 'provider1', 'Duck 1', options);
|
|
129
|
+
// Should fallback and try to extract from text
|
|
130
|
+
expect(result.choice).toBe('Option A');
|
|
131
|
+
expect(result.confidence).toBe(50); // Default fallback confidence
|
|
132
|
+
});
|
|
133
|
+
|
|
124
134
|
it('should parse string confidence values', () => {
|
|
125
135
|
const response = JSON.stringify({
|
|
126
136
|
choice: 'Option A',
|
|
@@ -305,4 +305,90 @@ describe('ConversationManager', () => {
|
|
|
305
305
|
expect(conversation!.provider).toBe('groq');
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
+
|
|
309
|
+
describe('clearOldConversations', () => {
|
|
310
|
+
it('should not delete recent conversations', () => {
|
|
311
|
+
conversationManager.createConversation('test-1', 'openai');
|
|
312
|
+
conversationManager.createConversation('test-2', 'groq');
|
|
313
|
+
|
|
314
|
+
// Clear with default maxAge (24 hours)
|
|
315
|
+
conversationManager.clearOldConversations();
|
|
316
|
+
|
|
317
|
+
// Both conversations should still exist
|
|
318
|
+
expect(conversationManager.getConversation('test-1')).toBeDefined();
|
|
319
|
+
expect(conversationManager.getConversation('test-2')).toBeDefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should delete conversations older than maxAge', () => {
|
|
323
|
+
// Create conversations with mocked old dates
|
|
324
|
+
const oldDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // 2 days ago
|
|
325
|
+
|
|
326
|
+
conversationManager.createConversation('old-1', 'openai');
|
|
327
|
+
conversationManager.createConversation('recent-1', 'groq');
|
|
328
|
+
|
|
329
|
+
// Manually set the updatedAt to simulate old conversation
|
|
330
|
+
const oldConversation = conversationManager.getConversation('old-1')!;
|
|
331
|
+
oldConversation.updatedAt = oldDate;
|
|
332
|
+
|
|
333
|
+
// Clear with 24 hour maxAge
|
|
334
|
+
conversationManager.clearOldConversations(24 * 60 * 60 * 1000);
|
|
335
|
+
|
|
336
|
+
// Old conversation should be deleted
|
|
337
|
+
expect(conversationManager.getConversation('old-1')).toBeUndefined();
|
|
338
|
+
// Recent conversation should still exist
|
|
339
|
+
expect(conversationManager.getConversation('recent-1')).toBeDefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should delete multiple old conversations', () => {
|
|
343
|
+
const oldDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
344
|
+
|
|
345
|
+
conversationManager.createConversation('old-1', 'openai');
|
|
346
|
+
conversationManager.createConversation('old-2', 'groq');
|
|
347
|
+
conversationManager.createConversation('recent-1', 'gemini');
|
|
348
|
+
|
|
349
|
+
const old1 = conversationManager.getConversation('old-1')!;
|
|
350
|
+
const old2 = conversationManager.getConversation('old-2')!;
|
|
351
|
+
old1.updatedAt = oldDate;
|
|
352
|
+
old2.updatedAt = oldDate;
|
|
353
|
+
|
|
354
|
+
conversationManager.clearOldConversations();
|
|
355
|
+
|
|
356
|
+
expect(conversationManager.getConversation('old-1')).toBeUndefined();
|
|
357
|
+
expect(conversationManager.getConversation('old-2')).toBeUndefined();
|
|
358
|
+
expect(conversationManager.getConversation('recent-1')).toBeDefined();
|
|
359
|
+
expect(conversationManager.listConversations()).toHaveLength(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should handle empty conversation list', () => {
|
|
363
|
+
// Should not throw
|
|
364
|
+
expect(() => conversationManager.clearOldConversations()).not.toThrow();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should use custom maxAge parameter', () => {
|
|
368
|
+
conversationManager.createConversation('test-1', 'openai');
|
|
369
|
+
|
|
370
|
+
const conv = conversationManager.getConversation('test-1')!;
|
|
371
|
+
// Set updatedAt to 1 hour ago
|
|
372
|
+
conv.updatedAt = new Date(Date.now() - 60 * 60 * 1000);
|
|
373
|
+
|
|
374
|
+
// Clear with 30 minute maxAge (should delete)
|
|
375
|
+
conversationManager.clearOldConversations(30 * 60 * 1000);
|
|
376
|
+
|
|
377
|
+
expect(conversationManager.getConversation('test-1')).toBeUndefined();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should not delete conversations exactly at maxAge boundary', () => {
|
|
381
|
+
conversationManager.createConversation('test-1', 'openai');
|
|
382
|
+
|
|
383
|
+
const conv = conversationManager.getConversation('test-1')!;
|
|
384
|
+
// Set to exactly maxAge ago (should NOT be deleted as condition is >)
|
|
385
|
+
const maxAge = 60 * 60 * 1000;
|
|
386
|
+
conv.updatedAt = new Date(Date.now() - maxAge);
|
|
387
|
+
|
|
388
|
+
conversationManager.clearOldConversations(maxAge);
|
|
389
|
+
|
|
390
|
+
// Should still exist (edge case: equal time is not "older than")
|
|
391
|
+
expect(conversationManager.getConversation('test-1')).toBeDefined();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
308
394
|
});
|