@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,602 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import type { HandlerContext } from './types.js';
4
+ import {
5
+ getTasks,
6
+ addTask,
7
+ updateTask,
8
+ completeTask,
9
+ deleteTask,
10
+ addTaskReference,
11
+ removeTaskReference,
12
+ getProjectGitConfig,
13
+ } from './tasks.js';
14
+ import { ValidationError } from '../validators.js';
15
+
16
+ // ============================================================================
17
+ // Test Utilities
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Creates a mock Supabase client with chainable methods
22
+ */
23
+ function createMockSupabase(overrides: {
24
+ selectResult?: { data: unknown; error: unknown };
25
+ insertResult?: { data: unknown; error: unknown };
26
+ updateResult?: { data: unknown; error: unknown };
27
+ deleteResult?: { data: unknown; error: unknown };
28
+ } = {}) {
29
+ const defaultResult = { data: null, error: null };
30
+
31
+ // Track both the operation type AND if insert has been followed by select
32
+ let currentOperation = 'select';
33
+ let insertThenSelect = false;
34
+
35
+ const mock = {
36
+ from: vi.fn().mockReturnThis(),
37
+ select: vi.fn(() => {
38
+ // If we just did an insert and now calling select, it's insert().select() chain
39
+ if (currentOperation === 'insert') {
40
+ insertThenSelect = true;
41
+ } else {
42
+ currentOperation = 'select';
43
+ insertThenSelect = false;
44
+ }
45
+ return mock;
46
+ }),
47
+ insert: vi.fn(() => {
48
+ currentOperation = 'insert';
49
+ insertThenSelect = false;
50
+ return mock;
51
+ }),
52
+ update: vi.fn(() => {
53
+ currentOperation = 'update';
54
+ insertThenSelect = false;
55
+ return mock;
56
+ }),
57
+ delete: vi.fn(() => {
58
+ currentOperation = 'delete';
59
+ insertThenSelect = false;
60
+ return mock;
61
+ }),
62
+ eq: vi.fn().mockReturnThis(),
63
+ neq: vi.fn().mockReturnThis(),
64
+ in: vi.fn().mockReturnThis(),
65
+ is: vi.fn().mockReturnThis(),
66
+ not: vi.fn().mockReturnThis(),
67
+ or: vi.fn().mockReturnThis(),
68
+ lt: vi.fn().mockReturnThis(),
69
+ order: vi.fn().mockReturnThis(),
70
+ limit: vi.fn().mockReturnThis(),
71
+ single: vi.fn(() => {
72
+ // Handle insert().select().single() pattern
73
+ if (currentOperation === 'insert' || insertThenSelect) {
74
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
75
+ }
76
+ if (currentOperation === 'select') {
77
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
78
+ }
79
+ if (currentOperation === 'update') {
80
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
81
+ }
82
+ return Promise.resolve(defaultResult);
83
+ }),
84
+ maybeSingle: vi.fn(() => {
85
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
86
+ }),
87
+ then: vi.fn((resolve: (value: unknown) => void) => {
88
+ if (currentOperation === 'insert' || insertThenSelect) {
89
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
90
+ }
91
+ if (currentOperation === 'select') {
92
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
93
+ }
94
+ if (currentOperation === 'update') {
95
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
96
+ }
97
+ if (currentOperation === 'delete') {
98
+ return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
99
+ }
100
+ return Promise.resolve(defaultResult).then(resolve);
101
+ }),
102
+ };
103
+
104
+ return mock as unknown as SupabaseClient;
105
+ }
106
+
107
+ /**
108
+ * Creates a mock handler context
109
+ */
110
+ function createMockContext(supabase: SupabaseClient, sessionId: string | null = 'session-123'): HandlerContext {
111
+ return {
112
+ supabase,
113
+ auth: {
114
+ userId: 'user-123',
115
+ apiKeyId: 'api-key-123',
116
+ },
117
+ session: {
118
+ currentSessionId: sessionId,
119
+ },
120
+ };
121
+ }
122
+
123
+ // ============================================================================
124
+ // getTasks Tests
125
+ // ============================================================================
126
+
127
+ describe('getTasks', () => {
128
+ beforeEach(() => {
129
+ vi.clearAllMocks();
130
+ });
131
+
132
+ it('should return tasks for a valid project', async () => {
133
+ const mockTasks = [
134
+ { id: 'task-1', title: 'Task 1', status: 'pending', priority: 1 },
135
+ { id: 'task-2', title: 'Task 2', status: 'in_progress', priority: 2 },
136
+ ];
137
+
138
+ const supabase = createMockSupabase({
139
+ selectResult: { data: mockTasks, error: null },
140
+ });
141
+ const ctx = createMockContext(supabase);
142
+
143
+ const result = await getTasks(
144
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
145
+ ctx
146
+ );
147
+
148
+ expect(result.result.tasks).toEqual(mockTasks);
149
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
150
+ });
151
+
152
+ it('should throw error for missing project_id', async () => {
153
+ const supabase = createMockSupabase();
154
+ const ctx = createMockContext(supabase);
155
+
156
+ await expect(getTasks({}, ctx)).rejects.toThrow(ValidationError);
157
+ });
158
+
159
+ it('should throw error for invalid project_id UUID', async () => {
160
+ const supabase = createMockSupabase();
161
+ const ctx = createMockContext(supabase);
162
+
163
+ await expect(getTasks({ project_id: 'not-a-uuid' }, ctx)).rejects.toThrow(ValidationError);
164
+ });
165
+
166
+ it('should filter by status when provided', async () => {
167
+ const mockTasks = [{ id: 'task-1', title: 'Task 1', status: 'pending', priority: 1 }];
168
+
169
+ const supabase = createMockSupabase({
170
+ selectResult: { data: mockTasks, error: null },
171
+ });
172
+ const ctx = createMockContext(supabase);
173
+
174
+ await getTasks(
175
+ { project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'pending' },
176
+ ctx
177
+ );
178
+
179
+ expect(supabase.eq).toHaveBeenCalledWith('status', 'pending');
180
+ });
181
+
182
+ it('should throw error for invalid status', async () => {
183
+ const supabase = createMockSupabase();
184
+ const ctx = createMockContext(supabase);
185
+
186
+ await expect(
187
+ getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)
188
+ ).rejects.toThrow(ValidationError);
189
+ });
190
+
191
+ it('should handle database errors', async () => {
192
+ const supabase = createMockSupabase({
193
+ selectResult: { data: null, error: { message: 'Database error' } },
194
+ });
195
+ const ctx = createMockContext(supabase);
196
+
197
+ // The handler uses .then() pattern, so we need to adjust the mock
198
+ vi.mocked(supabase.from('tasks').select).mockReturnValue({
199
+ ...supabase,
200
+ then: (resolve: (val: unknown) => void) =>
201
+ Promise.resolve({ data: null, error: { message: 'Database error' } }).then(resolve),
202
+ } as unknown as ReturnType<SupabaseClient['from']>);
203
+
204
+ await expect(
205
+ getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
206
+ ).rejects.toThrow('Failed to fetch tasks');
207
+ });
208
+ });
209
+
210
+ // ============================================================================
211
+ // addTask Tests
212
+ // ============================================================================
213
+
214
+ describe('addTask', () => {
215
+ beforeEach(() => {
216
+ vi.clearAllMocks();
217
+ });
218
+
219
+ it('should add a task successfully', async () => {
220
+ const supabase = createMockSupabase({
221
+ insertResult: { data: { id: 'new-task-123', blocking: false }, error: null },
222
+ });
223
+ const ctx = createMockContext(supabase);
224
+
225
+ const result = await addTask(
226
+ {
227
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
228
+ title: 'New Task',
229
+ description: 'Task description',
230
+ priority: 2,
231
+ },
232
+ ctx
233
+ );
234
+
235
+ expect(result.result.success).toBe(true);
236
+ expect(result.result.task_id).toBe('new-task-123');
237
+ expect(result.result.title).toBe('New Task');
238
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
239
+ expect(supabase.insert).toHaveBeenCalled();
240
+ });
241
+
242
+ it('should throw error for missing project_id', async () => {
243
+ const supabase = createMockSupabase();
244
+ const ctx = createMockContext(supabase);
245
+
246
+ await expect(addTask({ title: 'Test' }, ctx)).rejects.toThrow(ValidationError);
247
+ });
248
+
249
+ it('should throw error for missing title', async () => {
250
+ const supabase = createMockSupabase();
251
+ const ctx = createMockContext(supabase);
252
+
253
+ await expect(
254
+ addTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
255
+ ).rejects.toThrow(ValidationError);
256
+ });
257
+
258
+ it('should throw error for invalid priority', async () => {
259
+ const supabase = createMockSupabase();
260
+ const ctx = createMockContext(supabase);
261
+
262
+ await expect(
263
+ addTask({
264
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
265
+ title: 'Test',
266
+ priority: 10, // Invalid: should be 1-5
267
+ }, ctx)
268
+ ).rejects.toThrow(ValidationError);
269
+ });
270
+
271
+ it('should throw error for invalid estimated_minutes', async () => {
272
+ const supabase = createMockSupabase();
273
+ const ctx = createMockContext(supabase);
274
+
275
+ await expect(
276
+ addTask({
277
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
278
+ title: 'Test',
279
+ estimated_minutes: 0, // Invalid: must be positive
280
+ }, ctx)
281
+ ).rejects.toThrow(ValidationError);
282
+ });
283
+
284
+ it('should include blocking message for blocking tasks', async () => {
285
+ const supabase = createMockSupabase({
286
+ insertResult: { data: { id: 'blocking-task', blocking: true }, error: null },
287
+ });
288
+ const ctx = createMockContext(supabase);
289
+
290
+ const result = await addTask(
291
+ {
292
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
293
+ title: 'Blocking Task',
294
+ blocking: true,
295
+ },
296
+ ctx
297
+ );
298
+
299
+ expect(result.result.blocking).toBe(true);
300
+ expect(result.result.message).toContain('BLOCKING TASK');
301
+ });
302
+
303
+ it('should use default priority when not provided', async () => {
304
+ const supabase = createMockSupabase({
305
+ insertResult: { data: { id: 'task-1', blocking: false }, error: null },
306
+ });
307
+ const ctx = createMockContext(supabase);
308
+
309
+ await addTask(
310
+ {
311
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
312
+ title: 'Test',
313
+ },
314
+ ctx
315
+ );
316
+
317
+ // Check that insert was called with priority: 3 (default)
318
+ expect(supabase.insert).toHaveBeenCalledWith(
319
+ expect.objectContaining({ priority: 3 })
320
+ );
321
+ });
322
+ });
323
+
324
+ // ============================================================================
325
+ // deleteTask Tests
326
+ // ============================================================================
327
+
328
+ describe('deleteTask', () => {
329
+ beforeEach(() => {
330
+ vi.clearAllMocks();
331
+ });
332
+
333
+ it('should delete a task successfully', async () => {
334
+ const supabase = createMockSupabase({
335
+ deleteResult: { data: null, error: null },
336
+ });
337
+ const ctx = createMockContext(supabase);
338
+
339
+ const result = await deleteTask(
340
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
341
+ ctx
342
+ );
343
+
344
+ expect(result.result.success).toBe(true);
345
+ expect(result.result.deleted_id).toBe('123e4567-e89b-12d3-a456-426614174000');
346
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
347
+ expect(supabase.delete).toHaveBeenCalled();
348
+ });
349
+
350
+ it('should throw error for missing task_id', async () => {
351
+ const supabase = createMockSupabase();
352
+ const ctx = createMockContext(supabase);
353
+
354
+ await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
355
+ });
356
+
357
+ it('should throw error for invalid task_id UUID', async () => {
358
+ const supabase = createMockSupabase();
359
+ const ctx = createMockContext(supabase);
360
+
361
+ await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
362
+ });
363
+ });
364
+
365
+ // ============================================================================
366
+ // addTaskReference Tests
367
+ // ============================================================================
368
+
369
+ describe('addTaskReference', () => {
370
+ beforeEach(() => {
371
+ vi.clearAllMocks();
372
+ });
373
+
374
+ it('should add a reference successfully', async () => {
375
+ const supabase = createMockSupabase({
376
+ selectResult: { data: { references: [] }, error: null },
377
+ updateResult: { data: null, error: null },
378
+ });
379
+ const ctx = createMockContext(supabase);
380
+
381
+ const result = await addTaskReference(
382
+ {
383
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
384
+ url: 'https://github.com/user/repo/pull/123',
385
+ label: 'PR #123',
386
+ },
387
+ ctx
388
+ );
389
+
390
+ expect(result.result.success).toBe(true);
391
+ expect(result.result.reference).toEqual({
392
+ url: 'https://github.com/user/repo/pull/123',
393
+ label: 'PR #123',
394
+ });
395
+ expect(result.result.total_references).toBe(1);
396
+ });
397
+
398
+ it('should throw error for missing task_id', async () => {
399
+ const supabase = createMockSupabase();
400
+ const ctx = createMockContext(supabase);
401
+
402
+ await expect(
403
+ addTaskReference({ url: 'https://example.com' }, ctx)
404
+ ).rejects.toThrow(ValidationError);
405
+ });
406
+
407
+ it('should throw error for missing url', async () => {
408
+ const supabase = createMockSupabase();
409
+ const ctx = createMockContext(supabase);
410
+
411
+ await expect(
412
+ addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
413
+ ).rejects.toThrow(ValidationError);
414
+ });
415
+
416
+ it('should reject duplicate URL', async () => {
417
+ const existingRef = { url: 'https://example.com', label: 'Existing' };
418
+ const supabase = createMockSupabase({
419
+ selectResult: { data: { references: [existingRef] }, error: null },
420
+ });
421
+ const ctx = createMockContext(supabase);
422
+
423
+ const result = await addTaskReference(
424
+ {
425
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
426
+ url: 'https://example.com',
427
+ },
428
+ ctx
429
+ );
430
+
431
+ expect(result.result.success).toBe(false);
432
+ expect(result.result.error).toContain('already exists');
433
+ });
434
+ });
435
+
436
+ // ============================================================================
437
+ // removeTaskReference Tests
438
+ // ============================================================================
439
+
440
+ describe('removeTaskReference', () => {
441
+ beforeEach(() => {
442
+ vi.clearAllMocks();
443
+ });
444
+
445
+ it('should remove a reference successfully', async () => {
446
+ const existingRef = { url: 'https://example.com', label: 'Test' };
447
+ const supabase = createMockSupabase({
448
+ selectResult: { data: { references: [existingRef] }, error: null },
449
+ updateResult: { data: null, error: null },
450
+ });
451
+ const ctx = createMockContext(supabase);
452
+
453
+ const result = await removeTaskReference(
454
+ {
455
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
456
+ url: 'https://example.com',
457
+ },
458
+ ctx
459
+ );
460
+
461
+ expect(result.result.success).toBe(true);
462
+ expect(result.result.remaining_references).toBe(0);
463
+ });
464
+
465
+ it('should return error when URL not found', async () => {
466
+ const supabase = createMockSupabase({
467
+ selectResult: { data: { references: [] }, error: null },
468
+ });
469
+ const ctx = createMockContext(supabase);
470
+
471
+ const result = await removeTaskReference(
472
+ {
473
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
474
+ url: 'https://nonexistent.com',
475
+ },
476
+ ctx
477
+ );
478
+
479
+ expect(result.result.success).toBe(false);
480
+ expect(result.result.error).toContain('not found');
481
+ });
482
+ });
483
+
484
+ // ============================================================================
485
+ // getProjectGitConfig Tests
486
+ // ============================================================================
487
+
488
+ describe('getProjectGitConfig', () => {
489
+ it('should return git config for a project', async () => {
490
+ const mockConfig = {
491
+ git_workflow: 'github-flow',
492
+ git_main_branch: 'main',
493
+ git_develop_branch: null,
494
+ git_auto_branch: true,
495
+ };
496
+
497
+ const supabase = createMockSupabase({
498
+ selectResult: { data: mockConfig, error: null },
499
+ });
500
+
501
+ const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
502
+
503
+ expect(result).toEqual(mockConfig);
504
+ expect(supabase.from).toHaveBeenCalledWith('projects');
505
+ });
506
+
507
+ it('should return null when project not found', async () => {
508
+ const supabase = createMockSupabase({
509
+ selectResult: { data: null, error: { message: 'Not found' } },
510
+ });
511
+
512
+ const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
513
+
514
+ expect(result).toBeNull();
515
+ });
516
+ });
517
+
518
+ // ============================================================================
519
+ // completeTask Tests
520
+ // ============================================================================
521
+
522
+ describe('completeTask', () => {
523
+ beforeEach(() => {
524
+ vi.clearAllMocks();
525
+ });
526
+
527
+ it('should throw error for missing task_id', async () => {
528
+ const supabase = createMockSupabase();
529
+ const ctx = createMockContext(supabase);
530
+
531
+ await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
532
+ });
533
+
534
+ it('should throw error for invalid task_id UUID', async () => {
535
+ const supabase = createMockSupabase();
536
+ const ctx = createMockContext(supabase);
537
+
538
+ await expect(completeTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
539
+ });
540
+
541
+ it('should throw error when task not found', async () => {
542
+ const supabase = createMockSupabase({
543
+ selectResult: { data: null, error: { message: 'Not found' } },
544
+ });
545
+ const ctx = createMockContext(supabase);
546
+
547
+ await expect(
548
+ completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
549
+ ).rejects.toThrow('Task not found');
550
+ });
551
+ });
552
+
553
+ // ============================================================================
554
+ // updateTask Tests
555
+ // ============================================================================
556
+
557
+ describe('updateTask', () => {
558
+ beforeEach(() => {
559
+ vi.clearAllMocks();
560
+ });
561
+
562
+ it('should throw error for missing task_id', async () => {
563
+ const supabase = createMockSupabase();
564
+ const ctx = createMockContext(supabase);
565
+
566
+ await expect(updateTask({}, ctx)).rejects.toThrow(ValidationError);
567
+ });
568
+
569
+ it('should throw error for invalid task_id UUID', async () => {
570
+ const supabase = createMockSupabase();
571
+ const ctx = createMockContext(supabase);
572
+
573
+ await expect(updateTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
574
+ });
575
+
576
+ it('should throw error for invalid status', async () => {
577
+ const supabase = createMockSupabase();
578
+ const ctx = createMockContext(supabase);
579
+
580
+ await expect(
581
+ updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)
582
+ ).rejects.toThrow(ValidationError);
583
+ });
584
+
585
+ it('should throw error for invalid priority', async () => {
586
+ const supabase = createMockSupabase();
587
+ const ctx = createMockContext(supabase);
588
+
589
+ await expect(
590
+ updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', priority: 0 }, ctx)
591
+ ).rejects.toThrow(ValidationError);
592
+ });
593
+
594
+ it('should throw error for invalid progress_percentage', async () => {
595
+ const supabase = createMockSupabase();
596
+ const ctx = createMockContext(supabase);
597
+
598
+ await expect(
599
+ updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', progress_percentage: 150 }, ctx)
600
+ ).rejects.toThrow(ValidationError);
601
+ });
602
+ });