mcp-rubber-duck 1.2.5 → 1.4.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.
Files changed (51) hide show
  1. package/.env.desktop.example +1 -1
  2. package/.env.pi.example +1 -1
  3. package/.env.template +1 -1
  4. package/.eslintrc.json +1 -0
  5. package/CHANGELOG.md +19 -0
  6. package/README.md +238 -44
  7. package/assets/mcp-rubber-duck.png +0 -0
  8. package/audit-ci.json +2 -1
  9. package/config/config.example.json +4 -4
  10. package/dist/config/config.js +4 -4
  11. package/dist/config/config.js.map +1 -1
  12. package/dist/config/types.d.ts +78 -0
  13. package/dist/config/types.d.ts.map +1 -1
  14. package/dist/server.d.ts.map +1 -1
  15. package/dist/server.js +150 -0
  16. package/dist/server.js.map +1 -1
  17. package/dist/services/consensus.d.ts +28 -0
  18. package/dist/services/consensus.d.ts.map +1 -0
  19. package/dist/services/consensus.js +257 -0
  20. package/dist/services/consensus.js.map +1 -0
  21. package/dist/tools/duck-debate.d.ts +16 -0
  22. package/dist/tools/duck-debate.d.ts.map +1 -0
  23. package/dist/tools/duck-debate.js +272 -0
  24. package/dist/tools/duck-debate.js.map +1 -0
  25. package/dist/tools/duck-iterate.d.ts +14 -0
  26. package/dist/tools/duck-iterate.d.ts.map +1 -0
  27. package/dist/tools/duck-iterate.js +195 -0
  28. package/dist/tools/duck-iterate.js.map +1 -0
  29. package/dist/tools/duck-judge.d.ts +15 -0
  30. package/dist/tools/duck-judge.d.ts.map +1 -0
  31. package/dist/tools/duck-judge.js +208 -0
  32. package/dist/tools/duck-judge.js.map +1 -0
  33. package/dist/tools/duck-vote.d.ts +14 -0
  34. package/dist/tools/duck-vote.d.ts.map +1 -0
  35. package/dist/tools/duck-vote.js +46 -0
  36. package/dist/tools/duck-vote.js.map +1 -0
  37. package/docker-compose.yml +1 -1
  38. package/package.json +1 -1
  39. package/src/config/config.ts +4 -4
  40. package/src/config/types.ts +92 -0
  41. package/src/server.ts +154 -0
  42. package/src/services/consensus.ts +324 -0
  43. package/src/tools/duck-debate.ts +383 -0
  44. package/src/tools/duck-iterate.ts +253 -0
  45. package/src/tools/duck-judge.ts +301 -0
  46. package/src/tools/duck-vote.ts +87 -0
  47. package/tests/consensus.test.ts +282 -0
  48. package/tests/duck-debate.test.ts +286 -0
  49. package/tests/duck-iterate.test.ts +249 -0
  50. package/tests/duck-judge.test.ts +296 -0
  51. package/tests/duck-vote.test.ts +250 -0
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+
3
+ // Mock OpenAI BEFORE importing the provider
4
+ const mockCreate = jest.fn();
5
+ jest.mock('openai', () => {
6
+ const MockOpenAI = jest.fn().mockImplementation(() => ({
7
+ chat: {
8
+ completions: {
9
+ create: mockCreate,
10
+ },
11
+ },
12
+ }));
13
+ return {
14
+ __esModule: true,
15
+ default: MockOpenAI,
16
+ };
17
+ });
18
+
19
+ // Mock config manager and logger
20
+ jest.mock('../src/config/config');
21
+ jest.mock('../src/utils/logger');
22
+
23
+ import { duckVoteTool } from '../src/tools/duck-vote';
24
+ import { ProviderManager } from '../src/providers/manager';
25
+ import { ConfigManager } from '../src/config/config';
26
+
27
+ describe('duckVoteTool', () => {
28
+ let mockProviderManager: ProviderManager;
29
+ let mockConfigManager: jest.Mocked<ConfigManager>;
30
+
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+
34
+ mockConfigManager = {
35
+ getConfig: jest.fn().mockReturnValue({
36
+ providers: {
37
+ openai: {
38
+ api_key: 'key1',
39
+ base_url: 'https://api.openai.com/v1',
40
+ default_model: 'gpt-4',
41
+ nickname: 'GPT-4',
42
+ models: ['gpt-4'],
43
+ },
44
+ gemini: {
45
+ api_key: 'key2',
46
+ base_url: 'https://api.gemini.com/v1',
47
+ default_model: 'gemini-pro',
48
+ nickname: 'Gemini',
49
+ models: ['gemini-pro'],
50
+ },
51
+ },
52
+ default_provider: 'openai',
53
+ cache_ttl: 300,
54
+ enable_failover: true,
55
+ default_temperature: 0.7,
56
+ }),
57
+ } as any;
58
+
59
+ mockProviderManager = new ProviderManager(mockConfigManager);
60
+
61
+ // Override the client method on all providers
62
+ const provider1 = mockProviderManager.getProvider('openai');
63
+ const provider2 = mockProviderManager.getProvider('gemini');
64
+ provider1['client'].chat.completions.create = mockCreate;
65
+ provider2['client'].chat.completions.create = mockCreate;
66
+ });
67
+
68
+ it('should throw error when question is missing', async () => {
69
+ await expect(
70
+ duckVoteTool(mockProviderManager, { options: ['A', 'B'] })
71
+ ).rejects.toThrow('Question is required');
72
+ });
73
+
74
+ it('should throw error when options are missing', async () => {
75
+ await expect(
76
+ duckVoteTool(mockProviderManager, { question: 'Test?' })
77
+ ).rejects.toThrow('At least 2 options are required');
78
+ });
79
+
80
+ it('should throw error when less than 2 options', async () => {
81
+ await expect(
82
+ duckVoteTool(mockProviderManager, { question: 'Test?', options: ['A'] })
83
+ ).rejects.toThrow('At least 2 options are required');
84
+ });
85
+
86
+ it('should throw error when more than 10 options', async () => {
87
+ const options = Array.from({ length: 11 }, (_, i) => `Option ${i + 1}`);
88
+ await expect(
89
+ duckVoteTool(mockProviderManager, { question: 'Test?', options })
90
+ ).rejects.toThrow('Maximum 10 options allowed');
91
+ });
92
+
93
+ it('should conduct vote with all providers', async () => {
94
+ // Mock responses with valid JSON votes
95
+ mockCreate
96
+ .mockResolvedValueOnce({
97
+ choices: [{
98
+ message: { content: '{"choice": "Option A", "confidence": 85, "reasoning": "Best for performance"}' },
99
+ finish_reason: 'stop',
100
+ }],
101
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
102
+ model: 'gpt-4',
103
+ })
104
+ .mockResolvedValueOnce({
105
+ choices: [{
106
+ message: { content: '{"choice": "Option A", "confidence": 75, "reasoning": "Scalable solution"}' },
107
+ finish_reason: 'stop',
108
+ }],
109
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
110
+ model: 'gemini-pro',
111
+ });
112
+
113
+ const result = await duckVoteTool(mockProviderManager, {
114
+ question: 'Best approach?',
115
+ options: ['Option A', 'Option B'],
116
+ });
117
+
118
+ expect(result.content).toHaveLength(1);
119
+ expect(result.content[0].type).toBe('text');
120
+
121
+ const text = result.content[0].text;
122
+ expect(text).toContain('Vote Results');
123
+ expect(text).toContain('Best approach?');
124
+ expect(text).toContain('Option A');
125
+ expect(text).toContain('Winner');
126
+ expect(text).toContain('unanimous');
127
+ });
128
+
129
+ it('should handle split votes', async () => {
130
+ mockCreate
131
+ .mockResolvedValueOnce({
132
+ choices: [{
133
+ message: { content: '{"choice": "Option A", "confidence": 60, "reasoning": "Good choice"}' },
134
+ finish_reason: 'stop',
135
+ }],
136
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
137
+ model: 'gpt-4',
138
+ })
139
+ .mockResolvedValueOnce({
140
+ choices: [{
141
+ message: { content: '{"choice": "Option B", "confidence": 90, "reasoning": "Better choice"}' },
142
+ finish_reason: 'stop',
143
+ }],
144
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
145
+ model: 'gemini-pro',
146
+ });
147
+
148
+ const result = await duckVoteTool(mockProviderManager, {
149
+ question: 'Which option?',
150
+ options: ['Option A', 'Option B'],
151
+ });
152
+
153
+ const text = result.content[0].text;
154
+ expect(text).toContain('split');
155
+ expect(text).toContain('tie-breaker');
156
+ expect(text).toContain('Option B'); // Higher confidence wins
157
+ });
158
+
159
+ it('should use specific voters when provided', async () => {
160
+ mockCreate.mockResolvedValueOnce({
161
+ choices: [{
162
+ message: { content: '{"choice": "Option A", "confidence": 80, "reasoning": "Only choice"}' },
163
+ finish_reason: 'stop',
164
+ }],
165
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
166
+ model: 'gpt-4',
167
+ });
168
+
169
+ const result = await duckVoteTool(mockProviderManager, {
170
+ question: 'Test?',
171
+ options: ['Option A', 'Option B'],
172
+ voters: ['openai'],
173
+ });
174
+
175
+ expect(mockCreate).toHaveBeenCalledTimes(1);
176
+ expect(result.content[0].text).toContain('1/1 valid votes');
177
+ });
178
+
179
+ it('should handle invalid JSON responses gracefully', async () => {
180
+ mockCreate
181
+ .mockResolvedValueOnce({
182
+ choices: [{
183
+ message: { content: 'I think Option A is clearly the best because of its simplicity.' },
184
+ finish_reason: 'stop',
185
+ }],
186
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
187
+ model: 'gpt-4',
188
+ })
189
+ .mockResolvedValueOnce({
190
+ choices: [{
191
+ message: { content: '{"choice": "Option B", "confidence": 70}' },
192
+ finish_reason: 'stop',
193
+ }],
194
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
195
+ model: 'gemini-pro',
196
+ });
197
+
198
+ const result = await duckVoteTool(mockProviderManager, {
199
+ question: 'Test?',
200
+ options: ['Option A', 'Option B'],
201
+ });
202
+
203
+ // Should still work - fallback parsing should find "Option A"
204
+ const text = result.content[0].text;
205
+ expect(text).toContain('2/2 valid votes');
206
+ });
207
+
208
+ it('should handle provider errors gracefully', async () => {
209
+ mockCreate
210
+ .mockResolvedValueOnce({
211
+ choices: [{
212
+ message: { content: '{"choice": "Option A", "confidence": 85}' },
213
+ finish_reason: 'stop',
214
+ }],
215
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
216
+ model: 'gpt-4',
217
+ })
218
+ .mockRejectedValueOnce(new Error('API Error'));
219
+
220
+ const result = await duckVoteTool(mockProviderManager, {
221
+ question: 'Test?',
222
+ options: ['Option A', 'Option B'],
223
+ });
224
+
225
+ // One valid vote, one error
226
+ const text = result.content[0].text;
227
+ expect(text).toContain('Option A');
228
+ expect(text).toContain('Invalid vote'); // Error response should be marked invalid
229
+ });
230
+
231
+ it('should work without reasoning requirement', async () => {
232
+ mockCreate.mockResolvedValueOnce({
233
+ choices: [{
234
+ message: { content: '{"choice": "Option A", "confidence": 80}' },
235
+ finish_reason: 'stop',
236
+ }],
237
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
238
+ model: 'gpt-4',
239
+ });
240
+
241
+ const result = await duckVoteTool(mockProviderManager, {
242
+ question: 'Test?',
243
+ options: ['Option A', 'Option B'],
244
+ voters: ['openai'],
245
+ require_reasoning: false,
246
+ });
247
+
248
+ expect(result.content[0].text).toContain('Option A');
249
+ });
250
+ });