@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,584 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import type { HandlerContext, TokenUsage } from './types.js';
4
+ import {
5
+ addMilestone,
6
+ updateMilestone,
7
+ completeMilestone,
8
+ deleteMilestone,
9
+ getMilestones,
10
+ } from './milestones.js';
11
+ import { ValidationError } from '../validators.js';
12
+
13
+ // ============================================================================
14
+ // Test Utilities
15
+ // ============================================================================
16
+
17
+ function createMockSupabase(overrides: {
18
+ selectResult?: { data: unknown; error: unknown };
19
+ insertResult?: { data: unknown; error: unknown };
20
+ updateResult?: { data: unknown; error: unknown };
21
+ deleteResult?: { data: unknown; error: unknown };
22
+ } = {}) {
23
+ const defaultResult = { data: null, error: null };
24
+ let currentOperation = 'select';
25
+ let insertThenSelect = false;
26
+
27
+ const mock = {
28
+ from: vi.fn().mockReturnThis(),
29
+ select: vi.fn(() => {
30
+ if (currentOperation === 'insert' || currentOperation === 'update') {
31
+ insertThenSelect = true;
32
+ } else {
33
+ currentOperation = 'select';
34
+ insertThenSelect = false;
35
+ }
36
+ return mock;
37
+ }),
38
+ insert: vi.fn(() => {
39
+ currentOperation = 'insert';
40
+ insertThenSelect = false;
41
+ return mock;
42
+ }),
43
+ update: vi.fn(() => {
44
+ currentOperation = 'update';
45
+ insertThenSelect = false;
46
+ return mock;
47
+ }),
48
+ delete: vi.fn(() => {
49
+ currentOperation = 'delete';
50
+ insertThenSelect = false;
51
+ return mock;
52
+ }),
53
+ eq: vi.fn().mockReturnThis(),
54
+ neq: vi.fn().mockReturnThis(),
55
+ in: vi.fn().mockReturnThis(),
56
+ is: vi.fn().mockReturnThis(),
57
+ not: vi.fn().mockReturnThis(),
58
+ or: vi.fn().mockReturnThis(),
59
+ gt: vi.fn().mockReturnThis(),
60
+ gte: vi.fn().mockReturnThis(),
61
+ lte: vi.fn().mockReturnThis(),
62
+ lt: vi.fn().mockReturnThis(),
63
+ order: vi.fn().mockReturnThis(),
64
+ limit: vi.fn().mockReturnThis(),
65
+ single: vi.fn(() => {
66
+ if (currentOperation === 'insert' || insertThenSelect) {
67
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
68
+ }
69
+ if (currentOperation === 'select') {
70
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
71
+ }
72
+ if (currentOperation === 'update') {
73
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
74
+ }
75
+ return Promise.resolve(defaultResult);
76
+ }),
77
+ maybeSingle: vi.fn(() => {
78
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
79
+ }),
80
+ then: vi.fn((resolve: (value: unknown) => void) => {
81
+ if (currentOperation === 'insert' || insertThenSelect) {
82
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
83
+ }
84
+ if (currentOperation === 'select') {
85
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
86
+ }
87
+ if (currentOperation === 'update') {
88
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
89
+ }
90
+ if (currentOperation === 'delete') {
91
+ return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
92
+ }
93
+ return Promise.resolve(defaultResult).then(resolve);
94
+ }),
95
+ };
96
+
97
+ return mock as unknown as SupabaseClient;
98
+ }
99
+
100
+ function createMockContext(
101
+ supabase: SupabaseClient,
102
+ options: { sessionId?: string | null } = {}
103
+ ): HandlerContext {
104
+ const defaultTokenUsage: TokenUsage = {
105
+ callCount: 5,
106
+ totalTokens: 2500,
107
+ byTool: {},
108
+ };
109
+
110
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
111
+
112
+ return {
113
+ supabase,
114
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
115
+ session: {
116
+ instanceId: 'instance-abc',
117
+ currentSessionId: sessionId,
118
+ currentPersona: 'Wave',
119
+ tokenUsage: defaultTokenUsage,
120
+ },
121
+ updateSession: vi.fn(),
122
+ };
123
+ }
124
+
125
+ // ============================================================================
126
+ // addMilestone Tests
127
+ // ============================================================================
128
+
129
+ describe('addMilestone', () => {
130
+ beforeEach(() => vi.clearAllMocks());
131
+
132
+ it('should throw error for missing task_id', async () => {
133
+ const supabase = createMockSupabase();
134
+ const ctx = createMockContext(supabase);
135
+
136
+ await expect(
137
+ addMilestone({ title: 'Milestone 1' }, ctx)
138
+ ).rejects.toThrow(ValidationError);
139
+ });
140
+
141
+ it('should throw error for invalid task_id UUID', async () => {
142
+ const supabase = createMockSupabase();
143
+ const ctx = createMockContext(supabase);
144
+
145
+ await expect(
146
+ addMilestone({ task_id: 'invalid', title: 'Milestone 1' }, ctx)
147
+ ).rejects.toThrow(ValidationError);
148
+ });
149
+
150
+ it('should throw error for missing title', async () => {
151
+ const supabase = createMockSupabase();
152
+ const ctx = createMockContext(supabase);
153
+
154
+ await expect(
155
+ addMilestone({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
156
+ ).rejects.toThrow(ValidationError);
157
+ });
158
+
159
+ it('should throw error when task not found', async () => {
160
+ const supabase = createMockSupabase({
161
+ selectResult: { data: null, error: { message: 'Not found' } },
162
+ });
163
+ const ctx = createMockContext(supabase);
164
+
165
+ await expect(
166
+ addMilestone({
167
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
168
+ title: 'Milestone 1',
169
+ }, ctx)
170
+ ).rejects.toThrow('Task not found');
171
+ });
172
+
173
+ it('should add milestone successfully', async () => {
174
+ const mockMilestone = {
175
+ id: 'milestone-1',
176
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
177
+ title: 'First Milestone',
178
+ order_index: 0,
179
+ status: 'pending',
180
+ };
181
+
182
+ const supabase = createMockSupabase({
183
+ selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
184
+ insertResult: { data: mockMilestone, error: null },
185
+ });
186
+ const ctx = createMockContext(supabase);
187
+
188
+ const result = await addMilestone(
189
+ {
190
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
191
+ title: 'First Milestone',
192
+ },
193
+ ctx
194
+ );
195
+
196
+ expect(result.result).toMatchObject({
197
+ success: true,
198
+ milestone: mockMilestone,
199
+ });
200
+ });
201
+
202
+ it('should use provided order_index', async () => {
203
+ const supabase = createMockSupabase({
204
+ selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
205
+ insertResult: { data: { id: 'milestone-1', order_index: 5 }, error: null },
206
+ });
207
+ const ctx = createMockContext(supabase);
208
+
209
+ await addMilestone(
210
+ {
211
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
212
+ title: 'Test Milestone',
213
+ order_index: 5,
214
+ },
215
+ ctx
216
+ );
217
+
218
+ expect(supabase.insert).toHaveBeenCalledWith(
219
+ expect.objectContaining({
220
+ order_index: 5,
221
+ })
222
+ );
223
+ });
224
+
225
+ it('should include session_id in insert', async () => {
226
+ const supabase = createMockSupabase({
227
+ selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
228
+ insertResult: { data: { id: 'milestone-1' }, error: null },
229
+ });
230
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
231
+
232
+ await addMilestone(
233
+ {
234
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
235
+ title: 'Test',
236
+ },
237
+ ctx
238
+ );
239
+
240
+ expect(supabase.insert).toHaveBeenCalledWith(
241
+ expect.objectContaining({
242
+ created_by: 'agent',
243
+ created_by_session_id: 'my-session',
244
+ })
245
+ );
246
+ });
247
+ });
248
+
249
+ // ============================================================================
250
+ // updateMilestone Tests
251
+ // ============================================================================
252
+
253
+ describe('updateMilestone', () => {
254
+ beforeEach(() => vi.clearAllMocks());
255
+
256
+ it('should throw error for missing milestone_id', async () => {
257
+ const supabase = createMockSupabase();
258
+ const ctx = createMockContext(supabase);
259
+
260
+ await expect(updateMilestone({}, ctx)).rejects.toThrow(ValidationError);
261
+ });
262
+
263
+ it('should throw error for invalid milestone_id UUID', async () => {
264
+ const supabase = createMockSupabase();
265
+ const ctx = createMockContext(supabase);
266
+
267
+ await expect(
268
+ updateMilestone({ milestone_id: 'invalid', title: 'New Title' }, ctx)
269
+ ).rejects.toThrow(ValidationError);
270
+ });
271
+
272
+ it('should throw error when no fields to update', async () => {
273
+ const supabase = createMockSupabase();
274
+ const ctx = createMockContext(supabase);
275
+
276
+ await expect(
277
+ updateMilestone({ milestone_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
278
+ ).rejects.toThrow('At least one field to update is required');
279
+ });
280
+
281
+ it('should throw error for invalid status', async () => {
282
+ const supabase = createMockSupabase();
283
+ const ctx = createMockContext(supabase);
284
+
285
+ await expect(
286
+ updateMilestone({
287
+ milestone_id: '123e4567-e89b-12d3-a456-426614174000',
288
+ status: 'invalid_status',
289
+ }, ctx)
290
+ ).rejects.toThrow('status must be pending, in_progress, or completed');
291
+ });
292
+
293
+ it('should update title successfully', async () => {
294
+ const supabase = createMockSupabase({
295
+ updateResult: { data: { id: 'milestone-1', title: 'Updated Title' }, error: null },
296
+ });
297
+ const ctx = createMockContext(supabase);
298
+
299
+ const result = await updateMilestone(
300
+ {
301
+ milestone_id: '123e4567-e89b-12d3-a456-426614174000',
302
+ title: 'Updated Title',
303
+ },
304
+ ctx
305
+ );
306
+
307
+ expect(result.result).toMatchObject({
308
+ success: true,
309
+ });
310
+ });
311
+
312
+ it('should set completed_at when status is completed', async () => {
313
+ const supabase = createMockSupabase({
314
+ updateResult: { data: { id: 'milestone-1', status: 'completed' }, error: null },
315
+ });
316
+ const ctx = createMockContext(supabase);
317
+
318
+ await updateMilestone(
319
+ {
320
+ milestone_id: '123e4567-e89b-12d3-a456-426614174000',
321
+ status: 'completed',
322
+ },
323
+ ctx
324
+ );
325
+
326
+ expect(supabase.update).toHaveBeenCalledWith(
327
+ expect.objectContaining({
328
+ status: 'completed',
329
+ completed_at: expect.any(String),
330
+ })
331
+ );
332
+ });
333
+
334
+ it('should clear completed_at when status is not completed', async () => {
335
+ const supabase = createMockSupabase({
336
+ updateResult: { data: { id: 'milestone-1', status: 'in_progress' }, error: null },
337
+ });
338
+ const ctx = createMockContext(supabase);
339
+
340
+ await updateMilestone(
341
+ {
342
+ milestone_id: '123e4567-e89b-12d3-a456-426614174000',
343
+ status: 'in_progress',
344
+ },
345
+ ctx
346
+ );
347
+
348
+ expect(supabase.update).toHaveBeenCalledWith(
349
+ expect.objectContaining({
350
+ status: 'in_progress',
351
+ completed_at: null,
352
+ })
353
+ );
354
+ });
355
+ });
356
+
357
+ // ============================================================================
358
+ // completeMilestone Tests
359
+ // ============================================================================
360
+
361
+ describe('completeMilestone', () => {
362
+ beforeEach(() => vi.clearAllMocks());
363
+
364
+ it('should throw error for missing milestone_id', async () => {
365
+ const supabase = createMockSupabase();
366
+ const ctx = createMockContext(supabase);
367
+
368
+ await expect(completeMilestone({}, ctx)).rejects.toThrow(ValidationError);
369
+ });
370
+
371
+ it('should throw error for invalid milestone_id UUID', async () => {
372
+ const supabase = createMockSupabase();
373
+ const ctx = createMockContext(supabase);
374
+
375
+ await expect(
376
+ completeMilestone({ milestone_id: 'invalid' }, ctx)
377
+ ).rejects.toThrow(ValidationError);
378
+ });
379
+
380
+ it('should complete milestone successfully', async () => {
381
+ const supabase = createMockSupabase({
382
+ updateResult: {
383
+ data: { id: 'milestone-1', status: 'completed', completed_at: '2025-01-14T12:00:00Z' },
384
+ error: null,
385
+ },
386
+ });
387
+ const ctx = createMockContext(supabase);
388
+
389
+ const result = await completeMilestone(
390
+ { milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
391
+ ctx
392
+ );
393
+
394
+ expect(result.result).toMatchObject({
395
+ success: true,
396
+ });
397
+ });
398
+
399
+ it('should set status to completed with timestamp', async () => {
400
+ const supabase = createMockSupabase({
401
+ updateResult: { data: { id: 'milestone-1' }, error: null },
402
+ });
403
+ const ctx = createMockContext(supabase);
404
+
405
+ await completeMilestone(
406
+ { milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
407
+ ctx
408
+ );
409
+
410
+ expect(supabase.update).toHaveBeenCalledWith(
411
+ expect.objectContaining({
412
+ status: 'completed',
413
+ completed_at: expect.any(String),
414
+ })
415
+ );
416
+ });
417
+ });
418
+
419
+ // ============================================================================
420
+ // deleteMilestone Tests
421
+ // ============================================================================
422
+
423
+ describe('deleteMilestone', () => {
424
+ beforeEach(() => vi.clearAllMocks());
425
+
426
+ it('should throw error for missing milestone_id', async () => {
427
+ const supabase = createMockSupabase();
428
+ const ctx = createMockContext(supabase);
429
+
430
+ await expect(deleteMilestone({}, ctx)).rejects.toThrow(ValidationError);
431
+ });
432
+
433
+ it('should throw error for invalid milestone_id UUID', async () => {
434
+ const supabase = createMockSupabase();
435
+ const ctx = createMockContext(supabase);
436
+
437
+ await expect(
438
+ deleteMilestone({ milestone_id: 'invalid' }, ctx)
439
+ ).rejects.toThrow(ValidationError);
440
+ });
441
+
442
+ it('should delete milestone successfully', async () => {
443
+ const supabase = createMockSupabase({
444
+ deleteResult: { data: null, error: null },
445
+ });
446
+ const ctx = createMockContext(supabase);
447
+
448
+ const result = await deleteMilestone(
449
+ { milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
450
+ ctx
451
+ );
452
+
453
+ expect(result.result).toMatchObject({
454
+ success: true,
455
+ message: 'Milestone deleted',
456
+ });
457
+ });
458
+
459
+ it('should call delete on task_milestones table', async () => {
460
+ const supabase = createMockSupabase({
461
+ deleteResult: { data: null, error: null },
462
+ });
463
+ const ctx = createMockContext(supabase);
464
+
465
+ await deleteMilestone(
466
+ { milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
467
+ ctx
468
+ );
469
+
470
+ expect(supabase.from).toHaveBeenCalledWith('task_milestones');
471
+ expect(supabase.delete).toHaveBeenCalled();
472
+ });
473
+ });
474
+
475
+ // ============================================================================
476
+ // getMilestones Tests
477
+ // ============================================================================
478
+
479
+ describe('getMilestones', () => {
480
+ beforeEach(() => vi.clearAllMocks());
481
+
482
+ it('should throw error for missing task_id', async () => {
483
+ const supabase = createMockSupabase();
484
+ const ctx = createMockContext(supabase);
485
+
486
+ await expect(getMilestones({}, ctx)).rejects.toThrow(ValidationError);
487
+ });
488
+
489
+ it('should throw error for invalid task_id UUID', async () => {
490
+ const supabase = createMockSupabase();
491
+ const ctx = createMockContext(supabase);
492
+
493
+ await expect(
494
+ getMilestones({ task_id: 'invalid' }, ctx)
495
+ ).rejects.toThrow(ValidationError);
496
+ });
497
+
498
+ it('should return empty list with zero stats', async () => {
499
+ const supabase = createMockSupabase({
500
+ selectResult: { data: [], error: null },
501
+ });
502
+ const ctx = createMockContext(supabase);
503
+
504
+ const result = await getMilestones(
505
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
506
+ ctx
507
+ );
508
+
509
+ expect(result.result).toMatchObject({
510
+ milestones: [],
511
+ stats: {
512
+ total: 0,
513
+ completed: 0,
514
+ in_progress: 0,
515
+ pending: 0,
516
+ progress_percentage: 0,
517
+ },
518
+ });
519
+ });
520
+
521
+ it('should return milestones with stats', async () => {
522
+ const mockMilestones = [
523
+ { id: 'm1', title: 'Step 1', status: 'completed', order_index: 0 },
524
+ { id: 'm2', title: 'Step 2', status: 'in_progress', order_index: 1 },
525
+ { id: 'm3', title: 'Step 3', status: 'pending', order_index: 2 },
526
+ { id: 'm4', title: 'Step 4', status: 'pending', order_index: 3 },
527
+ ];
528
+
529
+ const supabase = createMockSupabase({
530
+ selectResult: { data: mockMilestones, error: null },
531
+ });
532
+ const ctx = createMockContext(supabase);
533
+
534
+ const result = await getMilestones(
535
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
536
+ ctx
537
+ );
538
+
539
+ expect(result.result).toMatchObject({
540
+ milestones: mockMilestones,
541
+ stats: {
542
+ total: 4,
543
+ completed: 1,
544
+ in_progress: 1,
545
+ pending: 2,
546
+ progress_percentage: 25, // 1/4 = 25%
547
+ },
548
+ });
549
+ });
550
+
551
+ it('should calculate 100% when all completed', async () => {
552
+ const mockMilestones = [
553
+ { id: 'm1', status: 'completed' },
554
+ { id: 'm2', status: 'completed' },
555
+ ];
556
+
557
+ const supabase = createMockSupabase({
558
+ selectResult: { data: mockMilestones, error: null },
559
+ });
560
+ const ctx = createMockContext(supabase);
561
+
562
+ const result = await getMilestones(
563
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
564
+ ctx
565
+ );
566
+
567
+ expect((result.result as { stats: { progress_percentage: number } }).stats.progress_percentage).toBe(100);
568
+ });
569
+
570
+ it('should query task_milestones table ordered by order_index', async () => {
571
+ const supabase = createMockSupabase({
572
+ selectResult: { data: [], error: null },
573
+ });
574
+ const ctx = createMockContext(supabase);
575
+
576
+ await getMilestones(
577
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
578
+ ctx
579
+ );
580
+
581
+ expect(supabase.from).toHaveBeenCalledWith('task_milestones');
582
+ expect(supabase.order).toHaveBeenCalledWith('order_index', { ascending: true });
583
+ });
584
+ });