mcp-rubber-duck 1.5.1 → 1.5.2

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.
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { duckCouncilTool } from '../../src/tools/duck-council.js';
3
+ import { ProviderManager } from '../../src/providers/manager.js';
4
+
5
+ // Mock dependencies
6
+ jest.mock('../../src/utils/logger');
7
+ jest.mock('../../src/providers/manager.js');
8
+
9
+ describe('duckCouncilTool', () => {
10
+ let mockProviderManager: jest.Mocked<ProviderManager>;
11
+
12
+ const mockResponses = [
13
+ {
14
+ provider: 'openai',
15
+ nickname: 'OpenAI Duck',
16
+ content: 'I think TypeScript is great for large projects.',
17
+ model: 'gpt-4',
18
+ latency: 150,
19
+ usage: { prompt_tokens: 10, completion_tokens: 25, total_tokens: 35 },
20
+ },
21
+ {
22
+ provider: 'groq',
23
+ nickname: 'Groq Duck',
24
+ content: 'TypeScript adds type safety which prevents many bugs.',
25
+ model: 'llama-3.1-70b',
26
+ latency: 80,
27
+ usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 },
28
+ },
29
+ ];
30
+
31
+ beforeEach(() => {
32
+ mockProviderManager = {
33
+ getProviderNames: jest.fn().mockReturnValue(['openai', 'groq']),
34
+ duckCouncil: jest.fn().mockResolvedValue(mockResponses),
35
+ } as unknown as jest.Mocked<ProviderManager>;
36
+ });
37
+
38
+ it('should throw error when prompt is missing', async () => {
39
+ await expect(duckCouncilTool(mockProviderManager, {})).rejects.toThrow(
40
+ 'Prompt is required for the duck council'
41
+ );
42
+ });
43
+
44
+ it('should throw error when no providers available', async () => {
45
+ mockProviderManager.getProviderNames.mockReturnValue([]);
46
+
47
+ await expect(
48
+ duckCouncilTool(mockProviderManager, { prompt: 'Test' })
49
+ ).rejects.toThrow('No ducks available for the council!');
50
+ });
51
+
52
+ it('should convene duck council with prompt', async () => {
53
+ const result = await duckCouncilTool(mockProviderManager, {
54
+ prompt: 'What is TypeScript?',
55
+ });
56
+
57
+ expect(mockProviderManager.duckCouncil).toHaveBeenCalledWith('What is TypeScript?', {
58
+ model: undefined,
59
+ });
60
+ expect(result.content).toHaveLength(1);
61
+ expect(result.content[0].type).toBe('text');
62
+ });
63
+
64
+ it('should display council topic', async () => {
65
+ const result = await duckCouncilTool(mockProviderManager, {
66
+ prompt: 'What is TypeScript?',
67
+ });
68
+
69
+ expect(result.content[0].text).toContain('Duck Council Topic');
70
+ expect(result.content[0].text).toContain('What is TypeScript?');
71
+ });
72
+
73
+ it('should display number of ducks in attendance', async () => {
74
+ const result = await duckCouncilTool(mockProviderManager, {
75
+ prompt: 'Test',
76
+ });
77
+
78
+ expect(result.content[0].text).toContain('2 ducks in attendance');
79
+ });
80
+
81
+ it('should display each duck response with number', async () => {
82
+ const result = await duckCouncilTool(mockProviderManager, {
83
+ prompt: 'Test',
84
+ });
85
+
86
+ expect(result.content[0].text).toContain('Duck #1: OpenAI Duck');
87
+ expect(result.content[0].text).toContain('Duck #2: Groq Duck');
88
+ expect(result.content[0].text).toContain('TypeScript is great');
89
+ expect(result.content[0].text).toContain('type safety');
90
+ });
91
+
92
+ it('should display model and latency metadata', async () => {
93
+ const result = await duckCouncilTool(mockProviderManager, {
94
+ prompt: 'Test',
95
+ });
96
+
97
+ expect(result.content[0].text).toContain('gpt-4');
98
+ expect(result.content[0].text).toContain('150ms');
99
+ expect(result.content[0].text).toContain('35 tokens');
100
+ });
101
+
102
+ it('should handle error responses from ducks', async () => {
103
+ mockProviderManager.duckCouncil.mockResolvedValue([
104
+ mockResponses[0],
105
+ {
106
+ provider: 'groq',
107
+ nickname: 'Groq Duck',
108
+ content: 'Error: API key invalid',
109
+ model: '',
110
+ latency: 0,
111
+ },
112
+ ]);
113
+
114
+ const result = await duckCouncilTool(mockProviderManager, {
115
+ prompt: 'Test',
116
+ });
117
+
118
+ expect(result.content[0].text).toContain('Duck had to leave early');
119
+ expect(result.content[0].text).toContain('API key invalid');
120
+ });
121
+
122
+ it('should show council summary with success count', async () => {
123
+ const result = await duckCouncilTool(mockProviderManager, {
124
+ prompt: 'Test',
125
+ });
126
+
127
+ expect(result.content[0].text).toContain('Council Summary');
128
+ expect(result.content[0].text).toContain('2/2 ducks provided their wisdom');
129
+ });
130
+
131
+ it('should show partial council message when some fail', async () => {
132
+ mockProviderManager.duckCouncil.mockResolvedValue([
133
+ mockResponses[0],
134
+ {
135
+ provider: 'groq',
136
+ nickname: 'Groq Duck',
137
+ content: 'Error: Timeout',
138
+ model: '',
139
+ latency: 0,
140
+ },
141
+ ]);
142
+
143
+ const result = await duckCouncilTool(mockProviderManager, {
144
+ prompt: 'Test',
145
+ });
146
+
147
+ expect(result.content[0].text).toContain('1/2 ducks provided their wisdom');
148
+ expect(result.content[0].text).toContain('Partial council');
149
+ });
150
+
151
+ it('should pass model option', async () => {
152
+ await duckCouncilTool(mockProviderManager, {
153
+ prompt: 'Test',
154
+ model: 'gpt-3.5-turbo',
155
+ });
156
+
157
+ expect(mockProviderManager.duckCouncil).toHaveBeenCalledWith('Test', {
158
+ model: 'gpt-3.5-turbo',
159
+ });
160
+ });
161
+
162
+ it('should not show latency when zero', async () => {
163
+ mockProviderManager.duckCouncil.mockResolvedValue([
164
+ {
165
+ ...mockResponses[0],
166
+ latency: 0,
167
+ },
168
+ ]);
169
+
170
+ const result = await duckCouncilTool(mockProviderManager, {
171
+ prompt: 'Test',
172
+ });
173
+
174
+ // Should not show "0ms" in output
175
+ expect(result.content[0].text).not.toContain('0ms');
176
+ });
177
+
178
+ it('should not show tokens when usage missing', async () => {
179
+ mockProviderManager.duckCouncil.mockResolvedValue([
180
+ {
181
+ ...mockResponses[0],
182
+ usage: undefined,
183
+ },
184
+ ]);
185
+
186
+ const result = await duckCouncilTool(mockProviderManager, {
187
+ prompt: 'Test',
188
+ });
189
+
190
+ expect(result.content[0].text).not.toContain('tokens');
191
+ });
192
+
193
+ it('should show error message when all ducks fail', async () => {
194
+ mockProviderManager.duckCouncil.mockResolvedValue([
195
+ {
196
+ provider: 'openai',
197
+ nickname: 'OpenAI Duck',
198
+ content: 'Error: Connection timeout',
199
+ model: '',
200
+ latency: 0,
201
+ },
202
+ {
203
+ provider: 'groq',
204
+ nickname: 'Groq Duck',
205
+ content: 'Error: API error',
206
+ model: '',
207
+ latency: 0,
208
+ },
209
+ ]);
210
+
211
+ const result = await duckCouncilTool(mockProviderManager, {
212
+ prompt: 'Test',
213
+ });
214
+
215
+ expect(result.content[0].text).toContain('0/2 ducks provided their wisdom');
216
+ // Should not show partial council message
217
+ expect(result.content[0].text).not.toContain('Partial council');
218
+ });
219
+ });
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { getPendingApprovalsTool } from '../../src/tools/get-pending-approvals.js';
3
+ import { ApprovalService } from '../../src/services/approval.js';
4
+
5
+ // Mock dependencies
6
+ jest.mock('../../src/utils/logger');
7
+ jest.mock('../../src/services/approval.js');
8
+
9
+ describe('getPendingApprovalsTool', () => {
10
+ let mockApprovalService: jest.Mocked<ApprovalService>;
11
+
12
+ const now = Date.now();
13
+ const mockApprovals = [
14
+ {
15
+ id: 'approval-1',
16
+ duckName: 'OpenAI Duck',
17
+ mcpServer: 'filesystem',
18
+ toolName: 'read_file',
19
+ arguments: { path: '/tmp/test.txt' },
20
+ status: 'pending' as const,
21
+ timestamp: now - 30000, // 30 seconds ago
22
+ expiresAt: now + 30000, // expires in 30 seconds
23
+ },
24
+ {
25
+ id: 'approval-2',
26
+ duckName: 'Groq Duck',
27
+ mcpServer: 'database',
28
+ toolName: 'query',
29
+ arguments: { sql: 'SELECT * FROM users' },
30
+ status: 'pending' as const,
31
+ timestamp: now - 10000, // 10 seconds ago
32
+ expiresAt: now + 50000, // expires in 50 seconds
33
+ },
34
+ ];
35
+
36
+ beforeEach(() => {
37
+ mockApprovalService = {
38
+ getPendingApprovals: jest.fn(),
39
+ } as unknown as jest.Mocked<ApprovalService>;
40
+ });
41
+
42
+ describe('no pending approvals', () => {
43
+ it('should return success message when no approvals exist', () => {
44
+ mockApprovalService.getPendingApprovals.mockReturnValue([]);
45
+
46
+ const result = getPendingApprovalsTool(mockApprovalService, {});
47
+
48
+ expect(result.content[0].text).toContain('No pending MCP tool approvals');
49
+ expect(result.isError).toBeUndefined();
50
+ });
51
+ });
52
+
53
+ describe('with pending approvals', () => {
54
+ beforeEach(() => {
55
+ mockApprovalService.getPendingApprovals.mockReturnValue(mockApprovals);
56
+ });
57
+
58
+ it('should list all pending approvals', () => {
59
+ const result = getPendingApprovalsTool(mockApprovalService, {});
60
+
61
+ expect(result.content[0].text).toContain('2 pending MCP approvals');
62
+ expect(result.content[0].text).toContain('OpenAI Duck');
63
+ expect(result.content[0].text).toContain('Groq Duck');
64
+ expect(result.content[0].text).toContain('filesystem:read_file');
65
+ expect(result.content[0].text).toContain('database:query');
66
+ });
67
+
68
+ it('should display approval IDs', () => {
69
+ const result = getPendingApprovalsTool(mockApprovalService, {});
70
+
71
+ expect(result.content[0].text).toContain('approval-1');
72
+ expect(result.content[0].text).toContain('approval-2');
73
+ });
74
+
75
+ it('should display arguments for small payloads', () => {
76
+ const result = getPendingApprovalsTool(mockApprovalService, {});
77
+
78
+ expect(result.content[0].text).toContain('/tmp/test.txt');
79
+ expect(result.content[0].text).toContain('SELECT * FROM users');
80
+ });
81
+
82
+ it('should show parameter count for large arguments', () => {
83
+ mockApprovalService.getPendingApprovals.mockReturnValue([
84
+ {
85
+ ...mockApprovals[0],
86
+ arguments: {
87
+ param1: 'a'.repeat(50),
88
+ param2: 'b'.repeat(50),
89
+ param3: 'c'.repeat(50),
90
+ },
91
+ },
92
+ ]);
93
+
94
+ const result = getPendingApprovalsTool(mockApprovalService, {});
95
+
96
+ expect(result.content[0].text).toContain('3 parameters');
97
+ });
98
+
99
+ it('should include usage hint', () => {
100
+ const result = getPendingApprovalsTool(mockApprovalService, {});
101
+
102
+ expect(result.content[0].text).toContain('approve_mcp_request');
103
+ });
104
+
105
+ it('should handle singular approval text', () => {
106
+ mockApprovalService.getPendingApprovals.mockReturnValue([mockApprovals[0]]);
107
+
108
+ const result = getPendingApprovalsTool(mockApprovalService, {});
109
+
110
+ expect(result.content[0].text).toContain('1 pending MCP approval:');
111
+ expect(result.content[0].text).not.toContain('approvals:');
112
+ });
113
+ });
114
+
115
+ describe('filtering by duck', () => {
116
+ beforeEach(() => {
117
+ mockApprovalService.getPendingApprovals.mockReturnValue(mockApprovals);
118
+ });
119
+
120
+ it('should filter approvals by duck name', () => {
121
+ const result = getPendingApprovalsTool(mockApprovalService, {
122
+ duck: 'OpenAI Duck',
123
+ });
124
+
125
+ expect(result.content[0].text).toContain('OpenAI Duck');
126
+ expect(result.content[0].text).not.toContain('Groq Duck');
127
+ expect(result.content[0].text).toContain('1 pending MCP approval');
128
+ });
129
+
130
+ it('should return no approvals when duck filter has no matches', () => {
131
+ const result = getPendingApprovalsTool(mockApprovalService, {
132
+ duck: 'NonexistentDuck',
133
+ });
134
+
135
+ expect(result.content[0].text).toContain('No pending MCP tool approvals');
136
+ });
137
+ });
138
+
139
+ describe('time formatting', () => {
140
+ it('should show time since request', () => {
141
+ mockApprovalService.getPendingApprovals.mockReturnValue([mockApprovals[0]]);
142
+
143
+ const result = getPendingApprovalsTool(mockApprovalService, {});
144
+
145
+ // Approval was 30 seconds ago
146
+ expect(result.content[0].text).toMatch(/Requested: \d+s ago/);
147
+ });
148
+
149
+ it('should show expiration time', () => {
150
+ mockApprovalService.getPendingApprovals.mockReturnValue([mockApprovals[0]]);
151
+
152
+ const result = getPendingApprovalsTool(mockApprovalService, {});
153
+
154
+ expect(result.content[0].text).toMatch(/Expires: \d+s/);
155
+ });
156
+
157
+ it('should show expired status for expired approvals', () => {
158
+ mockApprovalService.getPendingApprovals.mockReturnValue([
159
+ {
160
+ ...mockApprovals[0],
161
+ expiresAt: now - 1000, // Already expired
162
+ },
163
+ ]);
164
+
165
+ const result = getPendingApprovalsTool(mockApprovalService, {});
166
+
167
+ expect(result.content[0].text).toContain('Expires: expired');
168
+ });
169
+ });
170
+
171
+ describe('error handling', () => {
172
+ it('should handle exceptions gracefully', () => {
173
+ mockApprovalService.getPendingApprovals.mockImplementation(() => {
174
+ throw new Error('Service unavailable');
175
+ });
176
+
177
+ const result = getPendingApprovalsTool(mockApprovalService, {});
178
+
179
+ expect(result.isError).toBe(true);
180
+ expect(result.content[0].text).toContain('Failed to get pending approvals');
181
+ expect(result.content[0].text).toContain('Service unavailable');
182
+ });
183
+
184
+ it('should handle non-Error exceptions', () => {
185
+ mockApprovalService.getPendingApprovals.mockImplementation(() => {
186
+ throw 'Unknown error';
187
+ });
188
+
189
+ const result = getPendingApprovalsTool(mockApprovalService, {});
190
+
191
+ expect(result.isError).toBe(true);
192
+ expect(result.content[0].text).toContain('Unknown error');
193
+ });
194
+ });
195
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
2
+ import { listDucksTool } from '../../src/tools/list-ducks.js';
3
+ import { ProviderManager } from '../../src/providers/manager.js';
4
+ import { HealthMonitor } from '../../src/services/health.js';
5
+
6
+ // Mock dependencies
7
+ jest.mock('../../src/utils/logger');
8
+ jest.mock('../../src/providers/manager.js');
9
+ jest.mock('../../src/services/health.js');
10
+
11
+ describe('listDucksTool', () => {
12
+ let mockProviderManager: jest.Mocked<ProviderManager>;
13
+ let mockHealthMonitor: jest.Mocked<HealthMonitor>;
14
+
15
+ const mockProviders = [
16
+ {
17
+ name: 'openai',
18
+ info: {
19
+ nickname: 'OpenAI Duck',
20
+ model: 'gpt-4',
21
+ baseURL: 'https://api.openai.com/v1',
22
+ hasApiKey: true,
23
+ },
24
+ },
25
+ {
26
+ name: 'groq',
27
+ info: {
28
+ nickname: 'Groq Duck',
29
+ model: 'llama-3.1-70b',
30
+ baseURL: 'https://api.groq.com/openai/v1',
31
+ hasApiKey: true,
32
+ },
33
+ },
34
+ ];
35
+
36
+ beforeEach(() => {
37
+ mockProviderManager = {
38
+ getAllProviders: jest.fn().mockReturnValue(mockProviders),
39
+ } as unknown as jest.Mocked<ProviderManager>;
40
+
41
+ mockHealthMonitor = {
42
+ performHealthChecks: jest.fn(),
43
+ } as unknown as jest.Mocked<HealthMonitor>;
44
+ });
45
+
46
+ it('should list all ducks without health check', async () => {
47
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
48
+
49
+ expect(mockProviderManager.getAllProviders).toHaveBeenCalled();
50
+ expect(mockHealthMonitor.performHealthChecks).not.toHaveBeenCalled();
51
+
52
+ expect(result.content).toHaveLength(1);
53
+ expect(result.content[0].type).toBe('text');
54
+ expect(result.content[0].text).toContain('Found 2 duck(s)');
55
+ expect(result.content[0].text).toContain('OpenAI Duck');
56
+ expect(result.content[0].text).toContain('Groq Duck');
57
+ });
58
+
59
+ it('should list ducks with health check when requested', async () => {
60
+ mockHealthMonitor.performHealthChecks.mockResolvedValue([
61
+ {
62
+ provider: 'openai',
63
+ healthy: true,
64
+ latency: 150,
65
+ lastCheck: new Date(),
66
+ },
67
+ {
68
+ provider: 'groq',
69
+ healthy: false,
70
+ lastCheck: new Date(),
71
+ error: 'Connection failed',
72
+ },
73
+ ]);
74
+
75
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
76
+ check_health: true,
77
+ });
78
+
79
+ expect(mockHealthMonitor.performHealthChecks).toHaveBeenCalled();
80
+ expect(result.content[0].text).toContain('Healthy');
81
+ expect(result.content[0].text).toContain('Unhealthy');
82
+ expect(result.content[0].text).toContain('150ms');
83
+ expect(result.content[0].text).toContain('Connection failed');
84
+ });
85
+
86
+ it('should show unknown status emoji for unchecked providers', async () => {
87
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
88
+ check_health: false,
89
+ });
90
+
91
+ // Without health check, status should be unknown (❓)
92
+ expect(result.content[0].text).toContain('❓');
93
+ });
94
+
95
+ it('should show healthy count in summary', async () => {
96
+ mockHealthMonitor.performHealthChecks.mockResolvedValue([
97
+ {
98
+ provider: 'openai',
99
+ healthy: true,
100
+ latency: 150,
101
+ lastCheck: new Date(),
102
+ },
103
+ {
104
+ provider: 'groq',
105
+ healthy: true,
106
+ latency: 80,
107
+ lastCheck: new Date(),
108
+ },
109
+ ]);
110
+
111
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {
112
+ check_health: true,
113
+ });
114
+
115
+ expect(result.content[0].text).toContain('2/2 ducks are healthy');
116
+ });
117
+
118
+ it('should handle empty provider list', async () => {
119
+ mockProviderManager.getAllProviders.mockReturnValue([]);
120
+
121
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
122
+
123
+ expect(result.content[0].text).toContain('Found 0 duck(s)');
124
+ expect(result.content[0].text).toContain('0/0 ducks are healthy');
125
+ });
126
+
127
+ it('should display provider without API key correctly', async () => {
128
+ mockProviderManager.getAllProviders.mockReturnValue([
129
+ {
130
+ name: 'ollama',
131
+ info: {
132
+ nickname: 'Ollama Duck',
133
+ model: 'llama3',
134
+ baseURL: 'http://localhost:11434/v1',
135
+ hasApiKey: false,
136
+ },
137
+ },
138
+ ]);
139
+
140
+ const result = await listDucksTool(mockProviderManager, mockHealthMonitor, {});
141
+
142
+ expect(result.content[0].text).toContain('Not required');
143
+ });
144
+ });