@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,880 @@
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
+ getTasksAwaitingValidation,
6
+ claimValidation,
7
+ validateTask,
8
+ } from './validation.js';
9
+ import { ValidationError } from '../validators.js';
10
+
11
+ // ============================================================================
12
+ // Test Utilities
13
+ // ============================================================================
14
+
15
+ function createMockSupabase(overrides: {
16
+ selectResult?: { data: unknown; error: unknown };
17
+ insertResult?: { data: unknown; error: unknown };
18
+ updateResult?: { data: unknown; error: unknown };
19
+ } = {}) {
20
+ const defaultResult = { data: null, error: null };
21
+ let currentOperation = 'select';
22
+ let insertThenSelect = false;
23
+
24
+ const mock = {
25
+ from: vi.fn().mockReturnThis(),
26
+ select: vi.fn(() => {
27
+ if (currentOperation === 'insert') {
28
+ insertThenSelect = true;
29
+ } else {
30
+ currentOperation = 'select';
31
+ insertThenSelect = false;
32
+ }
33
+ return mock;
34
+ }),
35
+ insert: vi.fn(() => {
36
+ currentOperation = 'insert';
37
+ insertThenSelect = false;
38
+ return mock;
39
+ }),
40
+ update: vi.fn(() => {
41
+ currentOperation = 'update';
42
+ insertThenSelect = false;
43
+ return mock;
44
+ }),
45
+ delete: vi.fn(() => {
46
+ currentOperation = 'delete';
47
+ insertThenSelect = false;
48
+ return mock;
49
+ }),
50
+ eq: vi.fn().mockReturnThis(),
51
+ neq: vi.fn().mockReturnThis(),
52
+ in: vi.fn().mockReturnThis(),
53
+ is: vi.fn().mockReturnThis(),
54
+ not: vi.fn().mockReturnThis(),
55
+ or: vi.fn().mockReturnThis(),
56
+ gte: vi.fn().mockReturnThis(),
57
+ lte: vi.fn().mockReturnThis(),
58
+ lt: vi.fn().mockReturnThis(),
59
+ order: vi.fn().mockReturnThis(),
60
+ limit: vi.fn().mockReturnThis(),
61
+ single: vi.fn(() => {
62
+ if (currentOperation === 'insert' || insertThenSelect) {
63
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
64
+ }
65
+ if (currentOperation === 'select') {
66
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
67
+ }
68
+ if (currentOperation === 'update') {
69
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
70
+ }
71
+ return Promise.resolve(defaultResult);
72
+ }),
73
+ maybeSingle: vi.fn(() => {
74
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
75
+ }),
76
+ then: vi.fn((resolve: (value: unknown) => void) => {
77
+ if (currentOperation === 'insert' || insertThenSelect) {
78
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
79
+ }
80
+ if (currentOperation === 'select') {
81
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
82
+ }
83
+ if (currentOperation === 'update') {
84
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
85
+ }
86
+ return Promise.resolve(defaultResult).then(resolve);
87
+ }),
88
+ };
89
+
90
+ return mock as unknown as SupabaseClient;
91
+ }
92
+
93
+ function createMockContext(
94
+ supabase: SupabaseClient,
95
+ options: { sessionId?: string | null } = {}
96
+ ): HandlerContext {
97
+ const defaultTokenUsage: TokenUsage = {
98
+ callCount: 5,
99
+ totalTokens: 2500,
100
+ byTool: {},
101
+ };
102
+
103
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
104
+
105
+ return {
106
+ supabase,
107
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
108
+ session: {
109
+ instanceId: 'instance-abc',
110
+ currentSessionId: sessionId,
111
+ currentPersona: 'Wave',
112
+ tokenUsage: defaultTokenUsage,
113
+ },
114
+ updateSession: vi.fn(),
115
+ };
116
+ }
117
+
118
+ // ============================================================================
119
+ // getTasksAwaitingValidation Tests
120
+ // ============================================================================
121
+
122
+ describe('getTasksAwaitingValidation', () => {
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(getTasksAwaitingValidation({}, 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
+ getTasksAwaitingValidation({ project_id: 'invalid' }, ctx)
138
+ ).rejects.toThrow(ValidationError);
139
+ });
140
+
141
+ it('should return empty list when no tasks awaiting validation', async () => {
142
+ const supabase = createMockSupabase({
143
+ selectResult: { data: [], error: null },
144
+ });
145
+ const ctx = createMockContext(supabase);
146
+
147
+ // Override to return array result
148
+ vi.mocked(supabase.from('').select).mockReturnValue({
149
+ ...supabase,
150
+ then: (resolve: (val: unknown) => void) =>
151
+ Promise.resolve({ data: [], error: null }).then(resolve),
152
+ } as unknown as ReturnType<SupabaseClient['from']>);
153
+
154
+ const result = await getTasksAwaitingValidation(
155
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
156
+ ctx
157
+ );
158
+
159
+ expect(result.result).toHaveProperty('tasks');
160
+ expect((result.result as { tasks: unknown[] }).tasks).toEqual([]);
161
+ expect((result.result as { hint?: string }).hint).toBeUndefined();
162
+ });
163
+
164
+ it('should return tasks with review status info', async () => {
165
+ const mockTasks = [
166
+ {
167
+ id: 'task-1',
168
+ title: 'Implement feature A',
169
+ completed_at: '2025-01-14T10:00:00Z',
170
+ completed_by_session_id: 'other-session',
171
+ reviewing_by_session_id: null,
172
+ reviewing_started_at: null,
173
+ },
174
+ {
175
+ id: 'task-2',
176
+ title: 'Implement feature B',
177
+ completed_at: '2025-01-14T11:00:00Z',
178
+ completed_by_session_id: 'other-session',
179
+ reviewing_by_session_id: 'reviewer-session',
180
+ reviewing_started_at: new Date(Date.now() - 5 * 60000).toISOString(), // 5 minutes ago
181
+ },
182
+ ];
183
+
184
+ const supabase = createMockSupabase({
185
+ selectResult: { data: mockTasks, error: null },
186
+ });
187
+ const ctx = createMockContext(supabase);
188
+
189
+ // Override to return array result
190
+ vi.mocked(supabase.from('').select).mockReturnValue({
191
+ ...supabase,
192
+ then: (resolve: (val: unknown) => void) =>
193
+ Promise.resolve({ data: mockTasks, error: null }).then(resolve),
194
+ } as unknown as ReturnType<SupabaseClient['from']>);
195
+
196
+ const result = await getTasksAwaitingValidation(
197
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
198
+ ctx
199
+ );
200
+
201
+ const tasks = (result.result as { tasks: Array<{ being_reviewed: boolean; review_minutes: number | null }> }).tasks;
202
+ expect(tasks).toHaveLength(2);
203
+
204
+ // First task not being reviewed
205
+ expect(tasks[0].being_reviewed).toBe(false);
206
+ expect(tasks[0].review_minutes).toBeNull();
207
+
208
+ // Second task being reviewed for ~5 minutes
209
+ expect(tasks[1].being_reviewed).toBe(true);
210
+ expect(tasks[1].review_minutes).toBeGreaterThanOrEqual(4);
211
+ expect(tasks[1].review_minutes).toBeLessThanOrEqual(6);
212
+
213
+ // Should have hint when tasks are present
214
+ expect((result.result as { hint?: string }).hint).toContain('claim_validation');
215
+ });
216
+
217
+ it('should query correct table with filters', async () => {
218
+ const supabase = createMockSupabase({
219
+ selectResult: { data: [], error: null },
220
+ });
221
+ const ctx = createMockContext(supabase);
222
+
223
+ await getTasksAwaitingValidation(
224
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
225
+ ctx
226
+ );
227
+
228
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
229
+ expect(supabase.eq).toHaveBeenCalledWith('project_id', '123e4567-e89b-12d3-a456-426614174000');
230
+ expect(supabase.eq).toHaveBeenCalledWith('status', 'completed');
231
+ expect(supabase.is).toHaveBeenCalledWith('validated_at', null);
232
+ });
233
+
234
+ it('should throw error when database query fails', async () => {
235
+ const supabase = createMockSupabase({
236
+ selectResult: { data: null, error: { message: 'Database error' } },
237
+ });
238
+ const ctx = createMockContext(supabase);
239
+
240
+ // Override to return error
241
+ vi.mocked(supabase.from('').select).mockReturnValue({
242
+ ...supabase,
243
+ then: (resolve: (val: unknown) => void) =>
244
+ Promise.resolve({ data: null, error: { message: 'Database error' } }).then(resolve),
245
+ } as unknown as ReturnType<SupabaseClient['from']>);
246
+
247
+ await expect(
248
+ getTasksAwaitingValidation({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
249
+ ).rejects.toThrow('Failed to fetch tasks');
250
+ });
251
+ });
252
+
253
+ // ============================================================================
254
+ // claimValidation Tests
255
+ // ============================================================================
256
+
257
+ describe('claimValidation', () => {
258
+ beforeEach(() => vi.clearAllMocks());
259
+
260
+ it('should throw error for missing task_id', async () => {
261
+ const supabase = createMockSupabase();
262
+ const ctx = createMockContext(supabase);
263
+
264
+ await expect(claimValidation({}, ctx)).rejects.toThrow(ValidationError);
265
+ });
266
+
267
+ it('should throw error for invalid task_id UUID', async () => {
268
+ const supabase = createMockSupabase();
269
+ const ctx = createMockContext(supabase);
270
+
271
+ await expect(
272
+ claimValidation({ task_id: 'not-a-uuid' }, ctx)
273
+ ).rejects.toThrow(ValidationError);
274
+ });
275
+
276
+ it('should throw error when task not found', async () => {
277
+ const supabase = createMockSupabase({
278
+ selectResult: { data: null, error: { message: 'Not found' } },
279
+ });
280
+ const ctx = createMockContext(supabase);
281
+
282
+ await expect(
283
+ claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
284
+ ).rejects.toThrow('Task not found');
285
+ });
286
+
287
+ it('should throw error when task is not completed', async () => {
288
+ const supabase = createMockSupabase({
289
+ selectResult: {
290
+ data: {
291
+ id: 'task-1',
292
+ title: 'Test Task',
293
+ status: 'in_progress',
294
+ validated_at: null,
295
+ completed_by_session_id: 'other-session',
296
+ reviewing_by_session_id: null,
297
+ },
298
+ error: null,
299
+ },
300
+ });
301
+ const ctx = createMockContext(supabase);
302
+
303
+ await expect(
304
+ claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
305
+ ).rejects.toThrow('Can only claim completed tasks for review');
306
+ });
307
+
308
+ it('should throw error when task is already validated', async () => {
309
+ const supabase = createMockSupabase({
310
+ selectResult: {
311
+ data: {
312
+ id: 'task-1',
313
+ title: 'Test Task',
314
+ status: 'completed',
315
+ validated_at: '2025-01-14T12:00:00Z',
316
+ completed_by_session_id: 'other-session',
317
+ reviewing_by_session_id: null,
318
+ },
319
+ error: null,
320
+ },
321
+ });
322
+ const ctx = createMockContext(supabase);
323
+
324
+ await expect(
325
+ claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
326
+ ).rejects.toThrow('Task has already been validated');
327
+ });
328
+
329
+ it('should throw error when task is being reviewed by another agent', async () => {
330
+ const supabase = createMockSupabase({
331
+ selectResult: {
332
+ data: {
333
+ id: 'task-1',
334
+ title: 'Test Task',
335
+ status: 'completed',
336
+ validated_at: null,
337
+ completed_by_session_id: 'other-session',
338
+ reviewing_by_session_id: 'different-reviewer',
339
+ },
340
+ error: null,
341
+ },
342
+ });
343
+ const ctx = createMockContext(supabase, { sessionId: 'session-123' });
344
+
345
+ await expect(
346
+ claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
347
+ ).rejects.toThrow('Task is already being reviewed by another agent');
348
+ });
349
+
350
+ it('should allow same agent to re-claim their own review', async () => {
351
+ const supabase = createMockSupabase({
352
+ selectResult: {
353
+ data: {
354
+ id: 'task-1',
355
+ title: 'Test Task',
356
+ status: 'completed',
357
+ validated_at: null,
358
+ completed_by_session_id: 'other-session',
359
+ reviewing_by_session_id: 'session-123', // Same as current session
360
+ },
361
+ error: null,
362
+ },
363
+ updateResult: { data: null, error: null },
364
+ });
365
+ const ctx = createMockContext(supabase, { sessionId: 'session-123' });
366
+
367
+ const result = await claimValidation(
368
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
369
+ ctx
370
+ );
371
+
372
+ expect(result.result).toMatchObject({
373
+ success: true,
374
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
375
+ title: 'Test Task',
376
+ });
377
+ });
378
+
379
+ it('should claim task successfully', async () => {
380
+ const supabase = createMockSupabase({
381
+ selectResult: {
382
+ data: {
383
+ id: 'task-1',
384
+ title: 'Test Task',
385
+ status: 'completed',
386
+ validated_at: null,
387
+ completed_by_session_id: 'other-session',
388
+ reviewing_by_session_id: null,
389
+ },
390
+ error: null,
391
+ },
392
+ updateResult: { data: null, error: null },
393
+ });
394
+ const ctx = createMockContext(supabase);
395
+
396
+ const result = await claimValidation(
397
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
398
+ ctx
399
+ );
400
+
401
+ expect(result.result).toMatchObject({
402
+ success: true,
403
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
404
+ title: 'Test Task',
405
+ message: expect.stringContaining('Dashboard'),
406
+ });
407
+ expect((result.result as { next_step: string }).next_step).toContain('validate_task');
408
+ });
409
+
410
+ it('should update task with reviewing session info', async () => {
411
+ const supabase = createMockSupabase({
412
+ selectResult: {
413
+ data: {
414
+ id: 'task-1',
415
+ title: 'Test Task',
416
+ status: 'completed',
417
+ validated_at: null,
418
+ completed_by_session_id: 'other-session',
419
+ reviewing_by_session_id: null,
420
+ },
421
+ error: null,
422
+ },
423
+ updateResult: { data: null, error: null },
424
+ });
425
+ const ctx = createMockContext(supabase, { sessionId: 'reviewer-session' });
426
+
427
+ await claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
428
+
429
+ expect(supabase.update).toHaveBeenCalledWith(
430
+ expect.objectContaining({
431
+ reviewing_by_session_id: 'reviewer-session',
432
+ reviewing_started_at: expect.any(String),
433
+ })
434
+ );
435
+ });
436
+
437
+ it('should throw error when update fails', async () => {
438
+ const supabase = createMockSupabase({
439
+ selectResult: {
440
+ data: {
441
+ id: 'task-1',
442
+ title: 'Test Task',
443
+ status: 'completed',
444
+ validated_at: null,
445
+ completed_by_session_id: 'other-session',
446
+ reviewing_by_session_id: null,
447
+ },
448
+ error: null,
449
+ },
450
+ });
451
+ const ctx = createMockContext(supabase);
452
+
453
+ // Override update to return error
454
+ vi.mocked(supabase.from('').update).mockReturnValue({
455
+ ...supabase,
456
+ eq: vi.fn().mockReturnValue({
457
+ then: (resolve: (val: unknown) => void) =>
458
+ Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
459
+ }),
460
+ } as unknown as ReturnType<SupabaseClient['from']>);
461
+
462
+ await expect(
463
+ claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
464
+ ).rejects.toThrow('Failed to claim task');
465
+ });
466
+ });
467
+
468
+ // ============================================================================
469
+ // validateTask Tests
470
+ // ============================================================================
471
+
472
+ describe('validateTask', () => {
473
+ beforeEach(() => vi.clearAllMocks());
474
+
475
+ it('should throw error for missing task_id', async () => {
476
+ const supabase = createMockSupabase();
477
+ const ctx = createMockContext(supabase);
478
+
479
+ await expect(
480
+ validateTask({ approved: true }, ctx)
481
+ ).rejects.toThrow(ValidationError);
482
+ });
483
+
484
+ it('should throw error for invalid task_id UUID', async () => {
485
+ const supabase = createMockSupabase();
486
+ const ctx = createMockContext(supabase);
487
+
488
+ await expect(
489
+ validateTask({ task_id: 'invalid', approved: true }, ctx)
490
+ ).rejects.toThrow(ValidationError);
491
+ });
492
+
493
+ it('should throw error when task not found', async () => {
494
+ const supabase = createMockSupabase({
495
+ selectResult: { data: null, error: { message: 'Not found' } },
496
+ });
497
+ const ctx = createMockContext(supabase);
498
+
499
+ await expect(
500
+ validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
501
+ ).rejects.toThrow('Task not found');
502
+ });
503
+
504
+ it('should throw error when task is not completed', async () => {
505
+ const supabase = createMockSupabase({
506
+ selectResult: {
507
+ data: {
508
+ id: 'task-1',
509
+ title: 'Test Task',
510
+ status: 'in_progress',
511
+ validated_at: null,
512
+ completed_by_session_id: 'other-session',
513
+ project_id: 'proj-1',
514
+ git_branch: null,
515
+ },
516
+ error: null,
517
+ },
518
+ });
519
+ const ctx = createMockContext(supabase);
520
+
521
+ await expect(
522
+ validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
523
+ ).rejects.toThrow('Can only validate completed tasks');
524
+ });
525
+
526
+ it('should throw error when task already validated', async () => {
527
+ const supabase = createMockSupabase({
528
+ selectResult: {
529
+ data: {
530
+ id: 'task-1',
531
+ title: 'Test Task',
532
+ status: 'completed',
533
+ validated_at: '2025-01-14T12:00:00Z',
534
+ completed_by_session_id: 'other-session',
535
+ project_id: 'proj-1',
536
+ git_branch: null,
537
+ },
538
+ error: null,
539
+ },
540
+ });
541
+ const ctx = createMockContext(supabase);
542
+
543
+ await expect(
544
+ validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
545
+ ).rejects.toThrow('Task has already been validated');
546
+ });
547
+
548
+ describe('when approved', () => {
549
+ it('should mark task as validated', async () => {
550
+ const supabase = createMockSupabase({
551
+ selectResult: {
552
+ data: {
553
+ id: 'task-1',
554
+ title: 'Test Task',
555
+ status: 'completed',
556
+ validated_at: null,
557
+ completed_by_session_id: 'other-session',
558
+ project_id: 'proj-1',
559
+ git_branch: null,
560
+ },
561
+ error: null,
562
+ },
563
+ updateResult: { data: null, error: null },
564
+ insertResult: { data: null, error: null },
565
+ });
566
+ const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
567
+
568
+ const result = await validateTask(
569
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
570
+ ctx
571
+ );
572
+
573
+ expect(result.result).toMatchObject({
574
+ success: true,
575
+ validated_task_id: '123e4567-e89b-12d3-a456-426614174000',
576
+ self_validated: false,
577
+ });
578
+ });
579
+
580
+ it('should detect self-validation', async () => {
581
+ const supabase = createMockSupabase({
582
+ selectResult: {
583
+ data: {
584
+ id: 'task-1',
585
+ title: 'Test Task',
586
+ status: 'completed',
587
+ validated_at: null,
588
+ completed_by_session_id: 'session-123', // Same as validator
589
+ project_id: 'proj-1',
590
+ git_branch: null,
591
+ },
592
+ error: null,
593
+ },
594
+ updateResult: { data: null, error: null },
595
+ insertResult: { data: null, error: null },
596
+ });
597
+ const ctx = createMockContext(supabase, { sessionId: 'session-123' });
598
+
599
+ const result = await validateTask(
600
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
601
+ ctx
602
+ );
603
+
604
+ expect(result.result).toMatchObject({
605
+ success: true,
606
+ self_validated: true,
607
+ });
608
+ });
609
+
610
+ it('should clear reviewing status on approval', async () => {
611
+ const supabase = createMockSupabase({
612
+ selectResult: {
613
+ data: {
614
+ id: 'task-1',
615
+ title: 'Test Task',
616
+ status: 'completed',
617
+ validated_at: null,
618
+ completed_by_session_id: 'other-session',
619
+ project_id: 'proj-1',
620
+ git_branch: null,
621
+ },
622
+ error: null,
623
+ },
624
+ updateResult: { data: null, error: null },
625
+ insertResult: { data: null, error: null },
626
+ });
627
+ const ctx = createMockContext(supabase);
628
+
629
+ await validateTask(
630
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
631
+ ctx
632
+ );
633
+
634
+ expect(supabase.update).toHaveBeenCalledWith(
635
+ expect.objectContaining({
636
+ validated_at: expect.any(String),
637
+ reviewing_by_session_id: null,
638
+ reviewing_started_at: null,
639
+ })
640
+ );
641
+ });
642
+
643
+ it('should log validation in progress_logs', async () => {
644
+ const supabase = createMockSupabase({
645
+ selectResult: {
646
+ data: {
647
+ id: 'task-1',
648
+ title: 'Test Feature',
649
+ status: 'completed',
650
+ validated_at: null,
651
+ completed_by_session_id: 'other-session',
652
+ project_id: 'proj-123',
653
+ git_branch: null,
654
+ },
655
+ error: null,
656
+ },
657
+ updateResult: { data: null, error: null },
658
+ insertResult: { data: null, error: null },
659
+ });
660
+ const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
661
+
662
+ await validateTask(
663
+ {
664
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
665
+ approved: true,
666
+ validation_notes: 'Looks good!',
667
+ },
668
+ ctx
669
+ );
670
+
671
+ expect(supabase.from).toHaveBeenCalledWith('progress_logs');
672
+ expect(supabase.insert).toHaveBeenCalledWith(
673
+ expect.objectContaining({
674
+ project_id: 'proj-123',
675
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
676
+ summary: expect.stringContaining('Validated'),
677
+ details: 'Looks good!',
678
+ created_by: 'agent',
679
+ })
680
+ );
681
+ });
682
+ });
683
+
684
+ describe('when rejected', () => {
685
+ it('should reopen task', async () => {
686
+ const supabase = createMockSupabase({
687
+ selectResult: {
688
+ data: {
689
+ id: 'task-1',
690
+ title: 'Test Task',
691
+ status: 'completed',
692
+ validated_at: null,
693
+ completed_by_session_id: 'other-session',
694
+ project_id: 'proj-1',
695
+ git_branch: null,
696
+ },
697
+ error: null,
698
+ },
699
+ updateResult: { data: null, error: null },
700
+ insertResult: { data: null, error: null },
701
+ });
702
+ const ctx = createMockContext(supabase);
703
+
704
+ const result = await validateTask(
705
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
706
+ ctx
707
+ );
708
+
709
+ expect(result.result).toMatchObject({
710
+ success: true,
711
+ reopened_task_id: '123e4567-e89b-12d3-a456-426614174000',
712
+ });
713
+ });
714
+
715
+ it('should set task to in_progress with 80% progress', async () => {
716
+ const supabase = createMockSupabase({
717
+ selectResult: {
718
+ data: {
719
+ id: 'task-1',
720
+ title: 'Test Task',
721
+ status: 'completed',
722
+ validated_at: null,
723
+ completed_by_session_id: 'other-session',
724
+ project_id: 'proj-1',
725
+ git_branch: null,
726
+ },
727
+ error: null,
728
+ },
729
+ updateResult: { data: null, error: null },
730
+ insertResult: { data: null, error: null },
731
+ });
732
+ const ctx = createMockContext(supabase);
733
+
734
+ await validateTask(
735
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
736
+ ctx
737
+ );
738
+
739
+ expect(supabase.update).toHaveBeenCalledWith(
740
+ expect.objectContaining({
741
+ status: 'in_progress',
742
+ completed_at: null,
743
+ progress_percentage: 80,
744
+ reviewing_by_session_id: null,
745
+ reviewing_started_at: null,
746
+ })
747
+ );
748
+ });
749
+
750
+ it('should log rejection in progress_logs', async () => {
751
+ const supabase = createMockSupabase({
752
+ selectResult: {
753
+ data: {
754
+ id: 'task-1',
755
+ title: 'Broken Feature',
756
+ status: 'completed',
757
+ validated_at: null,
758
+ completed_by_session_id: 'other-session',
759
+ project_id: 'proj-123',
760
+ git_branch: null,
761
+ },
762
+ error: null,
763
+ },
764
+ updateResult: { data: null, error: null },
765
+ insertResult: { data: null, error: null },
766
+ });
767
+ const ctx = createMockContext(supabase);
768
+
769
+ await validateTask(
770
+ {
771
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
772
+ approved: false,
773
+ validation_notes: 'Tests failing',
774
+ },
775
+ ctx
776
+ );
777
+
778
+ expect(supabase.from).toHaveBeenCalledWith('progress_logs');
779
+ expect(supabase.insert).toHaveBeenCalledWith(
780
+ expect.objectContaining({
781
+ summary: expect.stringContaining('Validation failed'),
782
+ details: 'Tests failing',
783
+ })
784
+ );
785
+ });
786
+
787
+ it('should use default message when no validation_notes provided', async () => {
788
+ const supabase = createMockSupabase({
789
+ selectResult: {
790
+ data: {
791
+ id: 'task-1',
792
+ title: 'Test Task',
793
+ status: 'completed',
794
+ validated_at: null,
795
+ completed_by_session_id: 'other-session',
796
+ project_id: 'proj-123',
797
+ git_branch: null,
798
+ },
799
+ error: null,
800
+ },
801
+ updateResult: { data: null, error: null },
802
+ insertResult: { data: null, error: null },
803
+ });
804
+ const ctx = createMockContext(supabase);
805
+
806
+ await validateTask(
807
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
808
+ ctx
809
+ );
810
+
811
+ expect(supabase.insert).toHaveBeenCalledWith(
812
+ expect.objectContaining({
813
+ details: 'Needs more work',
814
+ })
815
+ );
816
+ });
817
+ });
818
+
819
+ it('should throw error when update fails on approval', async () => {
820
+ const supabase = createMockSupabase({
821
+ selectResult: {
822
+ data: {
823
+ id: 'task-1',
824
+ title: 'Test Task',
825
+ status: 'completed',
826
+ validated_at: null,
827
+ completed_by_session_id: 'other-session',
828
+ project_id: 'proj-1',
829
+ git_branch: null,
830
+ },
831
+ error: null,
832
+ },
833
+ });
834
+ const ctx = createMockContext(supabase);
835
+
836
+ // Override update to return error
837
+ vi.mocked(supabase.from('').update).mockReturnValue({
838
+ ...supabase,
839
+ eq: vi.fn().mockReturnValue({
840
+ then: (resolve: (val: unknown) => void) =>
841
+ Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
842
+ }),
843
+ } as unknown as ReturnType<SupabaseClient['from']>);
844
+
845
+ await expect(
846
+ validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
847
+ ).rejects.toThrow('Failed to validate task');
848
+ });
849
+
850
+ it('should throw error when update fails on rejection', async () => {
851
+ const supabase = createMockSupabase({
852
+ selectResult: {
853
+ data: {
854
+ id: 'task-1',
855
+ title: 'Test Task',
856
+ status: 'completed',
857
+ validated_at: null,
858
+ completed_by_session_id: 'other-session',
859
+ project_id: 'proj-1',
860
+ git_branch: null,
861
+ },
862
+ error: null,
863
+ },
864
+ });
865
+ const ctx = createMockContext(supabase);
866
+
867
+ // Override update to return error
868
+ vi.mocked(supabase.from('').update).mockReturnValue({
869
+ ...supabase,
870
+ eq: vi.fn().mockReturnValue({
871
+ then: (resolve: (val: unknown) => void) =>
872
+ Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
873
+ }),
874
+ } as unknown as ReturnType<SupabaseClient['from']>);
875
+
876
+ await expect(
877
+ validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false }, ctx)
878
+ ).rejects.toThrow('Failed to reopen task');
879
+ });
880
+ });