@vibescope/mcp-server 0.0.1 → 0.2.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 (173) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1169 -0
  3. package/dist/api-client.js +713 -0
  4. package/dist/cli.d.ts +1 -6
  5. package/dist/cli.js +39 -240
  6. package/dist/config/tool-categories.d.ts +31 -0
  7. package/dist/config/tool-categories.js +253 -0
  8. package/dist/handlers/blockers.js +57 -58
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +108 -477
  11. package/dist/handlers/cost.d.ts +1 -0
  12. package/dist/handlers/cost.js +35 -113
  13. package/dist/handlers/decisions.d.ts +2 -0
  14. package/dist/handlers/decisions.js +28 -27
  15. package/dist/handlers/deployment.js +113 -828
  16. package/dist/handlers/discovery.d.ts +3 -0
  17. package/dist/handlers/discovery.js +26 -627
  18. package/dist/handlers/fallback.d.ts +2 -0
  19. package/dist/handlers/fallback.js +56 -142
  20. package/dist/handlers/findings.d.ts +8 -1
  21. package/dist/handlers/findings.js +65 -68
  22. package/dist/handlers/git-issues.d.ts +9 -13
  23. package/dist/handlers/git-issues.js +80 -225
  24. package/dist/handlers/ideas.d.ts +3 -0
  25. package/dist/handlers/ideas.js +53 -134
  26. package/dist/handlers/index.d.ts +2 -0
  27. package/dist/handlers/index.js +6 -0
  28. package/dist/handlers/milestones.d.ts +2 -0
  29. package/dist/handlers/milestones.js +51 -98
  30. package/dist/handlers/organizations.js +79 -275
  31. package/dist/handlers/progress.d.ts +2 -0
  32. package/dist/handlers/progress.js +25 -123
  33. package/dist/handlers/project.js +42 -221
  34. package/dist/handlers/requests.d.ts +2 -0
  35. package/dist/handlers/requests.js +23 -83
  36. package/dist/handlers/session.js +119 -590
  37. package/dist/handlers/sprints.d.ts +32 -0
  38. package/dist/handlers/sprints.js +275 -0
  39. package/dist/handlers/tasks.d.ts +7 -10
  40. package/dist/handlers/tasks.js +245 -894
  41. package/dist/handlers/tool-docs.d.ts +9 -0
  42. package/dist/handlers/tool-docs.js +904 -0
  43. package/dist/handlers/types.d.ts +11 -3
  44. package/dist/handlers/validation.d.ts +1 -1
  45. package/dist/handlers/validation.js +38 -153
  46. package/dist/index.js +493 -162
  47. package/dist/knowledge.js +106 -9
  48. package/dist/tools.js +34 -4
  49. package/dist/validators.d.ts +21 -0
  50. package/dist/validators.js +91 -0
  51. package/package.json +2 -3
  52. package/src/api-client.ts +1822 -0
  53. package/src/cli.test.ts +128 -302
  54. package/src/cli.ts +41 -285
  55. package/src/handlers/__test-setup__.ts +215 -0
  56. package/src/handlers/__test-utils__.ts +4 -134
  57. package/src/handlers/blockers.test.ts +114 -124
  58. package/src/handlers/blockers.ts +68 -70
  59. package/src/handlers/bodies-of-work.test.ts +236 -831
  60. package/src/handlers/bodies-of-work.ts +210 -525
  61. package/src/handlers/cost.test.ts +149 -113
  62. package/src/handlers/cost.ts +44 -132
  63. package/src/handlers/decisions.test.ts +111 -209
  64. package/src/handlers/decisions.ts +35 -27
  65. package/src/handlers/deployment.test.ts +193 -239
  66. package/src/handlers/deployment.ts +143 -896
  67. package/src/handlers/discovery.test.ts +20 -67
  68. package/src/handlers/discovery.ts +29 -714
  69. package/src/handlers/fallback.test.ts +206 -361
  70. package/src/handlers/fallback.ts +81 -156
  71. package/src/handlers/findings.test.ts +229 -320
  72. package/src/handlers/findings.ts +76 -64
  73. package/src/handlers/git-issues.test.ts +623 -0
  74. package/src/handlers/git-issues.ts +174 -0
  75. package/src/handlers/ideas.test.ts +229 -343
  76. package/src/handlers/ideas.ts +69 -143
  77. package/src/handlers/index.ts +6 -0
  78. package/src/handlers/milestones.test.ts +167 -281
  79. package/src/handlers/milestones.ts +54 -93
  80. package/src/handlers/organizations.test.ts +275 -467
  81. package/src/handlers/organizations.ts +84 -294
  82. package/src/handlers/progress.test.ts +112 -218
  83. package/src/handlers/progress.ts +29 -142
  84. package/src/handlers/project.test.ts +203 -226
  85. package/src/handlers/project.ts +48 -238
  86. package/src/handlers/requests.test.ts +74 -342
  87. package/src/handlers/requests.ts +25 -83
  88. package/src/handlers/session.test.ts +276 -206
  89. package/src/handlers/session.ts +136 -662
  90. package/src/handlers/sprints.test.ts +711 -0
  91. package/src/handlers/sprints.ts +510 -0
  92. package/src/handlers/tasks.test.ts +669 -353
  93. package/src/handlers/tasks.ts +263 -1015
  94. package/src/handlers/tool-docs.ts +1024 -0
  95. package/src/handlers/types.ts +12 -4
  96. package/src/handlers/validation.test.ts +237 -568
  97. package/src/handlers/validation.ts +43 -167
  98. package/src/index.ts +493 -186
  99. package/src/tools.ts +2532 -0
  100. package/src/validators.test.ts +223 -223
  101. package/src/validators.ts +127 -0
  102. package/tsconfig.json +1 -1
  103. package/vitest.config.ts +14 -13
  104. package/dist/cli.test.d.ts +0 -1
  105. package/dist/cli.test.js +0 -367
  106. package/dist/handlers/__test-utils__.d.ts +0 -72
  107. package/dist/handlers/__test-utils__.js +0 -176
  108. package/dist/handlers/checkouts.d.ts +0 -37
  109. package/dist/handlers/checkouts.js +0 -377
  110. package/dist/handlers/knowledge-query.d.ts +0 -22
  111. package/dist/handlers/knowledge-query.js +0 -253
  112. package/dist/handlers/knowledge.d.ts +0 -12
  113. package/dist/handlers/knowledge.js +0 -108
  114. package/dist/handlers/roles.d.ts +0 -30
  115. package/dist/handlers/roles.js +0 -281
  116. package/dist/handlers/tasks.test.d.ts +0 -1
  117. package/dist/handlers/tasks.test.js +0 -431
  118. package/dist/utils.test.d.ts +0 -1
  119. package/dist/utils.test.js +0 -532
  120. package/dist/validators.test.d.ts +0 -1
  121. package/dist/validators.test.js +0 -176
  122. package/src/knowledge.ts +0 -132
  123. package/src/tmpclaude-0078-cwd +0 -1
  124. package/src/tmpclaude-0ee1-cwd +0 -1
  125. package/src/tmpclaude-2dd5-cwd +0 -1
  126. package/src/tmpclaude-344c-cwd +0 -1
  127. package/src/tmpclaude-3860-cwd +0 -1
  128. package/src/tmpclaude-4b63-cwd +0 -1
  129. package/src/tmpclaude-5c73-cwd +0 -1
  130. package/src/tmpclaude-5ee3-cwd +0 -1
  131. package/src/tmpclaude-6795-cwd +0 -1
  132. package/src/tmpclaude-709e-cwd +0 -1
  133. package/src/tmpclaude-9839-cwd +0 -1
  134. package/src/tmpclaude-d829-cwd +0 -1
  135. package/src/tmpclaude-e072-cwd +0 -1
  136. package/src/tmpclaude-f6ee-cwd +0 -1
  137. package/tmpclaude-0439-cwd +0 -1
  138. package/tmpclaude-132f-cwd +0 -1
  139. package/tmpclaude-15bb-cwd +0 -1
  140. package/tmpclaude-165a-cwd +0 -1
  141. package/tmpclaude-1ba9-cwd +0 -1
  142. package/tmpclaude-21a3-cwd +0 -1
  143. package/tmpclaude-2a38-cwd +0 -1
  144. package/tmpclaude-2adf-cwd +0 -1
  145. package/tmpclaude-2f56-cwd +0 -1
  146. package/tmpclaude-3626-cwd +0 -1
  147. package/tmpclaude-3727-cwd +0 -1
  148. package/tmpclaude-40bc-cwd +0 -1
  149. package/tmpclaude-436f-cwd +0 -1
  150. package/tmpclaude-4783-cwd +0 -1
  151. package/tmpclaude-4b6d-cwd +0 -1
  152. package/tmpclaude-4ba4-cwd +0 -1
  153. package/tmpclaude-51e6-cwd +0 -1
  154. package/tmpclaude-5ecf-cwd +0 -1
  155. package/tmpclaude-6f97-cwd +0 -1
  156. package/tmpclaude-7fb2-cwd +0 -1
  157. package/tmpclaude-825c-cwd +0 -1
  158. package/tmpclaude-8baf-cwd +0 -1
  159. package/tmpclaude-8d9f-cwd +0 -1
  160. package/tmpclaude-975c-cwd +0 -1
  161. package/tmpclaude-9983-cwd +0 -1
  162. package/tmpclaude-a045-cwd +0 -1
  163. package/tmpclaude-ac4a-cwd +0 -1
  164. package/tmpclaude-b593-cwd +0 -1
  165. package/tmpclaude-b891-cwd +0 -1
  166. package/tmpclaude-c032-cwd +0 -1
  167. package/tmpclaude-cf43-cwd +0 -1
  168. package/tmpclaude-d040-cwd +0 -1
  169. package/tmpclaude-dcdd-cwd +0 -1
  170. package/tmpclaude-dcee-cwd +0 -1
  171. package/tmpclaude-e16b-cwd +0 -1
  172. package/tmpclaude-ecd2-cwd +0 -1
  173. package/tmpclaude-f48d-cwd +0 -1
@@ -1,116 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { SupabaseClient } from '@supabase/supabase-js';
3
- import type { HandlerContext, TokenUsage } from './types.js';
4
2
  import {
5
3
  addFinding,
6
4
  getFindings,
5
+ getFindingsStats,
7
6
  updateFinding,
8
7
  deleteFinding,
9
8
  } from './findings.js';
10
9
  import { ValidationError } from '../validators.js';
11
-
12
- // ============================================================================
13
- // Test Utilities
14
- // ============================================================================
15
-
16
- function createMockSupabase(overrides: {
17
- selectResult?: { data: unknown; error: unknown };
18
- insertResult?: { data: unknown; error: unknown };
19
- updateResult?: { data: unknown; error: unknown };
20
- deleteResult?: { data: unknown; error: unknown };
21
- } = {}) {
22
- const defaultResult = { data: null, error: null };
23
- let currentOperation = 'select';
24
- let insertThenSelect = false;
25
-
26
- const mock = {
27
- from: vi.fn().mockReturnThis(),
28
- select: vi.fn(() => {
29
- if (currentOperation === 'insert') {
30
- insertThenSelect = true;
31
- } else {
32
- currentOperation = 'select';
33
- insertThenSelect = false;
34
- }
35
- return mock;
36
- }),
37
- insert: vi.fn(() => {
38
- currentOperation = 'insert';
39
- insertThenSelect = false;
40
- return mock;
41
- }),
42
- update: vi.fn(() => {
43
- currentOperation = 'update';
44
- insertThenSelect = false;
45
- return mock;
46
- }),
47
- delete: vi.fn(() => {
48
- currentOperation = 'delete';
49
- insertThenSelect = false;
50
- return mock;
51
- }),
52
- eq: vi.fn().mockReturnThis(),
53
- neq: vi.fn().mockReturnThis(),
54
- in: vi.fn().mockReturnThis(),
55
- is: vi.fn().mockReturnThis(),
56
- order: vi.fn().mockReturnThis(),
57
- limit: vi.fn().mockReturnThis(),
58
- single: vi.fn(() => {
59
- if (currentOperation === 'insert' || insertThenSelect) {
60
- return Promise.resolve(overrides.insertResult ?? defaultResult);
61
- }
62
- if (currentOperation === 'select') {
63
- return Promise.resolve(overrides.selectResult ?? defaultResult);
64
- }
65
- if (currentOperation === 'update') {
66
- return Promise.resolve(overrides.updateResult ?? defaultResult);
67
- }
68
- return Promise.resolve(defaultResult);
69
- }),
70
- then: vi.fn((resolve: (value: unknown) => void) => {
71
- if (currentOperation === 'insert' || insertThenSelect) {
72
- return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
73
- }
74
- if (currentOperation === 'select') {
75
- return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
76
- }
77
- if (currentOperation === 'update') {
78
- return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
79
- }
80
- if (currentOperation === 'delete') {
81
- return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
82
- }
83
- return Promise.resolve(defaultResult).then(resolve);
84
- }),
85
- };
86
-
87
- return mock as unknown as SupabaseClient;
88
- }
89
-
90
- function createMockContext(
91
- supabase: SupabaseClient,
92
- options: { sessionId?: string | null } = {}
93
- ): HandlerContext {
94
- const defaultTokenUsage: TokenUsage = {
95
- callCount: 5,
96
- totalTokens: 2500,
97
- byTool: {},
98
- };
99
-
100
- const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
101
-
102
- return {
103
- supabase,
104
- auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
105
- session: {
106
- instanceId: 'instance-abc',
107
- currentSessionId: sessionId,
108
- currentPersona: 'Wave',
109
- tokenUsage: defaultTokenUsage,
110
- },
111
- updateSession: vi.fn(),
112
- };
113
- }
10
+ import { createMockContext } from './__test-utils__.js';
11
+ import { mockApiClient } from './__test-setup__.js';
114
12
 
115
13
  const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
116
14
  const VALID_UUID_2 = '223e4567-e89b-12d3-a456-426614174001';
@@ -123,15 +21,13 @@ describe('addFinding', () => {
123
21
  beforeEach(() => vi.clearAllMocks());
124
22
 
125
23
  it('should throw error for missing project_id', async () => {
126
- const supabase = createMockSupabase();
127
- const ctx = createMockContext(supabase);
24
+ const ctx = createMockContext();
128
25
 
129
26
  await expect(addFinding({ title: 'Test Finding' }, ctx)).rejects.toThrow(ValidationError);
130
27
  });
131
28
 
132
29
  it('should throw error for invalid project_id UUID', async () => {
133
- const supabase = createMockSupabase();
134
- const ctx = createMockContext(supabase);
30
+ const ctx = createMockContext();
135
31
 
136
32
  await expect(
137
33
  addFinding({ project_id: 'invalid', title: 'Test' }, ctx)
@@ -139,8 +35,7 @@ describe('addFinding', () => {
139
35
  });
140
36
 
141
37
  it('should throw error for missing title', async () => {
142
- const supabase = createMockSupabase();
143
- const ctx = createMockContext(supabase);
38
+ const ctx = createMockContext();
144
39
 
145
40
  await expect(
146
41
  addFinding({ project_id: VALID_UUID }, ctx)
@@ -148,19 +43,19 @@ describe('addFinding', () => {
148
43
  });
149
44
 
150
45
  it('should throw error for invalid related_task_id UUID', async () => {
151
- const supabase = createMockSupabase();
152
- const ctx = createMockContext(supabase);
46
+ const ctx = createMockContext();
153
47
 
154
48
  await expect(
155
49
  addFinding({ project_id: VALID_UUID, title: 'Test', related_task_id: 'invalid' }, ctx)
156
50
  ).rejects.toThrow(ValidationError);
157
51
  });
158
52
 
159
- it('should create finding with required fields and defaults', async () => {
160
- const supabase = createMockSupabase({
161
- insertResult: { data: { id: 'finding-1' }, error: null },
53
+ it('should create finding with required fields', async () => {
54
+ mockApiClient.addFinding.mockResolvedValue({
55
+ ok: true,
56
+ data: { success: true, finding_id: 'finding-1', title: 'Performance issue' },
162
57
  });
163
- const ctx = createMockContext(supabase);
58
+ const ctx = createMockContext();
164
59
 
165
60
  const result = await addFinding(
166
61
  { project_id: VALID_UUID, title: 'Performance issue' },
@@ -172,27 +67,14 @@ describe('addFinding', () => {
172
67
  finding_id: 'finding-1',
173
68
  title: 'Performance issue',
174
69
  });
175
- expect(supabase.from).toHaveBeenCalledWith('findings');
176
- expect(supabase.insert).toHaveBeenCalledWith(
177
- expect.objectContaining({
178
- project_id: VALID_UUID,
179
- title: 'Performance issue',
180
- category: 'other',
181
- severity: 'info',
182
- description: null,
183
- file_path: null,
184
- line_number: null,
185
- related_task_id: null,
186
- created_by: 'agent',
187
- })
188
- );
189
70
  });
190
71
 
191
- it('should create finding with all optional fields', async () => {
192
- const supabase = createMockSupabase({
193
- insertResult: { data: { id: 'finding-2' }, error: null },
72
+ it('should call API client with all parameters', async () => {
73
+ mockApiClient.addFinding.mockResolvedValue({
74
+ ok: true,
75
+ data: { success: true, finding_id: 'finding-2' },
194
76
  });
195
- const ctx = createMockContext(supabase);
77
+ const ctx = createMockContext({ sessionId: 'my-session' });
196
78
 
197
79
  await addFinding(
198
80
  {
@@ -208,8 +90,9 @@ describe('addFinding', () => {
208
90
  ctx
209
91
  );
210
92
 
211
- expect(supabase.insert).toHaveBeenCalledWith(
212
- expect.objectContaining({
93
+ expect(mockApiClient.addFinding).toHaveBeenCalledWith(
94
+ VALID_UUID,
95
+ {
213
96
  title: 'SQL Injection vulnerability',
214
97
  description: 'User input not sanitized',
215
98
  category: 'security',
@@ -217,19 +100,21 @@ describe('addFinding', () => {
217
100
  file_path: 'src/api/users.ts',
218
101
  line_number: 42,
219
102
  related_task_id: VALID_UUID_2,
220
- })
103
+ },
104
+ 'my-session'
221
105
  );
222
106
  });
223
107
 
224
- it('should throw error when database insert fails', async () => {
225
- const supabase = createMockSupabase({
226
- insertResult: { data: null, error: { message: 'Insert failed' } },
108
+ it('should throw error when API call fails', async () => {
109
+ mockApiClient.addFinding.mockResolvedValue({
110
+ ok: false,
111
+ error: 'Insert failed',
227
112
  });
228
- const ctx = createMockContext(supabase);
113
+ const ctx = createMockContext();
229
114
 
230
115
  await expect(
231
116
  addFinding({ project_id: VALID_UUID, title: 'Test' }, ctx)
232
- ).rejects.toThrow('Failed to add finding');
117
+ ).rejects.toThrow('Insert failed');
233
118
  });
234
119
  });
235
120
 
@@ -241,15 +126,13 @@ describe('getFindings', () => {
241
126
  beforeEach(() => vi.clearAllMocks());
242
127
 
243
128
  it('should throw error for missing project_id', async () => {
244
- const supabase = createMockSupabase();
245
- const ctx = createMockContext(supabase);
129
+ const ctx = createMockContext();
246
130
 
247
131
  await expect(getFindings({}, ctx)).rejects.toThrow(ValidationError);
248
132
  });
249
133
 
250
134
  it('should throw error for invalid project_id UUID', async () => {
251
- const supabase = createMockSupabase();
252
- const ctx = createMockContext(supabase);
135
+ const ctx = createMockContext();
253
136
 
254
137
  await expect(
255
138
  getFindings({ project_id: 'invalid' }, ctx)
@@ -261,83 +144,130 @@ describe('getFindings', () => {
261
144
  { id: 'f1', title: 'Finding 1', category: 'security', severity: 'high', status: 'open', file_path: null, created_at: '2026-01-14' },
262
145
  { id: 'f2', title: 'Finding 2', category: 'performance', severity: 'medium', status: 'addressed', file_path: 'src/app.ts', created_at: '2026-01-13' },
263
146
  ];
264
-
265
- const supabase = createMockSupabase({
266
- selectResult: { data: mockFindings, error: null },
147
+ mockApiClient.getFindings.mockResolvedValue({
148
+ ok: true,
149
+ data: { findings: mockFindings },
267
150
  });
268
- const ctx = createMockContext(supabase);
151
+ const ctx = createMockContext();
269
152
 
270
153
  const result = await getFindings({ project_id: VALID_UUID }, ctx);
271
154
 
272
155
  expect(result.result).toMatchObject({ findings: mockFindings });
273
- expect(supabase.from).toHaveBeenCalledWith('findings');
274
- expect(supabase.eq).toHaveBeenCalledWith('project_id', VALID_UUID);
275
156
  });
276
157
 
277
- it('should filter by category when provided', async () => {
278
- const supabase = createMockSupabase({
279
- selectResult: { data: [], error: null },
158
+ it('should pass filters to API client', async () => {
159
+ mockApiClient.getFindings.mockResolvedValue({
160
+ ok: true,
161
+ data: { findings: [] },
280
162
  });
281
- const ctx = createMockContext(supabase);
282
-
283
- await getFindings({ project_id: VALID_UUID, category: 'security' }, ctx);
284
-
285
- expect(supabase.eq).toHaveBeenCalledWith('category', 'security');
163
+ const ctx = createMockContext();
164
+
165
+ await getFindings({
166
+ project_id: VALID_UUID,
167
+ category: 'security',
168
+ severity: 'critical',
169
+ status: 'open',
170
+ limit: 10
171
+ }, ctx);
172
+
173
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
174
+ VALID_UUID,
175
+ expect.objectContaining({
176
+ category: 'security',
177
+ severity: 'critical',
178
+ status: 'open',
179
+ limit: 10,
180
+ })
181
+ );
286
182
  });
287
183
 
288
- it('should filter by severity when provided', async () => {
289
- const supabase = createMockSupabase({
290
- selectResult: { data: [], error: null },
184
+ it('should pass summary_only parameter to API client', async () => {
185
+ mockApiClient.getFindings.mockResolvedValue({
186
+ ok: true,
187
+ data: { findings: [], total_count: 0, has_more: false },
291
188
  });
292
- const ctx = createMockContext(supabase);
189
+ const ctx = createMockContext();
293
190
 
294
- await getFindings({ project_id: VALID_UUID, severity: 'critical' }, ctx);
191
+ await getFindings({
192
+ project_id: VALID_UUID,
193
+ summary_only: true
194
+ }, ctx);
295
195
 
296
- expect(supabase.eq).toHaveBeenCalledWith('severity', 'critical');
196
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
197
+ VALID_UUID,
198
+ expect.objectContaining({
199
+ summary_only: true,
200
+ })
201
+ );
297
202
  });
298
203
 
299
- it('should filter by status when provided', async () => {
300
- const supabase = createMockSupabase({
301
- selectResult: { data: [], error: null },
204
+ it('should pass search_query parameter to API client', async () => {
205
+ mockApiClient.getFindings.mockResolvedValue({
206
+ ok: true,
207
+ data: { findings: [], total_count: 0, has_more: false },
302
208
  });
303
- const ctx = createMockContext(supabase);
209
+ const ctx = createMockContext();
304
210
 
305
- await getFindings({ project_id: VALID_UUID, status: 'open' }, ctx);
211
+ await getFindings({
212
+ project_id: VALID_UUID,
213
+ search_query: 'security'
214
+ }, ctx);
306
215
 
307
- expect(supabase.eq).toHaveBeenCalledWith('status', 'open');
216
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
217
+ VALID_UUID,
218
+ expect.objectContaining({
219
+ search_query: 'security',
220
+ })
221
+ );
308
222
  });
309
223
 
310
- it('should use custom limit when provided', async () => {
311
- const supabase = createMockSupabase({
312
- selectResult: { data: [], error: null },
224
+ it('should pass offset parameter to API client', async () => {
225
+ mockApiClient.getFindings.mockResolvedValue({
226
+ ok: true,
227
+ data: { findings: [], total_count: 100, has_more: true },
313
228
  });
314
- const ctx = createMockContext(supabase);
229
+ const ctx = createMockContext();
315
230
 
316
- await getFindings({ project_id: VALID_UUID, limit: 10 }, ctx);
231
+ await getFindings({
232
+ project_id: VALID_UUID,
233
+ offset: 50,
234
+ limit: 25
235
+ }, ctx);
317
236
 
318
- expect(supabase.limit).toHaveBeenCalledWith(10);
237
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
238
+ VALID_UUID,
239
+ expect.objectContaining({
240
+ offset: 50,
241
+ limit: 25,
242
+ })
243
+ );
319
244
  });
320
245
 
321
246
  it('should use default limit of 50', async () => {
322
- const supabase = createMockSupabase({
323
- selectResult: { data: [], error: null },
247
+ mockApiClient.getFindings.mockResolvedValue({
248
+ ok: true,
249
+ data: { findings: [] },
324
250
  });
325
- const ctx = createMockContext(supabase);
251
+ const ctx = createMockContext();
326
252
 
327
253
  await getFindings({ project_id: VALID_UUID }, ctx);
328
254
 
329
- expect(supabase.limit).toHaveBeenCalledWith(50);
255
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
256
+ VALID_UUID,
257
+ expect.objectContaining({ limit: 50 })
258
+ );
330
259
  });
331
260
 
332
- it('should throw error when database query fails', async () => {
333
- const supabase = createMockSupabase({
334
- selectResult: { data: null, error: { message: 'Query failed' } },
261
+ it('should throw error when API call fails', async () => {
262
+ mockApiClient.getFindings.mockResolvedValue({
263
+ ok: false,
264
+ error: 'Query failed',
335
265
  });
336
- const ctx = createMockContext(supabase);
266
+ const ctx = createMockContext();
337
267
 
338
268
  await expect(
339
269
  getFindings({ project_id: VALID_UUID }, ctx)
340
- ).rejects.toThrow('Failed to get findings');
270
+ ).rejects.toThrow('Query failed');
341
271
  });
342
272
  });
343
273
 
@@ -349,15 +279,13 @@ describe('updateFinding', () => {
349
279
  beforeEach(() => vi.clearAllMocks());
350
280
 
351
281
  it('should throw error for missing finding_id', async () => {
352
- const supabase = createMockSupabase();
353
- const ctx = createMockContext(supabase);
282
+ const ctx = createMockContext();
354
283
 
355
284
  await expect(updateFinding({}, ctx)).rejects.toThrow(ValidationError);
356
285
  });
357
286
 
358
287
  it('should throw error for invalid finding_id UUID', async () => {
359
- const supabase = createMockSupabase();
360
- const ctx = createMockContext(supabase);
288
+ const ctx = createMockContext();
361
289
 
362
290
  await expect(
363
291
  updateFinding({ finding_id: 'invalid' }, ctx)
@@ -365,10 +293,11 @@ describe('updateFinding', () => {
365
293
  });
366
294
 
367
295
  it('should update title', async () => {
368
- const supabase = createMockSupabase({
369
- updateResult: { data: null, error: null },
296
+ mockApiClient.updateFinding.mockResolvedValue({
297
+ ok: true,
298
+ data: { success: true, finding_id: VALID_UUID },
370
299
  });
371
- const ctx = createMockContext(supabase);
300
+ const ctx = createMockContext();
372
301
 
373
302
  const result = await updateFinding(
374
303
  { finding_id: VALID_UUID, title: 'Updated Title' },
@@ -376,190 +305,170 @@ describe('updateFinding', () => {
376
305
  );
377
306
 
378
307
  expect(result.result).toMatchObject({ success: true, finding_id: VALID_UUID });
379
- expect(supabase.update).toHaveBeenCalledWith(
380
- expect.objectContaining({ title: 'Updated Title' })
381
- );
382
- });
383
-
384
- it('should update severity', async () => {
385
- const supabase = createMockSupabase({
386
- updateResult: { data: null, error: null },
387
- });
388
- const ctx = createMockContext(supabase);
389
-
390
- await updateFinding(
391
- { finding_id: VALID_UUID, severity: 'high' },
392
- ctx
393
- );
394
-
395
- expect(supabase.update).toHaveBeenCalledWith(
396
- expect.objectContaining({ severity: 'high' })
397
- );
398
308
  });
399
309
 
400
- it('should set addressed_at when status changed to addressed', async () => {
401
- const supabase = createMockSupabase({
402
- updateResult: { data: null, error: null },
310
+ it('should call API client with all update fields', async () => {
311
+ mockApiClient.updateFinding.mockResolvedValue({
312
+ ok: true,
313
+ data: { success: true },
403
314
  });
404
- const ctx = createMockContext(supabase, { sessionId: 'resolver-session' });
315
+ const ctx = createMockContext();
405
316
 
406
317
  await updateFinding(
407
- { finding_id: VALID_UUID, status: 'addressed' },
318
+ {
319
+ finding_id: VALID_UUID,
320
+ title: 'New Title',
321
+ description: 'New description',
322
+ severity: 'high',
323
+ status: 'addressed',
324
+ resolution_note: 'Fixed by sanitizing input'
325
+ },
408
326
  ctx
409
327
  );
410
328
 
411
- expect(supabase.update).toHaveBeenCalledWith(
412
- expect.objectContaining({
329
+ expect(mockApiClient.updateFinding).toHaveBeenCalledWith(
330
+ VALID_UUID,
331
+ {
332
+ title: 'New Title',
333
+ description: 'New description',
334
+ severity: 'high',
413
335
  status: 'addressed',
414
- addressed_at: expect.any(String),
415
- addressed_by_session_id: 'resolver-session',
416
- })
336
+ resolution_note: 'Fixed by sanitizing input',
337
+ }
417
338
  );
418
339
  });
419
340
 
420
- it('should set addressed_at when status changed to dismissed', async () => {
421
- const supabase = createMockSupabase({
422
- updateResult: { data: null, error: null },
341
+ it('should throw error when API call fails', async () => {
342
+ mockApiClient.updateFinding.mockResolvedValue({
343
+ ok: false,
344
+ error: 'Update failed',
423
345
  });
424
- const ctx = createMockContext(supabase);
346
+ const ctx = createMockContext();
425
347
 
426
- await updateFinding(
427
- { finding_id: VALID_UUID, status: 'dismissed' },
428
- ctx
429
- );
430
-
431
- expect(supabase.update).toHaveBeenCalledWith(
432
- expect.objectContaining({
433
- status: 'dismissed',
434
- addressed_at: expect.any(String),
435
- })
436
- );
348
+ await expect(
349
+ updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx)
350
+ ).rejects.toThrow('Update failed');
437
351
  });
352
+ });
438
353
 
439
- it('should set addressed_at when status changed to wontfix', async () => {
440
- const supabase = createMockSupabase({
441
- updateResult: { data: null, error: null },
442
- });
443
- const ctx = createMockContext(supabase);
354
+ // ============================================================================
355
+ // deleteFinding Tests
356
+ // ============================================================================
444
357
 
445
- await updateFinding(
446
- { finding_id: VALID_UUID, status: 'wontfix' },
447
- ctx
448
- );
358
+ describe('deleteFinding', () => {
359
+ beforeEach(() => vi.clearAllMocks());
449
360
 
450
- expect(supabase.update).toHaveBeenCalledWith(
451
- expect.objectContaining({
452
- status: 'wontfix',
453
- addressed_at: expect.any(String),
454
- })
455
- );
456
- });
361
+ it('should throw error for missing finding_id', async () => {
362
+ const ctx = createMockContext();
457
363
 
458
- it('should not set addressed_at when status changed to open', async () => {
459
- const supabase = createMockSupabase({
460
- updateResult: { data: null, error: null },
461
- });
462
- const ctx = createMockContext(supabase);
364
+ await expect(deleteFinding({}, ctx)).rejects.toThrow(ValidationError);
365
+ });
463
366
 
464
- await updateFinding(
465
- { finding_id: VALID_UUID, status: 'open' },
466
- ctx
467
- );
367
+ it('should throw error for invalid finding_id UUID', async () => {
368
+ const ctx = createMockContext();
468
369
 
469
- const updateCall = vi.mocked(supabase.update).mock.calls[0][0] as Record<string, unknown>;
470
- expect(updateCall.status).toBe('open');
471
- expect(updateCall.addressed_at).toBeUndefined();
370
+ await expect(
371
+ deleteFinding({ finding_id: 'invalid' }, ctx)
372
+ ).rejects.toThrow(ValidationError);
472
373
  });
473
374
 
474
- it('should update resolution_note', async () => {
475
- const supabase = createMockSupabase({
476
- updateResult: { data: null, error: null },
375
+ it('should delete finding successfully', async () => {
376
+ mockApiClient.deleteFinding.mockResolvedValue({
377
+ ok: true,
378
+ data: { success: true },
477
379
  });
478
- const ctx = createMockContext(supabase);
380
+ const ctx = createMockContext();
479
381
 
480
- await updateFinding(
481
- { finding_id: VALID_UUID, resolution_note: 'Fixed by sanitizing input' },
482
- ctx
483
- );
382
+ const result = await deleteFinding({ finding_id: VALID_UUID }, ctx);
484
383
 
485
- expect(supabase.update).toHaveBeenCalledWith(
486
- expect.objectContaining({ resolution_note: 'Fixed by sanitizing input' })
487
- );
384
+ expect(result.result).toMatchObject({ success: true });
488
385
  });
489
386
 
490
- it('should always update updated_at timestamp', async () => {
491
- const supabase = createMockSupabase({
492
- updateResult: { data: null, error: null },
387
+ it('should call API client deleteFinding', async () => {
388
+ mockApiClient.deleteFinding.mockResolvedValue({
389
+ ok: true,
390
+ data: { success: true },
493
391
  });
494
- const ctx = createMockContext(supabase);
392
+ const ctx = createMockContext();
495
393
 
496
- await updateFinding(
497
- { finding_id: VALID_UUID, title: 'Test' },
498
- ctx
499
- );
394
+ await deleteFinding({ finding_id: VALID_UUID }, ctx);
500
395
 
501
- expect(supabase.update).toHaveBeenCalledWith(
502
- expect.objectContaining({ updated_at: expect.any(String) })
503
- );
396
+ expect(mockApiClient.deleteFinding).toHaveBeenCalledWith(VALID_UUID);
504
397
  });
505
398
 
506
- it('should throw error when database update fails', async () => {
507
- const supabase = createMockSupabase({
508
- updateResult: { data: null, error: { message: 'Update failed' } },
399
+ it('should throw error when API call fails', async () => {
400
+ mockApiClient.deleteFinding.mockResolvedValue({
401
+ ok: false,
402
+ error: 'Delete failed',
509
403
  });
510
- const ctx = createMockContext(supabase);
404
+ const ctx = createMockContext();
511
405
 
512
406
  await expect(
513
- updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx)
514
- ).rejects.toThrow('Failed to update finding');
407
+ deleteFinding({ finding_id: VALID_UUID }, ctx)
408
+ ).rejects.toThrow('Delete failed');
515
409
  });
516
410
  });
517
411
 
518
412
  // ============================================================================
519
- // deleteFinding Tests
413
+ // getFindingsStats Tests
520
414
  // ============================================================================
521
415
 
522
- describe('deleteFinding', () => {
416
+ describe('getFindingsStats', () => {
523
417
  beforeEach(() => vi.clearAllMocks());
524
418
 
525
- it('should throw error for missing finding_id', async () => {
526
- const supabase = createMockSupabase();
527
- const ctx = createMockContext(supabase);
419
+ it('should throw error for missing project_id', async () => {
420
+ const ctx = createMockContext();
528
421
 
529
- await expect(deleteFinding({}, ctx)).rejects.toThrow(ValidationError);
422
+ await expect(getFindingsStats({}, ctx)).rejects.toThrow(ValidationError);
530
423
  });
531
424
 
532
- it('should throw error for invalid finding_id UUID', async () => {
533
- const supabase = createMockSupabase();
534
- const ctx = createMockContext(supabase);
425
+ it('should throw error for invalid project_id UUID', async () => {
426
+ const ctx = createMockContext();
535
427
 
536
428
  await expect(
537
- deleteFinding({ finding_id: 'invalid' }, ctx)
429
+ getFindingsStats({ project_id: 'invalid' }, ctx)
538
430
  ).rejects.toThrow(ValidationError);
539
431
  });
540
432
 
541
- it('should delete finding successfully', async () => {
542
- const supabase = createMockSupabase({
543
- deleteResult: { data: null, error: null },
433
+ it('should return findings stats for project', async () => {
434
+ const mockStats = {
435
+ total: 10,
436
+ by_status: { open: 5, addressed: 3, dismissed: 2 },
437
+ by_severity: { critical: 1, high: 3, medium: 4, low: 2 },
438
+ by_category: { security: 3, performance: 4, code_quality: 3 },
439
+ };
440
+ mockApiClient.getFindingsStats.mockResolvedValue({
441
+ ok: true,
442
+ data: mockStats,
544
443
  });
545
- const ctx = createMockContext(supabase);
444
+ const ctx = createMockContext();
546
445
 
547
- const result = await deleteFinding({ finding_id: VALID_UUID }, ctx);
446
+ const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
548
447
 
549
- expect(result.result).toMatchObject({ success: true });
550
- expect(supabase.from).toHaveBeenCalledWith('findings');
551
- expect(supabase.delete).toHaveBeenCalled();
552
- expect(supabase.eq).toHaveBeenCalledWith('id', VALID_UUID);
448
+ expect(result.result).toMatchObject(mockStats);
553
449
  });
554
450
 
555
- it('should throw error when database delete fails', async () => {
556
- const supabase = createMockSupabase({
557
- deleteResult: { data: null, error: { message: 'Delete failed' } },
451
+ it('should call API client getFindingsStats with project_id', async () => {
452
+ mockApiClient.getFindingsStats.mockResolvedValue({
453
+ ok: true,
454
+ data: { total: 0, by_status: {}, by_severity: {}, by_category: {} },
558
455
  });
559
- const ctx = createMockContext(supabase);
456
+ const ctx = createMockContext();
457
+
458
+ await getFindingsStats({ project_id: VALID_UUID }, ctx);
459
+
460
+ expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
461
+ });
462
+
463
+ it('should throw error when API call fails', async () => {
464
+ mockApiClient.getFindingsStats.mockResolvedValue({
465
+ ok: false,
466
+ error: 'Query failed',
467
+ });
468
+ const ctx = createMockContext();
560
469
 
561
470
  await expect(
562
- deleteFinding({ finding_id: VALID_UUID }, ctx)
563
- ).rejects.toThrow('Failed to delete finding');
471
+ getFindingsStats({ project_id: VALID_UUID }, ctx)
472
+ ).rejects.toThrow('Query failed');
564
473
  });
565
474
  });