@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,753 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ addIdea,
4
+ updateIdea,
5
+ getIdeas,
6
+ deleteIdea,
7
+ convertIdeaToTask,
8
+ } from './ideas.js';
9
+ import { ValidationError } from '../validators.js';
10
+ import { createMockSupabase, createMockContext } from './__test-utils__.js';
11
+
12
+ // ============================================================================
13
+ // addIdea Tests
14
+ // ============================================================================
15
+
16
+ describe('addIdea', () => {
17
+ beforeEach(() => vi.clearAllMocks());
18
+
19
+ it('should throw error for missing project_id', async () => {
20
+ const supabase = createMockSupabase();
21
+ const ctx = createMockContext(supabase);
22
+
23
+ await expect(
24
+ addIdea({ title: 'New Feature' }, ctx)
25
+ ).rejects.toThrow(ValidationError);
26
+ });
27
+
28
+ it('should throw error for invalid project_id UUID', async () => {
29
+ const supabase = createMockSupabase();
30
+ const ctx = createMockContext(supabase);
31
+
32
+ await expect(
33
+ addIdea({ project_id: 'invalid', title: 'New Feature' }, ctx)
34
+ ).rejects.toThrow(ValidationError);
35
+ });
36
+
37
+ it('should throw error for missing title', async () => {
38
+ const supabase = createMockSupabase();
39
+ const ctx = createMockContext(supabase);
40
+
41
+ await expect(
42
+ addIdea({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
43
+ ).rejects.toThrow(ValidationError);
44
+ });
45
+
46
+ it('should add idea successfully', async () => {
47
+ const supabase = createMockSupabase({
48
+ insertResult: { data: { id: 'idea-1' }, error: null },
49
+ });
50
+ const ctx = createMockContext(supabase);
51
+
52
+ const result = await addIdea(
53
+ {
54
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
55
+ title: 'Dark Mode Support',
56
+ },
57
+ ctx
58
+ );
59
+
60
+ expect(result.result).toMatchObject({
61
+ success: true,
62
+ idea_id: 'idea-1',
63
+ title: 'Dark Mode Support',
64
+ });
65
+ });
66
+
67
+ it('should insert with default status "raw"', async () => {
68
+ const supabase = createMockSupabase({
69
+ insertResult: { data: { id: 'idea-1' }, error: null },
70
+ });
71
+ const ctx = createMockContext(supabase);
72
+
73
+ await addIdea(
74
+ {
75
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
76
+ title: 'New Feature',
77
+ },
78
+ ctx
79
+ );
80
+
81
+ expect(supabase.insert).toHaveBeenCalledWith(
82
+ expect.objectContaining({
83
+ status: 'raw',
84
+ })
85
+ );
86
+ });
87
+
88
+ it('should insert with custom status', async () => {
89
+ const supabase = createMockSupabase({
90
+ insertResult: { data: { id: 'idea-1' }, error: null },
91
+ });
92
+ const ctx = createMockContext(supabase);
93
+
94
+ await addIdea(
95
+ {
96
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
97
+ title: 'New Feature',
98
+ status: 'exploring',
99
+ },
100
+ ctx
101
+ );
102
+
103
+ expect(supabase.insert).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ status: 'exploring',
106
+ })
107
+ );
108
+ });
109
+
110
+ it('should include session_id in insert', async () => {
111
+ const supabase = createMockSupabase({
112
+ insertResult: { data: { id: 'idea-1' }, error: null },
113
+ });
114
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
115
+
116
+ await addIdea(
117
+ {
118
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
119
+ title: 'New Feature',
120
+ },
121
+ ctx
122
+ );
123
+
124
+ expect(supabase.insert).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ created_by: 'agent',
127
+ created_by_session_id: 'my-session',
128
+ })
129
+ );
130
+ });
131
+
132
+ it('should throw error when insert fails', async () => {
133
+ const supabase = createMockSupabase({
134
+ insertResult: { data: null, error: { message: 'Insert failed' } },
135
+ });
136
+ const ctx = createMockContext(supabase);
137
+
138
+ await expect(
139
+ addIdea({
140
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
141
+ title: 'New Feature',
142
+ }, ctx)
143
+ ).rejects.toThrow('Failed to add idea');
144
+ });
145
+ });
146
+
147
+ // ============================================================================
148
+ // updateIdea Tests
149
+ // ============================================================================
150
+
151
+ describe('updateIdea', () => {
152
+ beforeEach(() => vi.clearAllMocks());
153
+
154
+ it('should throw error for missing idea_id', async () => {
155
+ const supabase = createMockSupabase();
156
+ const ctx = createMockContext(supabase);
157
+
158
+ await expect(updateIdea({}, ctx)).rejects.toThrow(ValidationError);
159
+ });
160
+
161
+ it('should throw error for invalid idea_id UUID', async () => {
162
+ const supabase = createMockSupabase();
163
+ const ctx = createMockContext(supabase);
164
+
165
+ await expect(
166
+ updateIdea({ idea_id: 'invalid' }, ctx)
167
+ ).rejects.toThrow(ValidationError);
168
+ });
169
+
170
+ it('should throw error when idea not found', async () => {
171
+ const supabase = createMockSupabase({
172
+ selectResult: { data: null, error: null },
173
+ });
174
+ const ctx = createMockContext(supabase);
175
+
176
+ await expect(
177
+ updateIdea({ idea_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
178
+ ).rejects.toThrow('Idea not found');
179
+ });
180
+
181
+ it('should update idea title', async () => {
182
+ const supabase = createMockSupabase({
183
+ selectResult: { data: { status: 'raw' }, error: null },
184
+ updateResult: { data: null, error: null },
185
+ });
186
+ const ctx = createMockContext(supabase);
187
+
188
+ const result = await updateIdea(
189
+ {
190
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
191
+ title: 'Updated Title',
192
+ },
193
+ ctx
194
+ );
195
+
196
+ expect(result.result).toMatchObject({
197
+ success: true,
198
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
199
+ });
200
+ expect(supabase.update).toHaveBeenCalledWith(
201
+ expect.objectContaining({
202
+ title: 'Updated Title',
203
+ updated_at: expect.any(String),
204
+ })
205
+ );
206
+ });
207
+
208
+ it('should set planned_at when transitioning to planned status', async () => {
209
+ const supabase = createMockSupabase({
210
+ selectResult: { data: { status: 'exploring' }, error: null },
211
+ updateResult: { data: null, error: null },
212
+ });
213
+ const ctx = createMockContext(supabase);
214
+
215
+ await updateIdea(
216
+ {
217
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
218
+ status: 'planned',
219
+ },
220
+ ctx
221
+ );
222
+
223
+ expect(supabase.update).toHaveBeenCalledWith(
224
+ expect.objectContaining({
225
+ status: 'planned',
226
+ planned_at: expect.any(String),
227
+ })
228
+ );
229
+ });
230
+
231
+ it('should not set planned_at when already in planned status', async () => {
232
+ const supabase = createMockSupabase({
233
+ selectResult: { data: { status: 'planned' }, error: null },
234
+ updateResult: { data: null, error: null },
235
+ });
236
+ const ctx = createMockContext(supabase);
237
+
238
+ await updateIdea(
239
+ {
240
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
241
+ status: 'planned',
242
+ },
243
+ ctx
244
+ );
245
+
246
+ // Should NOT have planned_at since it's already planned
247
+ expect(supabase.update).toHaveBeenCalledWith(
248
+ expect.not.objectContaining({
249
+ planned_at: expect.any(String),
250
+ })
251
+ );
252
+ });
253
+
254
+ it('should update doc_url', async () => {
255
+ const supabase = createMockSupabase({
256
+ selectResult: { data: { status: 'exploring' }, error: null },
257
+ updateResult: { data: null, error: null },
258
+ });
259
+ const ctx = createMockContext(supabase);
260
+
261
+ await updateIdea(
262
+ {
263
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
264
+ doc_url: 'https://docs.example.com/feature',
265
+ },
266
+ ctx
267
+ );
268
+
269
+ expect(supabase.update).toHaveBeenCalledWith(
270
+ expect.objectContaining({
271
+ doc_url: 'https://docs.example.com/feature',
272
+ })
273
+ );
274
+ });
275
+
276
+ it('should throw error when update fails', async () => {
277
+ const supabase = createMockSupabase({
278
+ selectResult: { data: { status: 'raw' }, error: null },
279
+ });
280
+ const ctx = createMockContext(supabase);
281
+
282
+ // Override update to return error
283
+ vi.mocked(supabase.from('').update).mockReturnValue({
284
+ ...supabase,
285
+ eq: vi.fn().mockReturnValue({
286
+ then: (resolve: (val: unknown) => void) =>
287
+ Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
288
+ }),
289
+ } as unknown as ReturnType<SupabaseClient['from']>);
290
+
291
+ await expect(
292
+ updateIdea({ idea_id: '123e4567-e89b-12d3-a456-426614174000', title: 'New' }, ctx)
293
+ ).rejects.toThrow('Failed to update idea');
294
+ });
295
+ });
296
+
297
+ // ============================================================================
298
+ // getIdeas Tests
299
+ // ============================================================================
300
+
301
+ describe('getIdeas', () => {
302
+ beforeEach(() => vi.clearAllMocks());
303
+
304
+ it('should throw error for missing project_id', async () => {
305
+ const supabase = createMockSupabase();
306
+ const ctx = createMockContext(supabase);
307
+
308
+ await expect(getIdeas({}, ctx)).rejects.toThrow(ValidationError);
309
+ });
310
+
311
+ it('should throw error for invalid project_id UUID', async () => {
312
+ const supabase = createMockSupabase();
313
+ const ctx = createMockContext(supabase);
314
+
315
+ await expect(
316
+ getIdeas({ project_id: 'invalid' }, ctx)
317
+ ).rejects.toThrow(ValidationError);
318
+ });
319
+
320
+ it('should return empty list when no ideas', async () => {
321
+ const supabase = createMockSupabase({
322
+ selectResult: { data: [], error: null },
323
+ });
324
+ const ctx = createMockContext(supabase);
325
+
326
+ // Override to return array result
327
+ vi.mocked(supabase.from('').select).mockReturnValue({
328
+ ...supabase,
329
+ then: (resolve: (val: unknown) => void) =>
330
+ Promise.resolve({ data: [], error: null }).then(resolve),
331
+ } as unknown as ReturnType<SupabaseClient['from']>);
332
+
333
+ const result = await getIdeas(
334
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
335
+ ctx
336
+ );
337
+
338
+ expect(result.result).toMatchObject({ ideas: [] });
339
+ });
340
+
341
+ it('should return ideas list', async () => {
342
+ const mockIdeas = [
343
+ { id: 'i1', title: 'Idea 1', description: null, status: 'raw', doc_url: null },
344
+ { id: 'i2', title: 'Idea 2', description: 'Some desc', status: 'exploring', doc_url: null },
345
+ ];
346
+
347
+ const supabase = createMockSupabase({
348
+ selectResult: { data: mockIdeas, error: null },
349
+ });
350
+ const ctx = createMockContext(supabase);
351
+
352
+ // Override to return array result
353
+ vi.mocked(supabase.from('').select).mockReturnValue({
354
+ ...supabase,
355
+ then: (resolve: (val: unknown) => void) =>
356
+ Promise.resolve({ data: mockIdeas, error: null }).then(resolve),
357
+ } as unknown as ReturnType<SupabaseClient['from']>);
358
+
359
+ const result = await getIdeas(
360
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
361
+ ctx
362
+ );
363
+
364
+ expect((result.result as { ideas: unknown[] }).ideas).toHaveLength(2);
365
+ });
366
+
367
+ it('should filter by status when provided', async () => {
368
+ const supabase = createMockSupabase({
369
+ selectResult: { data: [], error: null },
370
+ });
371
+ const ctx = createMockContext(supabase);
372
+
373
+ await getIdeas(
374
+ { project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'planned' },
375
+ ctx
376
+ );
377
+
378
+ expect(supabase.eq).toHaveBeenCalledWith('status', 'planned');
379
+ });
380
+
381
+ it('should query ideas table', async () => {
382
+ const supabase = createMockSupabase({
383
+ selectResult: { data: [], error: null },
384
+ });
385
+ const ctx = createMockContext(supabase);
386
+
387
+ await getIdeas(
388
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
389
+ ctx
390
+ );
391
+
392
+ expect(supabase.from).toHaveBeenCalledWith('ideas');
393
+ });
394
+
395
+ it('should order by created_at descending', async () => {
396
+ const supabase = createMockSupabase({
397
+ selectResult: { data: [], error: null },
398
+ });
399
+ const ctx = createMockContext(supabase);
400
+
401
+ await getIdeas(
402
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
403
+ ctx
404
+ );
405
+
406
+ expect(supabase.order).toHaveBeenCalledWith('created_at', { ascending: false });
407
+ });
408
+
409
+ it('should throw error when query fails', async () => {
410
+ const supabase = createMockSupabase();
411
+ const ctx = createMockContext(supabase);
412
+
413
+ // Override to return error
414
+ vi.mocked(supabase.from('').select).mockReturnValue({
415
+ ...supabase,
416
+ then: (resolve: (val: unknown) => void) =>
417
+ Promise.resolve({ data: null, error: { message: 'Query failed' } }).then(resolve),
418
+ } as unknown as ReturnType<SupabaseClient['from']>);
419
+
420
+ await expect(
421
+ getIdeas({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
422
+ ).rejects.toThrow('Failed to fetch ideas');
423
+ });
424
+ });
425
+
426
+ // ============================================================================
427
+ // deleteIdea Tests
428
+ // ============================================================================
429
+
430
+ describe('deleteIdea', () => {
431
+ beforeEach(() => vi.clearAllMocks());
432
+
433
+ it('should throw error for missing idea_id', async () => {
434
+ const supabase = createMockSupabase();
435
+ const ctx = createMockContext(supabase);
436
+
437
+ await expect(deleteIdea({}, ctx)).rejects.toThrow(ValidationError);
438
+ });
439
+
440
+ it('should throw error for invalid idea_id UUID', async () => {
441
+ const supabase = createMockSupabase();
442
+ const ctx = createMockContext(supabase);
443
+
444
+ await expect(
445
+ deleteIdea({ idea_id: 'invalid' }, ctx)
446
+ ).rejects.toThrow(ValidationError);
447
+ });
448
+
449
+ it('should delete idea successfully', async () => {
450
+ const supabase = createMockSupabase({
451
+ deleteResult: { data: null, error: null },
452
+ });
453
+ const ctx = createMockContext(supabase);
454
+
455
+ // Override delete to return success
456
+ vi.mocked(supabase.from('').delete).mockReturnValue({
457
+ ...supabase,
458
+ eq: vi.fn().mockReturnValue({
459
+ then: (resolve: (val: unknown) => void) =>
460
+ Promise.resolve({ data: null, error: null }).then(resolve),
461
+ }),
462
+ } as unknown as ReturnType<SupabaseClient['from']>);
463
+
464
+ const result = await deleteIdea(
465
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
466
+ ctx
467
+ );
468
+
469
+ expect(result.result).toMatchObject({ success: true });
470
+ });
471
+
472
+ it('should call delete on ideas table', async () => {
473
+ const supabase = createMockSupabase();
474
+ const ctx = createMockContext(supabase);
475
+
476
+ const mockEq = vi.fn().mockReturnValue({
477
+ then: (resolve: (val: unknown) => void) =>
478
+ Promise.resolve({ data: null, error: null }).then(resolve),
479
+ });
480
+
481
+ vi.mocked(supabase.from('').delete).mockReturnValue({
482
+ ...supabase,
483
+ eq: mockEq,
484
+ } as unknown as ReturnType<SupabaseClient['from']>);
485
+
486
+ await deleteIdea(
487
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
488
+ ctx
489
+ );
490
+
491
+ expect(supabase.from).toHaveBeenCalledWith('ideas');
492
+ expect(supabase.delete).toHaveBeenCalled();
493
+ expect(mockEq).toHaveBeenCalledWith('id', '123e4567-e89b-12d3-a456-426614174000');
494
+ });
495
+
496
+ it('should throw error when delete fails', async () => {
497
+ const supabase = createMockSupabase();
498
+ const ctx = createMockContext(supabase);
499
+
500
+ // Override delete to return error
501
+ vi.mocked(supabase.from('').delete).mockReturnValue({
502
+ ...supabase,
503
+ eq: vi.fn().mockReturnValue({
504
+ then: (resolve: (val: unknown) => void) =>
505
+ Promise.resolve({ data: null, error: { message: 'Delete failed' } }).then(resolve),
506
+ }),
507
+ } as unknown as ReturnType<SupabaseClient['from']>);
508
+
509
+ await expect(
510
+ deleteIdea({ idea_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
511
+ ).rejects.toThrow('Failed to delete idea');
512
+ });
513
+ });
514
+
515
+ // ============================================================================
516
+ // convertIdeaToTask Tests
517
+ // ============================================================================
518
+
519
+ describe('convertIdeaToTask', () => {
520
+ beforeEach(() => vi.clearAllMocks());
521
+
522
+ it('should throw error for missing idea_id', async () => {
523
+ const supabase = createMockSupabase();
524
+ const ctx = createMockContext(supabase);
525
+
526
+ await expect(convertIdeaToTask({}, ctx)).rejects.toThrow(ValidationError);
527
+ });
528
+
529
+ it('should throw error for invalid idea_id UUID', async () => {
530
+ const supabase = createMockSupabase();
531
+ const ctx = createMockContext(supabase);
532
+
533
+ await expect(
534
+ convertIdeaToTask({ idea_id: 'invalid' }, ctx)
535
+ ).rejects.toThrow(ValidationError);
536
+ });
537
+
538
+ it('should throw error for invalid priority', async () => {
539
+ const supabase = createMockSupabase();
540
+ const ctx = createMockContext(supabase);
541
+
542
+ await expect(
543
+ convertIdeaToTask({
544
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
545
+ priority: 10, // Invalid
546
+ }, ctx)
547
+ ).rejects.toThrow(ValidationError);
548
+ });
549
+
550
+ it('should throw error when idea not found', async () => {
551
+ const supabase = createMockSupabase({
552
+ selectResult: { data: null, error: { message: 'Not found' } },
553
+ });
554
+ const ctx = createMockContext(supabase);
555
+
556
+ await expect(
557
+ convertIdeaToTask({ idea_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
558
+ ).rejects.toThrow('Idea not found');
559
+ });
560
+
561
+ it('should return error when idea already converted', async () => {
562
+ const supabase = createMockSupabase({
563
+ selectResult: {
564
+ data: {
565
+ id: 'idea-1',
566
+ project_id: 'proj-1',
567
+ title: 'My Idea',
568
+ description: null,
569
+ status: 'in_development',
570
+ converted_to_task_id: 'existing-task-id',
571
+ },
572
+ error: null,
573
+ },
574
+ });
575
+ const ctx = createMockContext(supabase);
576
+
577
+ const result = await convertIdeaToTask(
578
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
579
+ ctx
580
+ );
581
+
582
+ expect(result.result).toMatchObject({
583
+ success: false,
584
+ error: 'Idea has already been converted to a task',
585
+ existing_task_id: 'existing-task-id',
586
+ });
587
+ });
588
+
589
+ it('should convert idea to task successfully', async () => {
590
+ const mockIdea = {
591
+ id: 'idea-1',
592
+ project_id: 'proj-1',
593
+ title: 'Feature Request',
594
+ description: 'Add dark mode',
595
+ status: 'planned',
596
+ converted_to_task_id: null,
597
+ };
598
+
599
+ const supabase = createMockSupabase({
600
+ selectResult: { data: mockIdea, error: null },
601
+ insertResult: { data: { id: 'task-1', title: 'Feature Request' }, error: null },
602
+ updateResult: { data: null, error: null },
603
+ });
604
+ const ctx = createMockContext(supabase);
605
+
606
+ const result = await convertIdeaToTask(
607
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
608
+ ctx
609
+ );
610
+
611
+ expect(result.result).toMatchObject({
612
+ success: true,
613
+ task_id: 'task-1',
614
+ task_title: 'Feature Request',
615
+ idea_id: 'idea-1',
616
+ });
617
+ });
618
+
619
+ it('should use default priority of 3', async () => {
620
+ const mockIdea = {
621
+ id: 'idea-1',
622
+ project_id: 'proj-1',
623
+ title: 'Feature Request',
624
+ description: null,
625
+ status: 'planned',
626
+ converted_to_task_id: null,
627
+ };
628
+
629
+ const supabase = createMockSupabase({
630
+ selectResult: { data: mockIdea, error: null },
631
+ insertResult: { data: { id: 'task-1', title: 'Feature Request' }, error: null },
632
+ updateResult: { data: null, error: null },
633
+ });
634
+ const ctx = createMockContext(supabase);
635
+
636
+ await convertIdeaToTask(
637
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
638
+ ctx
639
+ );
640
+
641
+ expect(supabase.insert).toHaveBeenCalledWith(
642
+ expect.objectContaining({
643
+ priority: 3,
644
+ })
645
+ );
646
+ });
647
+
648
+ it('should use custom priority when provided', async () => {
649
+ const mockIdea = {
650
+ id: 'idea-1',
651
+ project_id: 'proj-1',
652
+ title: 'Feature Request',
653
+ description: null,
654
+ status: 'planned',
655
+ converted_to_task_id: null,
656
+ };
657
+
658
+ const supabase = createMockSupabase({
659
+ selectResult: { data: mockIdea, error: null },
660
+ insertResult: { data: { id: 'task-1', title: 'Feature Request' }, error: null },
661
+ updateResult: { data: null, error: null },
662
+ });
663
+ const ctx = createMockContext(supabase);
664
+
665
+ await convertIdeaToTask(
666
+ {
667
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
668
+ priority: 1,
669
+ },
670
+ ctx
671
+ );
672
+
673
+ expect(supabase.insert).toHaveBeenCalledWith(
674
+ expect.objectContaining({
675
+ priority: 1,
676
+ })
677
+ );
678
+ });
679
+
680
+ it('should update idea status to in_development by default', async () => {
681
+ const mockIdea = {
682
+ id: 'idea-1',
683
+ project_id: 'proj-1',
684
+ title: 'Feature Request',
685
+ description: null,
686
+ status: 'planned',
687
+ converted_to_task_id: null,
688
+ };
689
+
690
+ const supabase = createMockSupabase({
691
+ selectResult: { data: mockIdea, error: null },
692
+ insertResult: { data: { id: 'task-1', title: 'Feature Request' }, error: null },
693
+ updateResult: { data: null, error: null },
694
+ });
695
+ const ctx = createMockContext(supabase);
696
+
697
+ const result = await convertIdeaToTask(
698
+ { idea_id: '123e4567-e89b-12d3-a456-426614174000' },
699
+ ctx
700
+ );
701
+
702
+ expect((result.result as { idea_status: string }).idea_status).toBe('in_development');
703
+ });
704
+
705
+ it('should not update status when update_status is false', async () => {
706
+ const mockIdea = {
707
+ id: 'idea-1',
708
+ project_id: 'proj-1',
709
+ title: 'Feature Request',
710
+ description: null,
711
+ status: 'planned',
712
+ converted_to_task_id: null,
713
+ };
714
+
715
+ const supabase = createMockSupabase({
716
+ selectResult: { data: mockIdea, error: null },
717
+ insertResult: { data: { id: 'task-1', title: 'Feature Request' }, error: null },
718
+ updateResult: { data: null, error: null },
719
+ });
720
+ const ctx = createMockContext(supabase);
721
+
722
+ const result = await convertIdeaToTask(
723
+ {
724
+ idea_id: '123e4567-e89b-12d3-a456-426614174000',
725
+ update_status: false,
726
+ },
727
+ ctx
728
+ );
729
+
730
+ expect((result.result as { idea_status: string }).idea_status).toBe('planned');
731
+ });
732
+
733
+ it('should throw error when task creation fails', async () => {
734
+ const mockIdea = {
735
+ id: 'idea-1',
736
+ project_id: 'proj-1',
737
+ title: 'Feature Request',
738
+ description: null,
739
+ status: 'planned',
740
+ converted_to_task_id: null,
741
+ };
742
+
743
+ const supabase = createMockSupabase({
744
+ selectResult: { data: mockIdea, error: null },
745
+ insertResult: { data: null, error: { message: 'Insert failed' } },
746
+ });
747
+ const ctx = createMockContext(supabase);
748
+
749
+ await expect(
750
+ convertIdeaToTask({ idea_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
751
+ ).rejects.toThrow('Failed to create task');
752
+ });
753
+ });