@vibescope/mcp-server 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1114 -0
  3. package/dist/api-client.js +698 -0
  4. package/dist/cli.d.ts +1 -6
  5. package/dist/cli.js +39 -240
  6. package/dist/config/tool-categories.d.ts +31 -0
  7. package/dist/config/tool-categories.js +253 -0
  8. package/dist/handlers/blockers.js +57 -58
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +106 -476
  11. package/dist/handlers/cost.d.ts +1 -0
  12. package/dist/handlers/cost.js +35 -113
  13. package/dist/handlers/decisions.d.ts +2 -0
  14. package/dist/handlers/decisions.js +28 -27
  15. package/dist/handlers/deployment.js +112 -828
  16. package/dist/handlers/discovery.js +31 -0
  17. package/dist/handlers/fallback.d.ts +2 -0
  18. package/dist/handlers/fallback.js +39 -134
  19. package/dist/handlers/findings.js +43 -67
  20. package/dist/handlers/git-issues.d.ts +9 -13
  21. package/dist/handlers/git-issues.js +80 -225
  22. package/dist/handlers/ideas.d.ts +3 -0
  23. package/dist/handlers/ideas.js +53 -134
  24. package/dist/handlers/index.d.ts +2 -0
  25. package/dist/handlers/index.js +6 -0
  26. package/dist/handlers/milestones.d.ts +2 -0
  27. package/dist/handlers/milestones.js +51 -98
  28. package/dist/handlers/organizations.js +79 -275
  29. package/dist/handlers/progress.d.ts +2 -0
  30. package/dist/handlers/progress.js +25 -123
  31. package/dist/handlers/project.js +42 -221
  32. package/dist/handlers/requests.d.ts +2 -0
  33. package/dist/handlers/requests.js +23 -83
  34. package/dist/handlers/session.js +99 -585
  35. package/dist/handlers/sprints.d.ts +32 -0
  36. package/dist/handlers/sprints.js +274 -0
  37. package/dist/handlers/tasks.d.ts +7 -10
  38. package/dist/handlers/tasks.js +230 -900
  39. package/dist/handlers/tool-docs.d.ts +8 -0
  40. package/dist/handlers/tool-docs.js +657 -0
  41. package/dist/handlers/types.d.ts +11 -3
  42. package/dist/handlers/validation.d.ts +1 -1
  43. package/dist/handlers/validation.js +26 -153
  44. package/dist/index.js +473 -160
  45. package/dist/knowledge.js +106 -9
  46. package/dist/tools.js +4 -0
  47. package/dist/validators.d.ts +21 -0
  48. package/dist/validators.js +91 -0
  49. package/package.json +2 -3
  50. package/src/api-client.ts +1752 -0
  51. package/src/cli.test.ts +128 -302
  52. package/src/cli.ts +41 -285
  53. package/src/handlers/__test-setup__.ts +210 -0
  54. package/src/handlers/__test-utils__.ts +4 -134
  55. package/src/handlers/blockers.test.ts +114 -124
  56. package/src/handlers/blockers.ts +68 -70
  57. package/src/handlers/bodies-of-work.test.ts +236 -831
  58. package/src/handlers/bodies-of-work.ts +194 -525
  59. package/src/handlers/cost.test.ts +149 -113
  60. package/src/handlers/cost.ts +44 -132
  61. package/src/handlers/decisions.test.ts +111 -209
  62. package/src/handlers/decisions.ts +35 -27
  63. package/src/handlers/deployment.test.ts +193 -239
  64. package/src/handlers/deployment.ts +140 -895
  65. package/src/handlers/discovery.test.ts +20 -67
  66. package/src/handlers/discovery.ts +32 -0
  67. package/src/handlers/fallback.test.ts +128 -361
  68. package/src/handlers/fallback.ts +62 -148
  69. package/src/handlers/findings.test.ts +127 -345
  70. package/src/handlers/findings.ts +49 -66
  71. package/src/handlers/git-issues.test.ts +623 -0
  72. package/src/handlers/git-issues.ts +174 -0
  73. package/src/handlers/ideas.test.ts +229 -343
  74. package/src/handlers/ideas.ts +69 -143
  75. package/src/handlers/index.ts +6 -0
  76. package/src/handlers/milestones.test.ts +167 -281
  77. package/src/handlers/milestones.ts +54 -93
  78. package/src/handlers/organizations.test.ts +275 -467
  79. package/src/handlers/organizations.ts +84 -294
  80. package/src/handlers/progress.test.ts +112 -218
  81. package/src/handlers/progress.ts +29 -142
  82. package/src/handlers/project.test.ts +203 -226
  83. package/src/handlers/project.ts +48 -238
  84. package/src/handlers/requests.test.ts +74 -342
  85. package/src/handlers/requests.ts +25 -83
  86. package/src/handlers/session.test.ts +241 -206
  87. package/src/handlers/session.ts +110 -657
  88. package/src/handlers/sprints.test.ts +711 -0
  89. package/src/handlers/sprints.ts +497 -0
  90. package/src/handlers/tasks.test.ts +608 -353
  91. package/src/handlers/tasks.ts +248 -1025
  92. package/src/handlers/types.ts +12 -4
  93. package/src/handlers/validation.test.ts +189 -572
  94. package/src/handlers/validation.ts +29 -166
  95. package/src/index.ts +473 -184
  96. package/src/knowledge.ts +107 -9
  97. package/src/tools.ts +2506 -0
  98. package/src/validators.test.ts +223 -223
  99. package/src/validators.ts +127 -0
  100. package/tsconfig.json +1 -1
  101. package/vitest.config.ts +14 -13
  102. package/dist/cli.test.d.ts +0 -1
  103. package/dist/cli.test.js +0 -367
  104. package/dist/handlers/__test-utils__.d.ts +0 -72
  105. package/dist/handlers/__test-utils__.js +0 -176
  106. package/dist/handlers/checkouts.d.ts +0 -37
  107. package/dist/handlers/checkouts.js +0 -377
  108. package/dist/handlers/knowledge-query.d.ts +0 -22
  109. package/dist/handlers/knowledge-query.js +0 -253
  110. package/dist/handlers/knowledge.d.ts +0 -12
  111. package/dist/handlers/knowledge.js +0 -108
  112. package/dist/handlers/roles.d.ts +0 -30
  113. package/dist/handlers/roles.js +0 -281
  114. package/dist/handlers/tasks.test.d.ts +0 -1
  115. package/dist/handlers/tasks.test.js +0 -431
  116. package/dist/utils.test.d.ts +0 -1
  117. package/dist/utils.test.js +0 -532
  118. package/dist/validators.test.d.ts +0 -1
  119. package/dist/validators.test.js +0 -176
  120. package/src/tmpclaude-0078-cwd +0 -1
  121. package/src/tmpclaude-0ee1-cwd +0 -1
  122. package/src/tmpclaude-2dd5-cwd +0 -1
  123. package/src/tmpclaude-344c-cwd +0 -1
  124. package/src/tmpclaude-3860-cwd +0 -1
  125. package/src/tmpclaude-4b63-cwd +0 -1
  126. package/src/tmpclaude-5c73-cwd +0 -1
  127. package/src/tmpclaude-5ee3-cwd +0 -1
  128. package/src/tmpclaude-6795-cwd +0 -1
  129. package/src/tmpclaude-709e-cwd +0 -1
  130. package/src/tmpclaude-9839-cwd +0 -1
  131. package/src/tmpclaude-d829-cwd +0 -1
  132. package/src/tmpclaude-e072-cwd +0 -1
  133. package/src/tmpclaude-f6ee-cwd +0 -1
  134. package/tmpclaude-0439-cwd +0 -1
  135. package/tmpclaude-132f-cwd +0 -1
  136. package/tmpclaude-15bb-cwd +0 -1
  137. package/tmpclaude-165a-cwd +0 -1
  138. package/tmpclaude-1ba9-cwd +0 -1
  139. package/tmpclaude-21a3-cwd +0 -1
  140. package/tmpclaude-2a38-cwd +0 -1
  141. package/tmpclaude-2adf-cwd +0 -1
  142. package/tmpclaude-2f56-cwd +0 -1
  143. package/tmpclaude-3626-cwd +0 -1
  144. package/tmpclaude-3727-cwd +0 -1
  145. package/tmpclaude-40bc-cwd +0 -1
  146. package/tmpclaude-436f-cwd +0 -1
  147. package/tmpclaude-4783-cwd +0 -1
  148. package/tmpclaude-4b6d-cwd +0 -1
  149. package/tmpclaude-4ba4-cwd +0 -1
  150. package/tmpclaude-51e6-cwd +0 -1
  151. package/tmpclaude-5ecf-cwd +0 -1
  152. package/tmpclaude-6f97-cwd +0 -1
  153. package/tmpclaude-7fb2-cwd +0 -1
  154. package/tmpclaude-825c-cwd +0 -1
  155. package/tmpclaude-8baf-cwd +0 -1
  156. package/tmpclaude-8d9f-cwd +0 -1
  157. package/tmpclaude-975c-cwd +0 -1
  158. package/tmpclaude-9983-cwd +0 -1
  159. package/tmpclaude-a045-cwd +0 -1
  160. package/tmpclaude-ac4a-cwd +0 -1
  161. package/tmpclaude-b593-cwd +0 -1
  162. package/tmpclaude-b891-cwd +0 -1
  163. package/tmpclaude-c032-cwd +0 -1
  164. package/tmpclaude-cf43-cwd +0 -1
  165. package/tmpclaude-d040-cwd +0 -1
  166. package/tmpclaude-dcdd-cwd +0 -1
  167. package/tmpclaude-dcee-cwd +0 -1
  168. package/tmpclaude-e16b-cwd +0 -1
  169. package/tmpclaude-ecd2-cwd +0 -1
  170. package/tmpclaude-f48d-cwd +0 -1
@@ -1,119 +1,18 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { SupabaseClient } from '@supabase/supabase-js';
3
- import type { HandlerContext, TokenUsage } from './types.js';
4
2
  import {
5
3
  getTasksAwaitingValidation,
6
4
  claimValidation,
7
5
  validateTask,
8
6
  } from './validation.js';
9
7
  import { ValidationError } from '../validators.js';
8
+ import { createMockContext } from './__test-utils__.js';
9
+ import { mockApiClient } from './__test-setup__.js';
10
10
 
11
11
  // ============================================================================
12
- // Test Utilities
12
+ // Test Constants
13
13
  // ============================================================================
14
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
- }
15
+ const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
117
16
 
118
17
  // ============================================================================
119
18
  // getTasksAwaitingValidation Tests
@@ -123,15 +22,13 @@ describe('getTasksAwaitingValidation', () => {
123
22
  beforeEach(() => vi.clearAllMocks());
124
23
 
125
24
  it('should throw error for missing project_id', async () => {
126
- const supabase = createMockSupabase();
127
- const ctx = createMockContext(supabase);
25
+ const ctx = createMockContext();
128
26
 
129
27
  await expect(getTasksAwaitingValidation({}, ctx)).rejects.toThrow(ValidationError);
130
28
  });
131
29
 
132
30
  it('should throw error for invalid project_id UUID', async () => {
133
- const supabase = createMockSupabase();
134
- const ctx = createMockContext(supabase);
31
+ const ctx = createMockContext();
135
32
 
136
33
  await expect(
137
34
  getTasksAwaitingValidation({ project_id: 'invalid' }, ctx)
@@ -139,26 +36,19 @@ describe('getTasksAwaitingValidation', () => {
139
36
  });
140
37
 
141
38
  it('should return empty list when no tasks awaiting validation', async () => {
142
- const supabase = createMockSupabase({
143
- selectResult: { data: [], error: null },
39
+ mockApiClient.getTasksAwaitingValidation.mockResolvedValue({
40
+ ok: true,
41
+ data: { tasks: [], count: 0 },
144
42
  });
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']>);
43
+ const ctx = createMockContext();
153
44
 
154
45
  const result = await getTasksAwaitingValidation(
155
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
46
+ { project_id: VALID_UUID },
156
47
  ctx
157
48
  );
158
49
 
159
50
  expect(result.result).toHaveProperty('tasks');
160
51
  expect((result.result as { tasks: unknown[] }).tasks).toEqual([]);
161
- expect((result.result as { hint?: string }).hint).toBeUndefined();
162
52
  });
163
53
 
164
54
  it('should return tasks with review status info', async () => {
@@ -168,33 +58,31 @@ describe('getTasksAwaitingValidation', () => {
168
58
  title: 'Implement feature A',
169
59
  completed_at: '2025-01-14T10:00:00Z',
170
60
  completed_by_session_id: 'other-session',
171
- reviewing_by_session_id: null,
172
- reviewing_started_at: null,
61
+ being_reviewed: false,
62
+ review_minutes: null,
173
63
  },
174
64
  {
175
65
  id: 'task-2',
176
66
  title: 'Implement feature B',
177
67
  completed_at: '2025-01-14T11:00:00Z',
178
68
  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
69
+ being_reviewed: true,
70
+ review_minutes: 5,
181
71
  },
182
72
  ];
183
73
 
184
- const supabase = createMockSupabase({
185
- selectResult: { data: mockTasks, error: null },
74
+ mockApiClient.getTasksAwaitingValidation.mockResolvedValue({
75
+ ok: true,
76
+ data: {
77
+ tasks: mockTasks,
78
+ count: 2,
79
+ hint: 'Use claim_validation to claim a task for review',
80
+ },
186
81
  });
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']>);
82
+ const ctx = createMockContext();
195
83
 
196
84
  const result = await getTasksAwaitingValidation(
197
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
85
+ { project_id: VALID_UUID },
198
86
  ctx
199
87
  );
200
88
 
@@ -207,46 +95,37 @@ describe('getTasksAwaitingValidation', () => {
207
95
 
208
96
  // Second task being reviewed for ~5 minutes
209
97
  expect(tasks[1].being_reviewed).toBe(true);
210
- expect(tasks[1].review_minutes).toBeGreaterThanOrEqual(4);
211
- expect(tasks[1].review_minutes).toBeLessThanOrEqual(6);
98
+ expect(tasks[1].review_minutes).toBe(5);
212
99
 
213
100
  // Should have hint when tasks are present
214
101
  expect((result.result as { hint?: string }).hint).toContain('claim_validation');
215
102
  });
216
103
 
217
- it('should query correct table with filters', async () => {
218
- const supabase = createMockSupabase({
219
- selectResult: { data: [], error: null },
104
+ it('should call API client with correct project_id', async () => {
105
+ mockApiClient.getTasksAwaitingValidation.mockResolvedValue({
106
+ ok: true,
107
+ data: { tasks: [], count: 0 },
220
108
  });
221
- const ctx = createMockContext(supabase);
109
+ const ctx = createMockContext();
222
110
 
223
111
  await getTasksAwaitingValidation(
224
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
112
+ { project_id: VALID_UUID },
225
113
  ctx
226
114
  );
227
115
 
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);
116
+ expect(mockApiClient.getTasksAwaitingValidation).toHaveBeenCalledWith(VALID_UUID);
232
117
  });
233
118
 
234
- it('should throw error when database query fails', async () => {
235
- const supabase = createMockSupabase({
236
- selectResult: { data: null, error: { message: 'Database error' } },
119
+ it('should throw error when API call fails', async () => {
120
+ mockApiClient.getTasksAwaitingValidation.mockResolvedValue({
121
+ ok: false,
122
+ error: 'Failed to fetch tasks awaiting validation',
237
123
  });
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']>);
124
+ const ctx = createMockContext();
246
125
 
247
126
  await expect(
248
- getTasksAwaitingValidation({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
249
- ).rejects.toThrow('Failed to fetch tasks');
127
+ getTasksAwaitingValidation({ project_id: VALID_UUID }, ctx)
128
+ ).rejects.toThrow('Failed to fetch tasks awaiting validation');
250
129
  });
251
130
  });
252
131
 
@@ -258,15 +137,13 @@ describe('claimValidation', () => {
258
137
  beforeEach(() => vi.clearAllMocks());
259
138
 
260
139
  it('should throw error for missing task_id', async () => {
261
- const supabase = createMockSupabase();
262
- const ctx = createMockContext(supabase);
140
+ const ctx = createMockContext();
263
141
 
264
142
  await expect(claimValidation({}, ctx)).rejects.toThrow(ValidationError);
265
143
  });
266
144
 
267
145
  it('should throw error for invalid task_id UUID', async () => {
268
- const supabase = createMockSupabase();
269
- const ctx = createMockContext(supabase);
146
+ const ctx = createMockContext();
270
147
 
271
148
  await expect(
272
149
  claimValidation({ task_id: 'not-a-uuid' }, ctx)
@@ -274,194 +151,133 @@ describe('claimValidation', () => {
274
151
  });
275
152
 
276
153
  it('should throw error when task not found', async () => {
277
- const supabase = createMockSupabase({
278
- selectResult: { data: null, error: { message: 'Not found' } },
154
+ mockApiClient.claimValidation.mockResolvedValue({
155
+ ok: false,
156
+ error: 'Task not found',
279
157
  });
280
- const ctx = createMockContext(supabase);
158
+ const ctx = createMockContext();
281
159
 
282
160
  await expect(
283
- claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
161
+ claimValidation({ task_id: VALID_UUID }, ctx)
284
162
  ).rejects.toThrow('Task not found');
285
163
  });
286
164
 
287
165
  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
- },
166
+ mockApiClient.claimValidation.mockResolvedValue({
167
+ ok: false,
168
+ error: 'Can only claim completed tasks for review',
300
169
  });
301
- const ctx = createMockContext(supabase);
170
+ const ctx = createMockContext();
302
171
 
303
172
  await expect(
304
- claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
173
+ claimValidation({ task_id: VALID_UUID }, ctx)
305
174
  ).rejects.toThrow('Can only claim completed tasks for review');
306
175
  });
307
176
 
308
177
  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
- },
178
+ mockApiClient.claimValidation.mockResolvedValue({
179
+ ok: false,
180
+ error: 'Task has already been validated',
321
181
  });
322
- const ctx = createMockContext(supabase);
182
+ const ctx = createMockContext();
323
183
 
324
184
  await expect(
325
- claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
185
+ claimValidation({ task_id: VALID_UUID }, ctx)
326
186
  ).rejects.toThrow('Task has already been validated');
327
187
  });
328
188
 
329
189
  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
- },
190
+ mockApiClient.claimValidation.mockResolvedValue({
191
+ ok: false,
192
+ error: 'Task is already being reviewed by another agent',
342
193
  });
343
- const ctx = createMockContext(supabase, { sessionId: 'session-123' });
194
+ const ctx = createMockContext({ sessionId: 'session-123' });
344
195
 
345
196
  await expect(
346
- claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
197
+ claimValidation({ task_id: VALID_UUID }, ctx)
347
198
  ).rejects.toThrow('Task is already being reviewed by another agent');
348
199
  });
349
200
 
350
201
  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,
202
+ mockApiClient.claimValidation.mockResolvedValue({
203
+ ok: true,
204
+ data: {
205
+ success: true,
206
+ task_id: VALID_UUID,
207
+ title: 'Test Task',
208
+ message: 'Re-claimed task for review',
362
209
  },
363
- updateResult: { data: null, error: null },
364
210
  });
365
- const ctx = createMockContext(supabase, { sessionId: 'session-123' });
211
+ const ctx = createMockContext({ sessionId: 'session-123' });
366
212
 
367
213
  const result = await claimValidation(
368
- { task_id: '123e4567-e89b-12d3-a456-426614174000' },
214
+ { task_id: VALID_UUID },
369
215
  ctx
370
216
  );
371
217
 
372
218
  expect(result.result).toMatchObject({
373
219
  success: true,
374
- task_id: '123e4567-e89b-12d3-a456-426614174000',
220
+ task_id: VALID_UUID,
375
221
  title: 'Test Task',
376
222
  });
377
223
  });
378
224
 
379
225
  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,
226
+ mockApiClient.claimValidation.mockResolvedValue({
227
+ ok: true,
228
+ data: {
229
+ success: true,
230
+ task_id: VALID_UUID,
231
+ title: 'Test Task',
232
+ message: 'Task claimed for review. Dashboard updated.',
233
+ next_step: 'Run tests and verify implementation. Call validate_task when done.',
391
234
  },
392
- updateResult: { data: null, error: null },
393
235
  });
394
- const ctx = createMockContext(supabase);
236
+ const ctx = createMockContext();
395
237
 
396
238
  const result = await claimValidation(
397
- { task_id: '123e4567-e89b-12d3-a456-426614174000' },
239
+ { task_id: VALID_UUID },
398
240
  ctx
399
241
  );
400
242
 
401
243
  expect(result.result).toMatchObject({
402
244
  success: true,
403
- task_id: '123e4567-e89b-12d3-a456-426614174000',
245
+ task_id: VALID_UUID,
404
246
  title: 'Test Task',
405
247
  message: expect.stringContaining('Dashboard'),
406
248
  });
407
249
  expect((result.result as { next_step: string }).next_step).toContain('validate_task');
408
250
  });
409
251
 
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,
252
+ it('should call API client with session_id', async () => {
253
+ mockApiClient.claimValidation.mockResolvedValue({
254
+ ok: true,
255
+ data: {
256
+ success: true,
257
+ task_id: VALID_UUID,
258
+ title: 'Test Task',
422
259
  },
423
- updateResult: { data: null, error: null },
424
260
  });
425
- const ctx = createMockContext(supabase, { sessionId: 'reviewer-session' });
261
+ const ctx = createMockContext({ sessionId: 'reviewer-session' });
426
262
 
427
- await claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
263
+ await claimValidation({ task_id: VALID_UUID }, ctx);
428
264
 
429
- expect(supabase.update).toHaveBeenCalledWith(
430
- expect.objectContaining({
431
- reviewing_by_session_id: 'reviewer-session',
432
- reviewing_started_at: expect.any(String),
433
- })
265
+ expect(mockApiClient.claimValidation).toHaveBeenCalledWith(
266
+ VALID_UUID,
267
+ 'reviewer-session'
434
268
  );
435
269
  });
436
270
 
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
- },
271
+ it('should throw error when claim fails', async () => {
272
+ mockApiClient.claimValidation.mockResolvedValue({
273
+ ok: false,
274
+ error: 'Failed to claim task for validation',
450
275
  });
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']>);
276
+ const ctx = createMockContext();
461
277
 
462
278
  await expect(
463
- claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
464
- ).rejects.toThrow('Failed to claim task');
279
+ claimValidation({ task_id: VALID_UUID }, ctx)
280
+ ).rejects.toThrow('Failed to claim task for validation');
465
281
  });
466
282
  });
467
283
 
@@ -473,8 +289,7 @@ describe('validateTask', () => {
473
289
  beforeEach(() => vi.clearAllMocks());
474
290
 
475
291
  it('should throw error for missing task_id', async () => {
476
- const supabase = createMockSupabase();
477
- const ctx = createMockContext(supabase);
292
+ const ctx = createMockContext();
478
293
 
479
294
  await expect(
480
295
  validateTask({ approved: true }, ctx)
@@ -482,122 +297,94 @@ describe('validateTask', () => {
482
297
  });
483
298
 
484
299
  it('should throw error for invalid task_id UUID', async () => {
485
- const supabase = createMockSupabase();
486
- const ctx = createMockContext(supabase);
300
+ const ctx = createMockContext();
487
301
 
488
302
  await expect(
489
303
  validateTask({ task_id: 'invalid', approved: true }, ctx)
490
304
  ).rejects.toThrow(ValidationError);
491
305
  });
492
306
 
307
+ it('should throw error when approved is missing', async () => {
308
+ const ctx = createMockContext();
309
+
310
+ await expect(
311
+ validateTask({ task_id: VALID_UUID }, ctx)
312
+ ).rejects.toThrow('approved is required');
313
+ });
314
+
493
315
  it('should throw error when task not found', async () => {
494
- const supabase = createMockSupabase({
495
- selectResult: { data: null, error: { message: 'Not found' } },
316
+ mockApiClient.validateTask.mockResolvedValue({
317
+ ok: false,
318
+ error: 'Task not found',
496
319
  });
497
- const ctx = createMockContext(supabase);
320
+ const ctx = createMockContext();
498
321
 
499
322
  await expect(
500
- validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
323
+ validateTask({ task_id: VALID_UUID, approved: true }, ctx)
501
324
  ).rejects.toThrow('Task not found');
502
325
  });
503
326
 
504
327
  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
- },
328
+ mockApiClient.validateTask.mockResolvedValue({
329
+ ok: false,
330
+ error: 'Can only validate completed tasks',
518
331
  });
519
- const ctx = createMockContext(supabase);
332
+ const ctx = createMockContext();
520
333
 
521
334
  await expect(
522
- validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
335
+ validateTask({ task_id: VALID_UUID, approved: true }, ctx)
523
336
  ).rejects.toThrow('Can only validate completed tasks');
524
337
  });
525
338
 
526
339
  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
- },
340
+ mockApiClient.validateTask.mockResolvedValue({
341
+ ok: false,
342
+ error: 'Task has already been validated',
540
343
  });
541
- const ctx = createMockContext(supabase);
344
+ const ctx = createMockContext();
542
345
 
543
346
  await expect(
544
- validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
347
+ validateTask({ task_id: VALID_UUID, approved: true }, ctx)
545
348
  ).rejects.toThrow('Task has already been validated');
546
349
  });
547
350
 
548
351
  describe('when approved', () => {
549
352
  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,
353
+ mockApiClient.validateTask.mockResolvedValue({
354
+ ok: true,
355
+ data: {
356
+ success: true,
357
+ validated_task_id: VALID_UUID,
358
+ self_validated: false,
562
359
  },
563
- updateResult: { data: null, error: null },
564
- insertResult: { data: null, error: null },
565
360
  });
566
- const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
361
+ const ctx = createMockContext({ sessionId: 'validator-session' });
567
362
 
568
363
  const result = await validateTask(
569
- { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
364
+ { task_id: VALID_UUID, approved: true },
570
365
  ctx
571
366
  );
572
367
 
573
368
  expect(result.result).toMatchObject({
574
369
  success: true,
575
- validated_task_id: '123e4567-e89b-12d3-a456-426614174000',
370
+ validated_task_id: VALID_UUID,
576
371
  self_validated: false,
577
372
  });
578
373
  });
579
374
 
580
375
  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,
376
+ mockApiClient.validateTask.mockResolvedValue({
377
+ ok: true,
378
+ data: {
379
+ success: true,
380
+ validated_task_id: VALID_UUID,
381
+ self_validated: true,
593
382
  },
594
- updateResult: { data: null, error: null },
595
- insertResult: { data: null, error: null },
596
383
  });
597
- const ctx = createMockContext(supabase, { sessionId: 'session-123' });
384
+ const ctx = createMockContext({ sessionId: 'session-123' });
598
385
 
599
386
  const result = await validateTask(
600
- { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
387
+ { task_id: VALID_UUID, approved: true },
601
388
  ctx
602
389
  );
603
390
 
@@ -607,274 +394,104 @@ describe('validateTask', () => {
607
394
  });
608
395
  });
609
396
 
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,
397
+ it('should pass validation_notes to API', async () => {
398
+ mockApiClient.validateTask.mockResolvedValue({
399
+ ok: true,
400
+ data: {
401
+ success: true,
402
+ validated_task_id: VALID_UUID,
403
+ self_validated: false,
656
404
  },
657
- updateResult: { data: null, error: null },
658
- insertResult: { data: null, error: null },
659
405
  });
660
- const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
406
+ const ctx = createMockContext({ sessionId: 'validator-session' });
661
407
 
662
408
  await validateTask(
663
409
  {
664
- task_id: '123e4567-e89b-12d3-a456-426614174000',
410
+ task_id: VALID_UUID,
665
411
  approved: true,
666
412
  validation_notes: 'Looks good!',
667
413
  },
668
414
  ctx
669
415
  );
670
416
 
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
- })
417
+ expect(mockApiClient.validateTask).toHaveBeenCalledWith(
418
+ VALID_UUID,
419
+ { approved: true, validation_notes: 'Looks good!' },
420
+ 'validator-session'
680
421
  );
681
422
  });
682
423
  });
683
424
 
684
425
  describe('when rejected', () => {
685
426
  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,
427
+ mockApiClient.validateTask.mockResolvedValue({
428
+ ok: true,
429
+ data: {
430
+ success: true,
431
+ reopened_task_id: VALID_UUID,
698
432
  },
699
- updateResult: { data: null, error: null },
700
- insertResult: { data: null, error: null },
701
433
  });
702
- const ctx = createMockContext(supabase);
434
+ const ctx = createMockContext();
703
435
 
704
436
  const result = await validateTask(
705
- { task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
437
+ { task_id: VALID_UUID, approved: false },
706
438
  ctx
707
439
  );
708
440
 
709
441
  expect(result.result).toMatchObject({
710
442
  success: true,
711
- reopened_task_id: '123e4567-e89b-12d3-a456-426614174000',
443
+ reopened_task_id: VALID_UUID,
712
444
  });
713
445
  });
714
446
 
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,
447
+ it('should pass rejection notes to API', async () => {
448
+ mockApiClient.validateTask.mockResolvedValue({
449
+ ok: true,
450
+ data: {
451
+ success: true,
452
+ reopened_task_id: VALID_UUID,
763
453
  },
764
- updateResult: { data: null, error: null },
765
- insertResult: { data: null, error: null },
766
454
  });
767
- const ctx = createMockContext(supabase);
455
+ const ctx = createMockContext();
768
456
 
769
457
  await validateTask(
770
458
  {
771
- task_id: '123e4567-e89b-12d3-a456-426614174000',
459
+ task_id: VALID_UUID,
772
460
  approved: false,
773
461
  validation_notes: 'Tests failing',
774
462
  },
775
463
  ctx
776
464
  );
777
465
 
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
- })
466
+ expect(mockApiClient.validateTask).toHaveBeenCalledWith(
467
+ VALID_UUID,
468
+ { approved: false, validation_notes: 'Tests failing' },
469
+ 'session-123'
815
470
  );
816
471
  });
817
472
  });
818
473
 
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
- },
474
+ it('should throw error when validation fails on approval', async () => {
475
+ mockApiClient.validateTask.mockResolvedValue({
476
+ ok: false,
477
+ error: 'Failed to validate task',
833
478
  });
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']>);
479
+ const ctx = createMockContext();
844
480
 
845
481
  await expect(
846
- validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
482
+ validateTask({ task_id: VALID_UUID, approved: true }, ctx)
847
483
  ).rejects.toThrow('Failed to validate task');
848
484
  });
849
485
 
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
- },
486
+ it('should throw error when validation fails on rejection', async () => {
487
+ mockApiClient.validateTask.mockResolvedValue({
488
+ ok: false,
489
+ error: 'Failed to validate task',
864
490
  });
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']>);
491
+ const ctx = createMockContext();
875
492
 
876
493
  await expect(
877
- validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false }, ctx)
878
- ).rejects.toThrow('Failed to reopen task');
494
+ validateTask({ task_id: VALID_UUID, approved: false }, ctx)
495
+ ).rejects.toThrow('Failed to validate task');
879
496
  });
880
497
  });