@vibescope/mcp-server 0.0.1

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 (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1,565 @@
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
+ import {
5
+ addFinding,
6
+ getFindings,
7
+ updateFinding,
8
+ deleteFinding,
9
+ } from './findings.js';
10
+ 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
+ }
114
+
115
+ const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
116
+ const VALID_UUID_2 = '223e4567-e89b-12d3-a456-426614174001';
117
+
118
+ // ============================================================================
119
+ // addFinding Tests
120
+ // ============================================================================
121
+
122
+ describe('addFinding', () => {
123
+ beforeEach(() => vi.clearAllMocks());
124
+
125
+ it('should throw error for missing project_id', async () => {
126
+ const supabase = createMockSupabase();
127
+ const ctx = createMockContext(supabase);
128
+
129
+ await expect(addFinding({ title: 'Test Finding' }, ctx)).rejects.toThrow(ValidationError);
130
+ });
131
+
132
+ it('should throw error for invalid project_id UUID', async () => {
133
+ const supabase = createMockSupabase();
134
+ const ctx = createMockContext(supabase);
135
+
136
+ await expect(
137
+ addFinding({ project_id: 'invalid', title: 'Test' }, ctx)
138
+ ).rejects.toThrow(ValidationError);
139
+ });
140
+
141
+ it('should throw error for missing title', async () => {
142
+ const supabase = createMockSupabase();
143
+ const ctx = createMockContext(supabase);
144
+
145
+ await expect(
146
+ addFinding({ project_id: VALID_UUID }, ctx)
147
+ ).rejects.toThrow(ValidationError);
148
+ });
149
+
150
+ it('should throw error for invalid related_task_id UUID', async () => {
151
+ const supabase = createMockSupabase();
152
+ const ctx = createMockContext(supabase);
153
+
154
+ await expect(
155
+ addFinding({ project_id: VALID_UUID, title: 'Test', related_task_id: 'invalid' }, ctx)
156
+ ).rejects.toThrow(ValidationError);
157
+ });
158
+
159
+ it('should create finding with required fields and defaults', async () => {
160
+ const supabase = createMockSupabase({
161
+ insertResult: { data: { id: 'finding-1' }, error: null },
162
+ });
163
+ const ctx = createMockContext(supabase);
164
+
165
+ const result = await addFinding(
166
+ { project_id: VALID_UUID, title: 'Performance issue' },
167
+ ctx
168
+ );
169
+
170
+ expect(result.result).toMatchObject({
171
+ success: true,
172
+ finding_id: 'finding-1',
173
+ title: 'Performance issue',
174
+ });
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
+ });
190
+
191
+ it('should create finding with all optional fields', async () => {
192
+ const supabase = createMockSupabase({
193
+ insertResult: { data: { id: 'finding-2' }, error: null },
194
+ });
195
+ const ctx = createMockContext(supabase);
196
+
197
+ await addFinding(
198
+ {
199
+ project_id: VALID_UUID,
200
+ title: 'SQL Injection vulnerability',
201
+ description: 'User input not sanitized',
202
+ category: 'security',
203
+ severity: 'critical',
204
+ file_path: 'src/api/users.ts',
205
+ line_number: 42,
206
+ related_task_id: VALID_UUID_2,
207
+ },
208
+ ctx
209
+ );
210
+
211
+ expect(supabase.insert).toHaveBeenCalledWith(
212
+ expect.objectContaining({
213
+ title: 'SQL Injection vulnerability',
214
+ description: 'User input not sanitized',
215
+ category: 'security',
216
+ severity: 'critical',
217
+ file_path: 'src/api/users.ts',
218
+ line_number: 42,
219
+ related_task_id: VALID_UUID_2,
220
+ })
221
+ );
222
+ });
223
+
224
+ it('should throw error when database insert fails', async () => {
225
+ const supabase = createMockSupabase({
226
+ insertResult: { data: null, error: { message: 'Insert failed' } },
227
+ });
228
+ const ctx = createMockContext(supabase);
229
+
230
+ await expect(
231
+ addFinding({ project_id: VALID_UUID, title: 'Test' }, ctx)
232
+ ).rejects.toThrow('Failed to add finding');
233
+ });
234
+ });
235
+
236
+ // ============================================================================
237
+ // getFindings Tests
238
+ // ============================================================================
239
+
240
+ describe('getFindings', () => {
241
+ beforeEach(() => vi.clearAllMocks());
242
+
243
+ it('should throw error for missing project_id', async () => {
244
+ const supabase = createMockSupabase();
245
+ const ctx = createMockContext(supabase);
246
+
247
+ await expect(getFindings({}, ctx)).rejects.toThrow(ValidationError);
248
+ });
249
+
250
+ it('should throw error for invalid project_id UUID', async () => {
251
+ const supabase = createMockSupabase();
252
+ const ctx = createMockContext(supabase);
253
+
254
+ await expect(
255
+ getFindings({ project_id: 'invalid' }, ctx)
256
+ ).rejects.toThrow(ValidationError);
257
+ });
258
+
259
+ it('should return findings for project', async () => {
260
+ const mockFindings = [
261
+ { id: 'f1', title: 'Finding 1', category: 'security', severity: 'high', status: 'open', file_path: null, created_at: '2026-01-14' },
262
+ { id: 'f2', title: 'Finding 2', category: 'performance', severity: 'medium', status: 'addressed', file_path: 'src/app.ts', created_at: '2026-01-13' },
263
+ ];
264
+
265
+ const supabase = createMockSupabase({
266
+ selectResult: { data: mockFindings, error: null },
267
+ });
268
+ const ctx = createMockContext(supabase);
269
+
270
+ const result = await getFindings({ project_id: VALID_UUID }, ctx);
271
+
272
+ expect(result.result).toMatchObject({ findings: mockFindings });
273
+ expect(supabase.from).toHaveBeenCalledWith('findings');
274
+ expect(supabase.eq).toHaveBeenCalledWith('project_id', VALID_UUID);
275
+ });
276
+
277
+ it('should filter by category when provided', async () => {
278
+ const supabase = createMockSupabase({
279
+ selectResult: { data: [], error: null },
280
+ });
281
+ const ctx = createMockContext(supabase);
282
+
283
+ await getFindings({ project_id: VALID_UUID, category: 'security' }, ctx);
284
+
285
+ expect(supabase.eq).toHaveBeenCalledWith('category', 'security');
286
+ });
287
+
288
+ it('should filter by severity when provided', async () => {
289
+ const supabase = createMockSupabase({
290
+ selectResult: { data: [], error: null },
291
+ });
292
+ const ctx = createMockContext(supabase);
293
+
294
+ await getFindings({ project_id: VALID_UUID, severity: 'critical' }, ctx);
295
+
296
+ expect(supabase.eq).toHaveBeenCalledWith('severity', 'critical');
297
+ });
298
+
299
+ it('should filter by status when provided', async () => {
300
+ const supabase = createMockSupabase({
301
+ selectResult: { data: [], error: null },
302
+ });
303
+ const ctx = createMockContext(supabase);
304
+
305
+ await getFindings({ project_id: VALID_UUID, status: 'open' }, ctx);
306
+
307
+ expect(supabase.eq).toHaveBeenCalledWith('status', 'open');
308
+ });
309
+
310
+ it('should use custom limit when provided', async () => {
311
+ const supabase = createMockSupabase({
312
+ selectResult: { data: [], error: null },
313
+ });
314
+ const ctx = createMockContext(supabase);
315
+
316
+ await getFindings({ project_id: VALID_UUID, limit: 10 }, ctx);
317
+
318
+ expect(supabase.limit).toHaveBeenCalledWith(10);
319
+ });
320
+
321
+ it('should use default limit of 50', async () => {
322
+ const supabase = createMockSupabase({
323
+ selectResult: { data: [], error: null },
324
+ });
325
+ const ctx = createMockContext(supabase);
326
+
327
+ await getFindings({ project_id: VALID_UUID }, ctx);
328
+
329
+ expect(supabase.limit).toHaveBeenCalledWith(50);
330
+ });
331
+
332
+ it('should throw error when database query fails', async () => {
333
+ const supabase = createMockSupabase({
334
+ selectResult: { data: null, error: { message: 'Query failed' } },
335
+ });
336
+ const ctx = createMockContext(supabase);
337
+
338
+ await expect(
339
+ getFindings({ project_id: VALID_UUID }, ctx)
340
+ ).rejects.toThrow('Failed to get findings');
341
+ });
342
+ });
343
+
344
+ // ============================================================================
345
+ // updateFinding Tests
346
+ // ============================================================================
347
+
348
+ describe('updateFinding', () => {
349
+ beforeEach(() => vi.clearAllMocks());
350
+
351
+ it('should throw error for missing finding_id', async () => {
352
+ const supabase = createMockSupabase();
353
+ const ctx = createMockContext(supabase);
354
+
355
+ await expect(updateFinding({}, ctx)).rejects.toThrow(ValidationError);
356
+ });
357
+
358
+ it('should throw error for invalid finding_id UUID', async () => {
359
+ const supabase = createMockSupabase();
360
+ const ctx = createMockContext(supabase);
361
+
362
+ await expect(
363
+ updateFinding({ finding_id: 'invalid' }, ctx)
364
+ ).rejects.toThrow(ValidationError);
365
+ });
366
+
367
+ it('should update title', async () => {
368
+ const supabase = createMockSupabase({
369
+ updateResult: { data: null, error: null },
370
+ });
371
+ const ctx = createMockContext(supabase);
372
+
373
+ const result = await updateFinding(
374
+ { finding_id: VALID_UUID, title: 'Updated Title' },
375
+ ctx
376
+ );
377
+
378
+ 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
+ });
399
+
400
+ it('should set addressed_at when status changed to addressed', async () => {
401
+ const supabase = createMockSupabase({
402
+ updateResult: { data: null, error: null },
403
+ });
404
+ const ctx = createMockContext(supabase, { sessionId: 'resolver-session' });
405
+
406
+ await updateFinding(
407
+ { finding_id: VALID_UUID, status: 'addressed' },
408
+ ctx
409
+ );
410
+
411
+ expect(supabase.update).toHaveBeenCalledWith(
412
+ expect.objectContaining({
413
+ status: 'addressed',
414
+ addressed_at: expect.any(String),
415
+ addressed_by_session_id: 'resolver-session',
416
+ })
417
+ );
418
+ });
419
+
420
+ it('should set addressed_at when status changed to dismissed', async () => {
421
+ const supabase = createMockSupabase({
422
+ updateResult: { data: null, error: null },
423
+ });
424
+ const ctx = createMockContext(supabase);
425
+
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
+ );
437
+ });
438
+
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);
444
+
445
+ await updateFinding(
446
+ { finding_id: VALID_UUID, status: 'wontfix' },
447
+ ctx
448
+ );
449
+
450
+ expect(supabase.update).toHaveBeenCalledWith(
451
+ expect.objectContaining({
452
+ status: 'wontfix',
453
+ addressed_at: expect.any(String),
454
+ })
455
+ );
456
+ });
457
+
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);
463
+
464
+ await updateFinding(
465
+ { finding_id: VALID_UUID, status: 'open' },
466
+ ctx
467
+ );
468
+
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();
472
+ });
473
+
474
+ it('should update resolution_note', async () => {
475
+ const supabase = createMockSupabase({
476
+ updateResult: { data: null, error: null },
477
+ });
478
+ const ctx = createMockContext(supabase);
479
+
480
+ await updateFinding(
481
+ { finding_id: VALID_UUID, resolution_note: 'Fixed by sanitizing input' },
482
+ ctx
483
+ );
484
+
485
+ expect(supabase.update).toHaveBeenCalledWith(
486
+ expect.objectContaining({ resolution_note: 'Fixed by sanitizing input' })
487
+ );
488
+ });
489
+
490
+ it('should always update updated_at timestamp', async () => {
491
+ const supabase = createMockSupabase({
492
+ updateResult: { data: null, error: null },
493
+ });
494
+ const ctx = createMockContext(supabase);
495
+
496
+ await updateFinding(
497
+ { finding_id: VALID_UUID, title: 'Test' },
498
+ ctx
499
+ );
500
+
501
+ expect(supabase.update).toHaveBeenCalledWith(
502
+ expect.objectContaining({ updated_at: expect.any(String) })
503
+ );
504
+ });
505
+
506
+ it('should throw error when database update fails', async () => {
507
+ const supabase = createMockSupabase({
508
+ updateResult: { data: null, error: { message: 'Update failed' } },
509
+ });
510
+ const ctx = createMockContext(supabase);
511
+
512
+ await expect(
513
+ updateFinding({ finding_id: VALID_UUID, title: 'Test' }, ctx)
514
+ ).rejects.toThrow('Failed to update finding');
515
+ });
516
+ });
517
+
518
+ // ============================================================================
519
+ // deleteFinding Tests
520
+ // ============================================================================
521
+
522
+ describe('deleteFinding', () => {
523
+ beforeEach(() => vi.clearAllMocks());
524
+
525
+ it('should throw error for missing finding_id', async () => {
526
+ const supabase = createMockSupabase();
527
+ const ctx = createMockContext(supabase);
528
+
529
+ await expect(deleteFinding({}, ctx)).rejects.toThrow(ValidationError);
530
+ });
531
+
532
+ it('should throw error for invalid finding_id UUID', async () => {
533
+ const supabase = createMockSupabase();
534
+ const ctx = createMockContext(supabase);
535
+
536
+ await expect(
537
+ deleteFinding({ finding_id: 'invalid' }, ctx)
538
+ ).rejects.toThrow(ValidationError);
539
+ });
540
+
541
+ it('should delete finding successfully', async () => {
542
+ const supabase = createMockSupabase({
543
+ deleteResult: { data: null, error: null },
544
+ });
545
+ const ctx = createMockContext(supabase);
546
+
547
+ const result = await deleteFinding({ finding_id: VALID_UUID }, ctx);
548
+
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);
553
+ });
554
+
555
+ it('should throw error when database delete fails', async () => {
556
+ const supabase = createMockSupabase({
557
+ deleteResult: { data: null, error: { message: 'Delete failed' } },
558
+ });
559
+ const ctx = createMockContext(supabase);
560
+
561
+ await expect(
562
+ deleteFinding({ finding_id: VALID_UUID }, ctx)
563
+ ).rejects.toThrow('Failed to delete finding');
564
+ });
565
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Findings Handlers
3
+ *
4
+ * Handles audit findings and knowledge base:
5
+ * - add_finding
6
+ * - get_findings
7
+ * - update_finding
8
+ * - delete_finding
9
+ */
10
+
11
+ import type { Handler, HandlerRegistry } from './types.js';
12
+ import { validateRequired, validateUUID } from '../validators.js';
13
+
14
+ type FindingCategory = 'performance' | 'security' | 'code_quality' | 'accessibility' | 'documentation' | 'architecture' | 'testing' | 'other';
15
+ type FindingSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
16
+ type FindingStatus = 'open' | 'addressed' | 'dismissed' | 'wontfix';
17
+
18
+ export const addFinding: Handler = async (args, ctx) => {
19
+ const { project_id, category, title, description, severity, file_path, line_number, related_task_id } = args as {
20
+ project_id: string;
21
+ category?: FindingCategory;
22
+ title: string;
23
+ description?: string;
24
+ severity?: FindingSeverity;
25
+ file_path?: string;
26
+ line_number?: number;
27
+ related_task_id?: string;
28
+ };
29
+
30
+ validateRequired(project_id, 'project_id');
31
+ validateUUID(project_id, 'project_id');
32
+ validateRequired(title, 'title');
33
+ if (related_task_id) validateUUID(related_task_id, 'related_task_id');
34
+
35
+ const { supabase, session } = ctx;
36
+
37
+ const { data, error } = await supabase
38
+ .from('findings')
39
+ .insert({
40
+ project_id,
41
+ category: category || 'other',
42
+ title,
43
+ description: description || null,
44
+ severity: severity || 'info',
45
+ file_path: file_path || null,
46
+ line_number: line_number || null,
47
+ related_task_id: related_task_id || null,
48
+ created_by: 'agent',
49
+ created_by_session_id: session.currentSessionId,
50
+ })
51
+ .select('id')
52
+ .single();
53
+
54
+ if (error) throw new Error(`Failed to add finding: ${error.message}`);
55
+
56
+ return { result: { success: true, finding_id: data.id, title } };
57
+ };
58
+
59
+ export const getFindings: Handler = async (args, ctx) => {
60
+ const { project_id, category, severity, status, limit } = args as {
61
+ project_id: string;
62
+ category?: FindingCategory;
63
+ severity?: FindingSeverity;
64
+ status?: FindingStatus;
65
+ limit?: number;
66
+ };
67
+
68
+ validateRequired(project_id, 'project_id');
69
+ validateUUID(project_id, 'project_id');
70
+
71
+ const { supabase } = ctx;
72
+
73
+ let query = supabase
74
+ .from('findings')
75
+ .select('id, title, category, severity, status, file_path, created_at')
76
+ .eq('project_id', project_id)
77
+ .order('created_at', { ascending: false })
78
+ .limit(limit || 50);
79
+
80
+ if (category) query = query.eq('category', category);
81
+ if (severity) query = query.eq('severity', severity);
82
+ if (status) query = query.eq('status', status);
83
+
84
+ const { data, error } = await query;
85
+
86
+ if (error) throw new Error(`Failed to get findings: ${error.message}`);
87
+
88
+ return { result: { findings: data } };
89
+ };
90
+
91
+ export const updateFinding: Handler = async (args, ctx) => {
92
+ const { finding_id, status, resolution_note, title, description, severity } = args as {
93
+ finding_id: string;
94
+ status?: FindingStatus;
95
+ resolution_note?: string;
96
+ title?: string;
97
+ description?: string;
98
+ severity?: FindingSeverity;
99
+ };
100
+
101
+ validateRequired(finding_id, 'finding_id');
102
+ validateUUID(finding_id, 'finding_id');
103
+
104
+ const { supabase, session } = ctx;
105
+
106
+ const updates: Record<string, unknown> = { updated_at: new Date().toISOString() };
107
+ if (title) updates.title = title;
108
+ if (description) updates.description = description;
109
+ if (severity) updates.severity = severity;
110
+ if (status) {
111
+ updates.status = status;
112
+ if (status === 'addressed' || status === 'dismissed' || status === 'wontfix') {
113
+ updates.addressed_at = new Date().toISOString();
114
+ updates.addressed_by_session_id = session.currentSessionId;
115
+ }
116
+ }
117
+ if (resolution_note) updates.resolution_note = resolution_note;
118
+
119
+ const { error } = await supabase
120
+ .from('findings')
121
+ .update(updates)
122
+ .eq('id', finding_id);
123
+
124
+ if (error) throw new Error(`Failed to update finding: ${error.message}`);
125
+
126
+ return { result: { success: true, finding_id } };
127
+ };
128
+
129
+ export const deleteFinding: Handler = async (args, ctx) => {
130
+ const { finding_id } = args as { finding_id: string };
131
+
132
+ validateRequired(finding_id, 'finding_id');
133
+ validateUUID(finding_id, 'finding_id');
134
+
135
+ const { error } = await ctx.supabase
136
+ .from('findings')
137
+ .delete()
138
+ .eq('id', finding_id);
139
+
140
+ if (error) throw new Error(`Failed to delete finding: ${error.message}`);
141
+
142
+ return { result: { success: true } };
143
+ };
144
+
145
+ /**
146
+ * Findings handlers registry
147
+ */
148
+ export const findingHandlers: HandlerRegistry = {
149
+ add_finding: addFinding,
150
+ get_findings: getFindings,
151
+ update_finding: updateFinding,
152
+ delete_finding: deleteFinding,
153
+ };