@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 @@
1
+ export {};
@@ -0,0 +1,431 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { getTasks, addTask, updateTask, completeTask, deleteTask, addTaskReference, removeTaskReference, getProjectGitConfig, } from './tasks.js';
3
+ import { ValidationError } from '../validators.js';
4
+ // ============================================================================
5
+ // Test Utilities
6
+ // ============================================================================
7
+ /**
8
+ * Creates a mock Supabase client with chainable methods
9
+ */
10
+ function createMockSupabase(overrides = {}) {
11
+ const defaultResult = { data: null, error: null };
12
+ // Track both the operation type AND if insert has been followed by select
13
+ let currentOperation = 'select';
14
+ let insertThenSelect = false;
15
+ const mock = {
16
+ from: vi.fn().mockReturnThis(),
17
+ select: vi.fn(() => {
18
+ // If we just did an insert and now calling select, it's insert().select() chain
19
+ if (currentOperation === 'insert') {
20
+ insertThenSelect = true;
21
+ }
22
+ else {
23
+ currentOperation = 'select';
24
+ insertThenSelect = false;
25
+ }
26
+ return mock;
27
+ }),
28
+ insert: vi.fn(() => {
29
+ currentOperation = 'insert';
30
+ insertThenSelect = false;
31
+ return mock;
32
+ }),
33
+ update: vi.fn(() => {
34
+ currentOperation = 'update';
35
+ insertThenSelect = false;
36
+ return mock;
37
+ }),
38
+ delete: vi.fn(() => {
39
+ currentOperation = 'delete';
40
+ insertThenSelect = false;
41
+ return mock;
42
+ }),
43
+ eq: vi.fn().mockReturnThis(),
44
+ neq: vi.fn().mockReturnThis(),
45
+ in: vi.fn().mockReturnThis(),
46
+ is: vi.fn().mockReturnThis(),
47
+ not: vi.fn().mockReturnThis(),
48
+ or: vi.fn().mockReturnThis(),
49
+ lt: vi.fn().mockReturnThis(),
50
+ order: vi.fn().mockReturnThis(),
51
+ limit: vi.fn().mockReturnThis(),
52
+ single: vi.fn(() => {
53
+ // Handle insert().select().single() pattern
54
+ if (currentOperation === 'insert' || insertThenSelect) {
55
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
56
+ }
57
+ if (currentOperation === 'select') {
58
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
59
+ }
60
+ if (currentOperation === 'update') {
61
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
62
+ }
63
+ return Promise.resolve(defaultResult);
64
+ }),
65
+ maybeSingle: vi.fn(() => {
66
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
67
+ }),
68
+ then: vi.fn((resolve) => {
69
+ if (currentOperation === 'insert' || insertThenSelect) {
70
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
71
+ }
72
+ if (currentOperation === 'select') {
73
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
74
+ }
75
+ if (currentOperation === 'update') {
76
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
77
+ }
78
+ if (currentOperation === 'delete') {
79
+ return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
80
+ }
81
+ return Promise.resolve(defaultResult).then(resolve);
82
+ }),
83
+ };
84
+ return mock;
85
+ }
86
+ /**
87
+ * Creates a mock handler context
88
+ */
89
+ function createMockContext(supabase, sessionId = 'session-123') {
90
+ return {
91
+ supabase,
92
+ auth: {
93
+ userId: 'user-123',
94
+ apiKeyId: 'api-key-123',
95
+ },
96
+ session: {
97
+ currentSessionId: sessionId,
98
+ },
99
+ };
100
+ }
101
+ // ============================================================================
102
+ // getTasks Tests
103
+ // ============================================================================
104
+ describe('getTasks', () => {
105
+ beforeEach(() => {
106
+ vi.clearAllMocks();
107
+ });
108
+ it('should return tasks for a valid project', async () => {
109
+ const mockTasks = [
110
+ { id: 'task-1', title: 'Task 1', status: 'pending', priority: 1 },
111
+ { id: 'task-2', title: 'Task 2', status: 'in_progress', priority: 2 },
112
+ ];
113
+ const supabase = createMockSupabase({
114
+ selectResult: { data: mockTasks, error: null },
115
+ });
116
+ const ctx = createMockContext(supabase);
117
+ const result = await getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
118
+ expect(result.result.tasks).toEqual(mockTasks);
119
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
120
+ });
121
+ it('should throw error for missing project_id', async () => {
122
+ const supabase = createMockSupabase();
123
+ const ctx = createMockContext(supabase);
124
+ await expect(getTasks({}, ctx)).rejects.toThrow(ValidationError);
125
+ });
126
+ it('should throw error for invalid project_id UUID', async () => {
127
+ const supabase = createMockSupabase();
128
+ const ctx = createMockContext(supabase);
129
+ await expect(getTasks({ project_id: 'not-a-uuid' }, ctx)).rejects.toThrow(ValidationError);
130
+ });
131
+ it('should filter by status when provided', async () => {
132
+ const mockTasks = [{ id: 'task-1', title: 'Task 1', status: 'pending', priority: 1 }];
133
+ const supabase = createMockSupabase({
134
+ selectResult: { data: mockTasks, error: null },
135
+ });
136
+ const ctx = createMockContext(supabase);
137
+ await getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'pending' }, ctx);
138
+ expect(supabase.eq).toHaveBeenCalledWith('status', 'pending');
139
+ });
140
+ it('should throw error for invalid status', async () => {
141
+ const supabase = createMockSupabase();
142
+ const ctx = createMockContext(supabase);
143
+ await expect(getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
144
+ });
145
+ it('should handle database errors', async () => {
146
+ const supabase = createMockSupabase({
147
+ selectResult: { data: null, error: { message: 'Database error' } },
148
+ });
149
+ const ctx = createMockContext(supabase);
150
+ // The handler uses .then() pattern, so we need to adjust the mock
151
+ vi.mocked(supabase.from('tasks').select).mockReturnValue({
152
+ ...supabase,
153
+ then: (resolve) => Promise.resolve({ data: null, error: { message: 'Database error' } }).then(resolve),
154
+ });
155
+ await expect(getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)).rejects.toThrow('Failed to fetch tasks');
156
+ });
157
+ });
158
+ // ============================================================================
159
+ // addTask Tests
160
+ // ============================================================================
161
+ describe('addTask', () => {
162
+ beforeEach(() => {
163
+ vi.clearAllMocks();
164
+ });
165
+ it('should add a task successfully', async () => {
166
+ const supabase = createMockSupabase({
167
+ insertResult: { data: { id: 'new-task-123', blocking: false }, error: null },
168
+ });
169
+ const ctx = createMockContext(supabase);
170
+ const result = await addTask({
171
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
172
+ title: 'New Task',
173
+ description: 'Task description',
174
+ priority: 2,
175
+ }, ctx);
176
+ expect(result.result.success).toBe(true);
177
+ expect(result.result.task_id).toBe('new-task-123');
178
+ expect(result.result.title).toBe('New Task');
179
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
180
+ expect(supabase.insert).toHaveBeenCalled();
181
+ });
182
+ it('should throw error for missing project_id', async () => {
183
+ const supabase = createMockSupabase();
184
+ const ctx = createMockContext(supabase);
185
+ await expect(addTask({ title: 'Test' }, ctx)).rejects.toThrow(ValidationError);
186
+ });
187
+ it('should throw error for missing title', async () => {
188
+ const supabase = createMockSupabase();
189
+ const ctx = createMockContext(supabase);
190
+ await expect(addTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)).rejects.toThrow(ValidationError);
191
+ });
192
+ it('should throw error for invalid priority', async () => {
193
+ const supabase = createMockSupabase();
194
+ const ctx = createMockContext(supabase);
195
+ await expect(addTask({
196
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
197
+ title: 'Test',
198
+ priority: 10, // Invalid: should be 1-5
199
+ }, ctx)).rejects.toThrow(ValidationError);
200
+ });
201
+ it('should throw error for invalid estimated_minutes', async () => {
202
+ const supabase = createMockSupabase();
203
+ const ctx = createMockContext(supabase);
204
+ await expect(addTask({
205
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
206
+ title: 'Test',
207
+ estimated_minutes: 0, // Invalid: must be positive
208
+ }, ctx)).rejects.toThrow(ValidationError);
209
+ });
210
+ it('should include blocking message for blocking tasks', async () => {
211
+ const supabase = createMockSupabase({
212
+ insertResult: { data: { id: 'blocking-task', blocking: true }, error: null },
213
+ });
214
+ const ctx = createMockContext(supabase);
215
+ const result = await addTask({
216
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
217
+ title: 'Blocking Task',
218
+ blocking: true,
219
+ }, ctx);
220
+ expect(result.result.blocking).toBe(true);
221
+ expect(result.result.message).toContain('BLOCKING TASK');
222
+ });
223
+ it('should use default priority when not provided', async () => {
224
+ const supabase = createMockSupabase({
225
+ insertResult: { data: { id: 'task-1', blocking: false }, error: null },
226
+ });
227
+ const ctx = createMockContext(supabase);
228
+ await addTask({
229
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
230
+ title: 'Test',
231
+ }, ctx);
232
+ // Check that insert was called with priority: 3 (default)
233
+ expect(supabase.insert).toHaveBeenCalledWith(expect.objectContaining({ priority: 3 }));
234
+ });
235
+ });
236
+ // ============================================================================
237
+ // deleteTask Tests
238
+ // ============================================================================
239
+ describe('deleteTask', () => {
240
+ beforeEach(() => {
241
+ vi.clearAllMocks();
242
+ });
243
+ it('should delete a task successfully', async () => {
244
+ const supabase = createMockSupabase({
245
+ deleteResult: { data: null, error: null },
246
+ });
247
+ const ctx = createMockContext(supabase);
248
+ const result = await deleteTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
249
+ expect(result.result.success).toBe(true);
250
+ expect(result.result.deleted_id).toBe('123e4567-e89b-12d3-a456-426614174000');
251
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
252
+ expect(supabase.delete).toHaveBeenCalled();
253
+ });
254
+ it('should throw error for missing task_id', async () => {
255
+ const supabase = createMockSupabase();
256
+ const ctx = createMockContext(supabase);
257
+ await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
258
+ });
259
+ it('should throw error for invalid task_id UUID', async () => {
260
+ const supabase = createMockSupabase();
261
+ const ctx = createMockContext(supabase);
262
+ await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
263
+ });
264
+ });
265
+ // ============================================================================
266
+ // addTaskReference Tests
267
+ // ============================================================================
268
+ describe('addTaskReference', () => {
269
+ beforeEach(() => {
270
+ vi.clearAllMocks();
271
+ });
272
+ it('should add a reference successfully', async () => {
273
+ const supabase = createMockSupabase({
274
+ selectResult: { data: { references: [] }, error: null },
275
+ updateResult: { data: null, error: null },
276
+ });
277
+ const ctx = createMockContext(supabase);
278
+ const result = await addTaskReference({
279
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
280
+ url: 'https://github.com/user/repo/pull/123',
281
+ label: 'PR #123',
282
+ }, ctx);
283
+ expect(result.result.success).toBe(true);
284
+ expect(result.result.reference).toEqual({
285
+ url: 'https://github.com/user/repo/pull/123',
286
+ label: 'PR #123',
287
+ });
288
+ expect(result.result.total_references).toBe(1);
289
+ });
290
+ it('should throw error for missing task_id', async () => {
291
+ const supabase = createMockSupabase();
292
+ const ctx = createMockContext(supabase);
293
+ await expect(addTaskReference({ url: 'https://example.com' }, ctx)).rejects.toThrow(ValidationError);
294
+ });
295
+ it('should throw error for missing url', async () => {
296
+ const supabase = createMockSupabase();
297
+ const ctx = createMockContext(supabase);
298
+ await expect(addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)).rejects.toThrow(ValidationError);
299
+ });
300
+ it('should reject duplicate URL', async () => {
301
+ const existingRef = { url: 'https://example.com', label: 'Existing' };
302
+ const supabase = createMockSupabase({
303
+ selectResult: { data: { references: [existingRef] }, error: null },
304
+ });
305
+ const ctx = createMockContext(supabase);
306
+ const result = await addTaskReference({
307
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
308
+ url: 'https://example.com',
309
+ }, ctx);
310
+ expect(result.result.success).toBe(false);
311
+ expect(result.result.error).toContain('already exists');
312
+ });
313
+ });
314
+ // ============================================================================
315
+ // removeTaskReference Tests
316
+ // ============================================================================
317
+ describe('removeTaskReference', () => {
318
+ beforeEach(() => {
319
+ vi.clearAllMocks();
320
+ });
321
+ it('should remove a reference successfully', async () => {
322
+ const existingRef = { url: 'https://example.com', label: 'Test' };
323
+ const supabase = createMockSupabase({
324
+ selectResult: { data: { references: [existingRef] }, error: null },
325
+ updateResult: { data: null, error: null },
326
+ });
327
+ const ctx = createMockContext(supabase);
328
+ const result = await removeTaskReference({
329
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
330
+ url: 'https://example.com',
331
+ }, ctx);
332
+ expect(result.result.success).toBe(true);
333
+ expect(result.result.remaining_references).toBe(0);
334
+ });
335
+ it('should return error when URL not found', async () => {
336
+ const supabase = createMockSupabase({
337
+ selectResult: { data: { references: [] }, error: null },
338
+ });
339
+ const ctx = createMockContext(supabase);
340
+ const result = await removeTaskReference({
341
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
342
+ url: 'https://nonexistent.com',
343
+ }, ctx);
344
+ expect(result.result.success).toBe(false);
345
+ expect(result.result.error).toContain('not found');
346
+ });
347
+ });
348
+ // ============================================================================
349
+ // getProjectGitConfig Tests
350
+ // ============================================================================
351
+ describe('getProjectGitConfig', () => {
352
+ it('should return git config for a project', async () => {
353
+ const mockConfig = {
354
+ git_workflow: 'github-flow',
355
+ git_main_branch: 'main',
356
+ git_develop_branch: null,
357
+ git_auto_branch: true,
358
+ };
359
+ const supabase = createMockSupabase({
360
+ selectResult: { data: mockConfig, error: null },
361
+ });
362
+ const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
363
+ expect(result).toEqual(mockConfig);
364
+ expect(supabase.from).toHaveBeenCalledWith('projects');
365
+ });
366
+ it('should return null when project not found', async () => {
367
+ const supabase = createMockSupabase({
368
+ selectResult: { data: null, error: { message: 'Not found' } },
369
+ });
370
+ const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
371
+ expect(result).toBeNull();
372
+ });
373
+ });
374
+ // ============================================================================
375
+ // completeTask Tests
376
+ // ============================================================================
377
+ describe('completeTask', () => {
378
+ beforeEach(() => {
379
+ vi.clearAllMocks();
380
+ });
381
+ it('should throw error for missing task_id', async () => {
382
+ const supabase = createMockSupabase();
383
+ const ctx = createMockContext(supabase);
384
+ await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
385
+ });
386
+ it('should throw error for invalid task_id UUID', async () => {
387
+ const supabase = createMockSupabase();
388
+ const ctx = createMockContext(supabase);
389
+ await expect(completeTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
390
+ });
391
+ it('should throw error when task not found', async () => {
392
+ const supabase = createMockSupabase({
393
+ selectResult: { data: null, error: { message: 'Not found' } },
394
+ });
395
+ const ctx = createMockContext(supabase);
396
+ await expect(completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)).rejects.toThrow('Task not found');
397
+ });
398
+ });
399
+ // ============================================================================
400
+ // updateTask Tests
401
+ // ============================================================================
402
+ describe('updateTask', () => {
403
+ beforeEach(() => {
404
+ vi.clearAllMocks();
405
+ });
406
+ it('should throw error for missing task_id', async () => {
407
+ const supabase = createMockSupabase();
408
+ const ctx = createMockContext(supabase);
409
+ await expect(updateTask({}, ctx)).rejects.toThrow(ValidationError);
410
+ });
411
+ it('should throw error for invalid task_id UUID', async () => {
412
+ const supabase = createMockSupabase();
413
+ const ctx = createMockContext(supabase);
414
+ await expect(updateTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
415
+ });
416
+ it('should throw error for invalid status', async () => {
417
+ const supabase = createMockSupabase();
418
+ const ctx = createMockContext(supabase);
419
+ await expect(updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
420
+ });
421
+ it('should throw error for invalid priority', async () => {
422
+ const supabase = createMockSupabase();
423
+ const ctx = createMockContext(supabase);
424
+ await expect(updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', priority: 0 }, ctx)).rejects.toThrow(ValidationError);
425
+ });
426
+ it('should throw error for invalid progress_percentage', async () => {
427
+ const supabase = createMockSupabase();
428
+ const ctx = createMockContext(supabase);
429
+ await expect(updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', progress_percentage: 150 }, ctx)).rejects.toThrow(ValidationError);
430
+ });
431
+ });
@@ -0,0 +1,94 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ /**
3
+ * Authentication context from API key validation
4
+ */
5
+ export interface AuthContext {
6
+ userId: string;
7
+ apiKeyId: string;
8
+ organizationId?: string;
9
+ scope: 'personal' | 'organization';
10
+ }
11
+ /**
12
+ * Model-specific token tracking
13
+ */
14
+ export interface ModelTokens {
15
+ input: number;
16
+ output: number;
17
+ }
18
+ /**
19
+ * Token usage tracking for the session
20
+ */
21
+ export interface TokenUsage {
22
+ callCount: number;
23
+ totalTokens: number;
24
+ byTool: Record<string, {
25
+ calls: number;
26
+ tokens: number;
27
+ }>;
28
+ byModel: Record<string, ModelTokens>;
29
+ currentModel: string | null;
30
+ }
31
+ /**
32
+ * Session state that persists across tool calls
33
+ */
34
+ export interface SessionState {
35
+ instanceId: string;
36
+ currentSessionId: string | null;
37
+ currentPersona: string | null;
38
+ tokenUsage: TokenUsage;
39
+ }
40
+ /**
41
+ * User updates since last sync (for start_work_session full mode)
42
+ */
43
+ export interface UserUpdates {
44
+ tasks?: Array<{
45
+ id: string;
46
+ title: string;
47
+ created_at: string;
48
+ }>;
49
+ blockers?: Array<{
50
+ id: string;
51
+ description: string;
52
+ created_at: string;
53
+ }>;
54
+ ideas?: Array<{
55
+ id: string;
56
+ title: string;
57
+ created_at: string;
58
+ }>;
59
+ }
60
+ /**
61
+ * Context passed to all handlers
62
+ */
63
+ export interface HandlerContext {
64
+ supabase: SupabaseClient;
65
+ auth: AuthContext;
66
+ session: SessionState;
67
+ /** Update session state (for handlers that modify session) */
68
+ updateSession: (updates: Partial<Pick<SessionState, 'currentSessionId' | 'currentPersona' | 'tokenUsage'>>) => void;
69
+ /** Get user updates since last sync (for session handlers) */
70
+ getUserUpdates?: (projectId: string) => Promise<UserUpdates | undefined>;
71
+ /** Select an available persona for the agent */
72
+ selectPersona?: (usedPersonas: Set<string>, instanceId: string) => string;
73
+ /** Extract project name from git URL */
74
+ extractProjectNameFromGitUrl?: (gitUrl: string) => string;
75
+ }
76
+ /**
77
+ * Result returned by handlers
78
+ */
79
+ export interface HandlerResult {
80
+ result?: unknown;
81
+ content?: Array<{
82
+ type: string;
83
+ text: string;
84
+ }>;
85
+ isError?: boolean;
86
+ }
87
+ /**
88
+ * Handler function type
89
+ */
90
+ export type Handler = (args: Record<string, unknown>, ctx: HandlerContext) => Promise<HandlerResult>;
91
+ /**
92
+ * Handler registry - maps tool names to handler functions
93
+ */
94
+ export type HandlerRegistry = Record<string, Handler>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Validation Handlers
3
+ *
4
+ * Handles cross-agent task validation:
5
+ * - get_tasks_awaiting_validation
6
+ * - claim_validation
7
+ * - validate_task
8
+ */
9
+ import type { Handler, HandlerRegistry } from './types.js';
10
+ export declare const getTasksAwaitingValidation: Handler;
11
+ export declare const claimValidation: Handler;
12
+ export declare const validateTask: Handler;
13
+ /**
14
+ * Validation handlers registry
15
+ */
16
+ export declare const validationHandlers: HandlerRegistry;