@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.
- package/README.md +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- 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
|
+
};
|