@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,562 @@
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
+ getProjectContext,
6
+ getGitWorkflow,
7
+ createProject,
8
+ updateProject,
9
+ updateProjectReadme,
10
+ } from './project.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
+ } = {}) {
22
+ const defaultResult = { data: null, error: null };
23
+ let currentOperation = 'select';
24
+ let insertThenSelect = false;
25
+
26
+ const mock = {
27
+ from: vi.fn().mockReturnThis(),
28
+ select: vi.fn(() => {
29
+ if (currentOperation === 'insert') {
30
+ insertThenSelect = true;
31
+ } else {
32
+ currentOperation = 'select';
33
+ insertThenSelect = false;
34
+ }
35
+ return mock;
36
+ }),
37
+ insert: vi.fn(() => {
38
+ currentOperation = 'insert';
39
+ insertThenSelect = false;
40
+ return mock;
41
+ }),
42
+ update: vi.fn(() => {
43
+ currentOperation = 'update';
44
+ insertThenSelect = false;
45
+ return mock;
46
+ }),
47
+ delete: vi.fn(() => {
48
+ currentOperation = 'delete';
49
+ insertThenSelect = false;
50
+ return mock;
51
+ }),
52
+ eq: vi.fn().mockReturnThis(),
53
+ neq: vi.fn().mockReturnThis(),
54
+ in: vi.fn().mockReturnThis(),
55
+ is: vi.fn().mockReturnThis(),
56
+ not: vi.fn().mockReturnThis(),
57
+ or: vi.fn().mockReturnThis(),
58
+ gt: vi.fn().mockReturnThis(),
59
+ gte: vi.fn().mockReturnThis(),
60
+ lte: vi.fn().mockReturnThis(),
61
+ lt: vi.fn().mockReturnThis(),
62
+ order: vi.fn().mockReturnThis(),
63
+ limit: vi.fn().mockReturnThis(),
64
+ single: vi.fn(() => {
65
+ if (currentOperation === 'insert' || insertThenSelect) {
66
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
67
+ }
68
+ if (currentOperation === 'select') {
69
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
70
+ }
71
+ if (currentOperation === 'update') {
72
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
73
+ }
74
+ return Promise.resolve(defaultResult);
75
+ }),
76
+ maybeSingle: vi.fn(() => {
77
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
78
+ }),
79
+ then: vi.fn((resolve: (value: unknown) => void) => {
80
+ if (currentOperation === 'insert' || insertThenSelect) {
81
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
82
+ }
83
+ if (currentOperation === 'select') {
84
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
85
+ }
86
+ if (currentOperation === 'update') {
87
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
88
+ }
89
+ return Promise.resolve(defaultResult).then(resolve);
90
+ }),
91
+ };
92
+
93
+ return mock as unknown as SupabaseClient;
94
+ }
95
+
96
+ function createMockContext(
97
+ supabase: SupabaseClient,
98
+ options: { sessionId?: string | null } = {}
99
+ ): HandlerContext {
100
+ const defaultTokenUsage: TokenUsage = {
101
+ callCount: 5,
102
+ totalTokens: 2500,
103
+ byTool: {},
104
+ };
105
+
106
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
107
+
108
+ return {
109
+ supabase,
110
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
111
+ session: {
112
+ instanceId: 'instance-abc',
113
+ currentSessionId: sessionId,
114
+ currentPersona: 'Wave',
115
+ tokenUsage: defaultTokenUsage,
116
+ },
117
+ updateSession: vi.fn(),
118
+ };
119
+ }
120
+
121
+ // ============================================================================
122
+ // getProjectContext Tests
123
+ // ============================================================================
124
+
125
+ describe('getProjectContext', () => {
126
+ beforeEach(() => vi.clearAllMocks());
127
+
128
+ it('should list all projects when no project_id or git_url', async () => {
129
+ const mockProjects = [
130
+ { id: 'proj-1', name: 'Project 1', description: 'Desc 1', status: 'active', git_url: null },
131
+ { id: 'proj-2', name: 'Project 2', description: 'Desc 2', status: 'active', git_url: 'https://github.com/test' },
132
+ ];
133
+
134
+ const supabase = createMockSupabase({
135
+ selectResult: { data: mockProjects, error: null },
136
+ });
137
+ const ctx = createMockContext(supabase);
138
+
139
+ const result = await getProjectContext({}, ctx);
140
+
141
+ expect(result.result).toHaveProperty('projects');
142
+ expect((result.result as { projects: unknown[] }).projects).toEqual(mockProjects);
143
+ expect(supabase.from).toHaveBeenCalledWith('projects');
144
+ });
145
+
146
+ it('should return found: false when project not found', async () => {
147
+ const supabase = createMockSupabase({
148
+ selectResult: { data: null, error: { message: 'Not found' } },
149
+ });
150
+ const ctx = createMockContext(supabase);
151
+
152
+ const result = await getProjectContext(
153
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
154
+ ctx
155
+ );
156
+
157
+ expect(result.result).toMatchObject({
158
+ found: false,
159
+ message: 'Project not found. Use create_project to create one.',
160
+ });
161
+ });
162
+
163
+ it('should find project by git_url', async () => {
164
+ const supabase = createMockSupabase({
165
+ selectResult: {
166
+ data: { id: 'proj-1', name: 'Test Project', description: null, goal: null, status: 'active', git_url: 'https://github.com/test', agent_instructions: null, tech_stack: null },
167
+ error: null,
168
+ },
169
+ });
170
+ const ctx = createMockContext(supabase);
171
+
172
+ const result = await getProjectContext(
173
+ { git_url: 'https://github.com/test' },
174
+ ctx
175
+ );
176
+
177
+ expect(result.result).toMatchObject({
178
+ found: true,
179
+ });
180
+ expect(supabase.eq).toHaveBeenCalledWith('git_url', 'https://github.com/test');
181
+ });
182
+
183
+ it('should include active_tasks in result', async () => {
184
+ const supabase = createMockSupabase({
185
+ selectResult: {
186
+ data: { id: 'proj-1', name: 'Test Project', description: null, goal: null, status: 'active', git_url: null, agent_instructions: null, tech_stack: null },
187
+ error: null,
188
+ },
189
+ });
190
+ const ctx = createMockContext(supabase);
191
+
192
+ const result = await getProjectContext(
193
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
194
+ ctx
195
+ );
196
+
197
+ expect(result.result).toHaveProperty('active_tasks');
198
+ });
199
+ });
200
+
201
+ // ============================================================================
202
+ // getGitWorkflow Tests
203
+ // ============================================================================
204
+
205
+ describe('getGitWorkflow', () => {
206
+ beforeEach(() => vi.clearAllMocks());
207
+
208
+ it('should throw error for missing project_id', async () => {
209
+ const supabase = createMockSupabase();
210
+ const ctx = createMockContext(supabase);
211
+
212
+ await expect(getGitWorkflow({}, ctx)).rejects.toThrow(ValidationError);
213
+ });
214
+
215
+ it('should throw error for invalid project_id UUID', async () => {
216
+ const supabase = createMockSupabase();
217
+ const ctx = createMockContext(supabase);
218
+
219
+ await expect(
220
+ getGitWorkflow({ project_id: 'invalid' }, ctx)
221
+ ).rejects.toThrow(ValidationError);
222
+ });
223
+
224
+ it('should return workflow instructions for github-flow', async () => {
225
+ const supabase = createMockSupabase({
226
+ selectResult: {
227
+ data: {
228
+ git_workflow: 'github-flow',
229
+ git_main_branch: 'main',
230
+ git_develop_branch: null,
231
+ git_auto_branch: true,
232
+ git_auto_tag: false,
233
+ git_url: 'https://github.com/test',
234
+ },
235
+ error: null,
236
+ },
237
+ });
238
+ const ctx = createMockContext(supabase);
239
+
240
+ const result = await getGitWorkflow(
241
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
242
+ ctx
243
+ );
244
+
245
+ expect(result.result).toMatchObject({
246
+ workflow: 'github-flow',
247
+ main_branch: 'main',
248
+ auto_branch: true,
249
+ });
250
+ expect((result.result as { instructions: string[] }).instructions.length).toBeGreaterThan(0);
251
+ });
252
+
253
+ it('should return workflow instructions for git-flow', async () => {
254
+ const supabase = createMockSupabase({
255
+ selectResult: {
256
+ data: {
257
+ git_workflow: 'git-flow',
258
+ git_main_branch: 'main',
259
+ git_develop_branch: 'develop',
260
+ git_auto_branch: false,
261
+ git_auto_tag: true,
262
+ git_url: 'https://github.com/test',
263
+ },
264
+ error: null,
265
+ },
266
+ });
267
+ const ctx = createMockContext(supabase);
268
+
269
+ const result = await getGitWorkflow(
270
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
271
+ ctx
272
+ );
273
+
274
+ expect(result.result).toMatchObject({
275
+ workflow: 'git-flow',
276
+ main_branch: 'main',
277
+ develop_branch: 'develop',
278
+ });
279
+ });
280
+
281
+ it('should return no workflow instructions when workflow is none', async () => {
282
+ const supabase = createMockSupabase({
283
+ selectResult: {
284
+ data: {
285
+ git_workflow: 'none',
286
+ git_main_branch: 'main',
287
+ git_develop_branch: null,
288
+ git_auto_branch: false,
289
+ git_auto_tag: false,
290
+ git_url: null,
291
+ },
292
+ error: null,
293
+ },
294
+ });
295
+ const ctx = createMockContext(supabase);
296
+
297
+ const result = await getGitWorkflow(
298
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
299
+ ctx
300
+ );
301
+
302
+ expect(result.result).toMatchObject({
303
+ workflow: 'none',
304
+ });
305
+ expect((result.result as { instructions: string[] }).instructions[0]).toContain('No git workflow configured');
306
+ });
307
+
308
+ it('should throw error when project not found', async () => {
309
+ const supabase = createMockSupabase({
310
+ selectResult: { data: null, error: { message: 'Not found' } },
311
+ });
312
+ const ctx = createMockContext(supabase);
313
+
314
+ await expect(
315
+ getGitWorkflow({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
316
+ ).rejects.toThrow('Project not found');
317
+ });
318
+ });
319
+
320
+ // ============================================================================
321
+ // createProject Tests
322
+ // ============================================================================
323
+
324
+ describe('createProject', () => {
325
+ beforeEach(() => vi.clearAllMocks());
326
+
327
+ it('should create project successfully with minimal args', async () => {
328
+ const mockProject = {
329
+ id: 'new-proj-1',
330
+ name: 'New Project',
331
+ description: null,
332
+ goal: null,
333
+ git_url: null,
334
+ tech_stack: null,
335
+ };
336
+
337
+ const supabase = createMockSupabase({
338
+ insertResult: { data: mockProject, error: null },
339
+ });
340
+ const ctx = createMockContext(supabase);
341
+
342
+ const result = await createProject({ name: 'New Project' }, ctx);
343
+
344
+ expect(result.result).toMatchObject({
345
+ success: true,
346
+ project: mockProject,
347
+ });
348
+ expect(supabase.insert).toHaveBeenCalled();
349
+ });
350
+
351
+ it('should create project with all optional fields', async () => {
352
+ const mockProject = {
353
+ id: 'new-proj-2',
354
+ name: 'Full Project',
355
+ description: 'A test project',
356
+ goal: 'Complete the test',
357
+ git_url: 'https://github.com/test/repo',
358
+ tech_stack: ['TypeScript', 'Svelte'],
359
+ };
360
+
361
+ const supabase = createMockSupabase({
362
+ insertResult: { data: mockProject, error: null },
363
+ });
364
+ const ctx = createMockContext(supabase);
365
+
366
+ const result = await createProject(
367
+ {
368
+ name: 'Full Project',
369
+ description: 'A test project',
370
+ goal: 'Complete the test',
371
+ git_url: 'https://github.com/test/repo',
372
+ tech_stack: ['TypeScript', 'Svelte'],
373
+ },
374
+ ctx
375
+ );
376
+
377
+ expect(result.result).toMatchObject({
378
+ success: true,
379
+ project: mockProject,
380
+ });
381
+ });
382
+
383
+ it('should throw error when insert fails', async () => {
384
+ const supabase = createMockSupabase({
385
+ insertResult: { data: null, error: { message: 'Insert failed' } },
386
+ });
387
+ const ctx = createMockContext(supabase);
388
+
389
+ await expect(
390
+ createProject({ name: 'Test Project' }, ctx)
391
+ ).rejects.toThrow('Failed to create project: Insert failed');
392
+ });
393
+ });
394
+
395
+ // ============================================================================
396
+ // updateProject Tests
397
+ // ============================================================================
398
+
399
+ describe('updateProject', () => {
400
+ beforeEach(() => vi.clearAllMocks());
401
+
402
+ it('should throw error for missing project_id', async () => {
403
+ const supabase = createMockSupabase();
404
+ const ctx = createMockContext(supabase);
405
+
406
+ await expect(updateProject({}, ctx)).rejects.toThrow(ValidationError);
407
+ });
408
+
409
+ it('should throw error for invalid project_id UUID', async () => {
410
+ const supabase = createMockSupabase();
411
+ const ctx = createMockContext(supabase);
412
+
413
+ await expect(
414
+ updateProject({ project_id: 'invalid' }, ctx)
415
+ ).rejects.toThrow(ValidationError);
416
+ });
417
+
418
+ it('should throw error for invalid status', async () => {
419
+ const supabase = createMockSupabase();
420
+ const ctx = createMockContext(supabase);
421
+
422
+ await expect(
423
+ updateProject({
424
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
425
+ status: 'invalid_status',
426
+ }, ctx)
427
+ ).rejects.toThrow(ValidationError);
428
+ });
429
+
430
+ it('should update project successfully', async () => {
431
+ const supabase = createMockSupabase({
432
+ updateResult: { data: null, error: null },
433
+ });
434
+ const ctx = createMockContext(supabase);
435
+
436
+ const result = await updateProject(
437
+ {
438
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
439
+ name: 'Updated Name',
440
+ status: 'active',
441
+ },
442
+ ctx
443
+ );
444
+
445
+ expect(result.result).toMatchObject({
446
+ success: true,
447
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
448
+ });
449
+ expect(supabase.update).toHaveBeenCalled();
450
+ });
451
+
452
+ it('should update git workflow settings', async () => {
453
+ const supabase = createMockSupabase({
454
+ updateResult: { data: null, error: null },
455
+ });
456
+ const ctx = createMockContext(supabase);
457
+
458
+ const result = await updateProject(
459
+ {
460
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
461
+ git_workflow: 'github-flow',
462
+ git_main_branch: 'main',
463
+ git_auto_branch: true,
464
+ },
465
+ ctx
466
+ );
467
+
468
+ expect(result.result).toMatchObject({
469
+ success: true,
470
+ });
471
+ });
472
+
473
+ it('should throw error when update fails', async () => {
474
+ const supabase = createMockSupabase({
475
+ updateResult: { data: null, error: { message: 'Update failed' } },
476
+ });
477
+ const ctx = createMockContext(supabase);
478
+
479
+ await expect(
480
+ updateProject({
481
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
482
+ name: 'Test',
483
+ }, ctx)
484
+ ).rejects.toThrow('Failed to update project: Update failed');
485
+ });
486
+ });
487
+
488
+ // ============================================================================
489
+ // updateProjectReadme Tests
490
+ // ============================================================================
491
+
492
+ describe('updateProjectReadme', () => {
493
+ beforeEach(() => vi.clearAllMocks());
494
+
495
+ it('should throw error for missing project_id', async () => {
496
+ const supabase = createMockSupabase();
497
+ const ctx = createMockContext(supabase);
498
+
499
+ await expect(
500
+ updateProjectReadme({ readme_content: '# README' }, ctx)
501
+ ).rejects.toThrow(ValidationError);
502
+ });
503
+
504
+ it('should throw error for missing readme_content', async () => {
505
+ const supabase = createMockSupabase();
506
+ const ctx = createMockContext(supabase);
507
+
508
+ await expect(
509
+ updateProjectReadme({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
510
+ ).rejects.toThrow(ValidationError);
511
+ });
512
+
513
+ it('should throw error for invalid project_id UUID', async () => {
514
+ const supabase = createMockSupabase();
515
+ const ctx = createMockContext(supabase);
516
+
517
+ await expect(
518
+ updateProjectReadme({
519
+ project_id: 'invalid',
520
+ readme_content: '# README',
521
+ }, ctx)
522
+ ).rejects.toThrow(ValidationError);
523
+ });
524
+
525
+ it('should update readme successfully', async () => {
526
+ const supabase = createMockSupabase({
527
+ updateResult: { data: null, error: null },
528
+ });
529
+ const ctx = createMockContext(supabase);
530
+
531
+ const result = await updateProjectReadme(
532
+ {
533
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
534
+ readme_content: '# My Project\n\nDescription here.',
535
+ },
536
+ ctx
537
+ );
538
+
539
+ expect(result.result).toMatchObject({
540
+ success: true,
541
+ });
542
+ expect(supabase.update).toHaveBeenCalledWith(
543
+ expect.objectContaining({
544
+ readme_content: '# My Project\n\nDescription here.',
545
+ })
546
+ );
547
+ });
548
+
549
+ it('should throw error when update fails', async () => {
550
+ const supabase = createMockSupabase({
551
+ updateResult: { data: null, error: { message: 'Update failed' } },
552
+ });
553
+ const ctx = createMockContext(supabase);
554
+
555
+ await expect(
556
+ updateProjectReadme({
557
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
558
+ readme_content: '# README',
559
+ }, ctx)
560
+ ).rejects.toThrow('Failed to update README: Update failed');
561
+ });
562
+ });