@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,86 @@
1
+ /**
2
+ * Decisions Handlers
3
+ *
4
+ * Handles architectural/technical decisions:
5
+ * - log_decision
6
+ * - get_decisions
7
+ * - delete_decision
8
+ */
9
+
10
+ import type { Handler, HandlerRegistry } from './types.js';
11
+ import { validateRequired, validateUUID } from '../validators.js';
12
+
13
+ export const logDecision: Handler = async (args, ctx) => {
14
+ const { project_id, title, description, rationale, alternatives_considered } = args as {
15
+ project_id: string;
16
+ title: string;
17
+ description: string;
18
+ rationale?: string;
19
+ alternatives_considered?: string[];
20
+ };
21
+
22
+ validateRequired(project_id, 'project_id');
23
+ validateUUID(project_id, 'project_id');
24
+ validateRequired(title, 'title');
25
+ validateRequired(description, 'description');
26
+
27
+ const { supabase, session } = ctx;
28
+
29
+ const { error } = await supabase
30
+ .from('decisions')
31
+ .insert({
32
+ project_id,
33
+ title,
34
+ description,
35
+ rationale: rationale || null,
36
+ alternatives_considered: alternatives_considered || null,
37
+ created_by: 'agent',
38
+ created_by_session_id: session.currentSessionId,
39
+ });
40
+
41
+ if (error) throw new Error(`Failed to log decision: ${error.message}`);
42
+
43
+ return { result: { success: true, title } };
44
+ };
45
+
46
+ export const getDecisions: Handler = async (args, ctx) => {
47
+ const { project_id } = args as { project_id: string };
48
+
49
+ validateRequired(project_id, 'project_id');
50
+ validateUUID(project_id, 'project_id');
51
+
52
+ const { data, error } = await ctx.supabase
53
+ .from('decisions')
54
+ .select('id, title, description, rationale, created_at')
55
+ .eq('project_id', project_id)
56
+ .order('created_at', { ascending: false });
57
+
58
+ if (error) throw new Error(`Failed to fetch decisions: ${error.message}`);
59
+
60
+ return { result: { decisions: data || [] } };
61
+ };
62
+
63
+ export const deleteDecision: Handler = async (args, ctx) => {
64
+ const { decision_id } = args as { decision_id: string };
65
+
66
+ validateRequired(decision_id, 'decision_id');
67
+ validateUUID(decision_id, 'decision_id');
68
+
69
+ const { error } = await ctx.supabase
70
+ .from('decisions')
71
+ .delete()
72
+ .eq('id', decision_id);
73
+
74
+ if (error) throw new Error(`Failed to delete decision: ${error.message}`);
75
+
76
+ return { result: { success: true } };
77
+ };
78
+
79
+ /**
80
+ * Decisions handlers registry
81
+ */
82
+ export const decisionHandlers: HandlerRegistry = {
83
+ log_decision: logDecision,
84
+ get_decisions: getDecisions,
85
+ delete_decision: deleteDecision,
86
+ };
@@ -0,0 +1,516 @@
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
+ claimDeploymentValidation,
6
+ reportValidation,
7
+ checkDeploymentStatus,
8
+ startDeployment,
9
+ completeDeployment,
10
+ cancelDeployment,
11
+ addDeploymentRequirement,
12
+ getDeploymentRequirements,
13
+ } from './deployment.js';
14
+ import { ValidationError } from '../validators.js';
15
+
16
+ // ============================================================================
17
+ // Test Utilities
18
+ // ============================================================================
19
+
20
+ function createMockSupabase(overrides: {
21
+ selectResult?: { data: unknown; error: unknown };
22
+ insertResult?: { data: unknown; error: unknown };
23
+ updateResult?: { data: unknown; error: unknown };
24
+ } = {}) {
25
+ const defaultResult = { data: null, error: null };
26
+ let currentOperation = 'select';
27
+ let insertThenSelect = false;
28
+
29
+ const mock = {
30
+ from: vi.fn().mockReturnThis(),
31
+ select: vi.fn(() => {
32
+ if (currentOperation === 'insert') {
33
+ insertThenSelect = true;
34
+ } else {
35
+ currentOperation = 'select';
36
+ insertThenSelect = false;
37
+ }
38
+ return mock;
39
+ }),
40
+ insert: vi.fn(() => {
41
+ currentOperation = 'insert';
42
+ insertThenSelect = false;
43
+ return mock;
44
+ }),
45
+ update: vi.fn(() => {
46
+ currentOperation = 'update';
47
+ insertThenSelect = false;
48
+ return mock;
49
+ }),
50
+ delete: vi.fn(() => {
51
+ currentOperation = 'delete';
52
+ insertThenSelect = false;
53
+ return mock;
54
+ }),
55
+ eq: vi.fn().mockReturnThis(),
56
+ neq: vi.fn().mockReturnThis(),
57
+ in: vi.fn().mockReturnThis(),
58
+ is: vi.fn().mockReturnThis(),
59
+ not: vi.fn().mockReturnThis(),
60
+ or: vi.fn().mockReturnThis(),
61
+ gte: vi.fn().mockReturnThis(),
62
+ lte: vi.fn().mockReturnThis(),
63
+ lt: vi.fn().mockReturnThis(),
64
+ order: vi.fn().mockReturnThis(),
65
+ limit: vi.fn().mockReturnThis(),
66
+ single: vi.fn(() => {
67
+ if (currentOperation === 'insert' || insertThenSelect) {
68
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
69
+ }
70
+ if (currentOperation === 'select') {
71
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
72
+ }
73
+ if (currentOperation === 'update') {
74
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
75
+ }
76
+ return Promise.resolve(defaultResult);
77
+ }),
78
+ maybeSingle: vi.fn(() => {
79
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
80
+ }),
81
+ then: vi.fn((resolve: (value: unknown) => void) => {
82
+ if (currentOperation === 'insert' || insertThenSelect) {
83
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
84
+ }
85
+ if (currentOperation === 'select') {
86
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
87
+ }
88
+ if (currentOperation === 'update') {
89
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
90
+ }
91
+ return Promise.resolve(defaultResult).then(resolve);
92
+ }),
93
+ };
94
+
95
+ return mock as unknown as SupabaseClient;
96
+ }
97
+
98
+ function createMockContext(
99
+ supabase: SupabaseClient,
100
+ options: { sessionId?: string | null } = {}
101
+ ): HandlerContext {
102
+ const defaultTokenUsage: TokenUsage = {
103
+ callCount: 5,
104
+ totalTokens: 2500,
105
+ byTool: {},
106
+ };
107
+
108
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
109
+
110
+ return {
111
+ supabase,
112
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
113
+ session: {
114
+ instanceId: 'instance-abc',
115
+ currentSessionId: sessionId,
116
+ currentPersona: 'Wave',
117
+ tokenUsage: defaultTokenUsage,
118
+ },
119
+ updateSession: vi.fn(),
120
+ };
121
+ }
122
+
123
+ // ============================================================================
124
+ // claimDeploymentValidation Tests
125
+ // ============================================================================
126
+
127
+ describe('claimDeploymentValidation', () => {
128
+ beforeEach(() => vi.clearAllMocks());
129
+
130
+ it('should throw error for missing project_id', async () => {
131
+ const supabase = createMockSupabase();
132
+ const ctx = createMockContext(supabase);
133
+
134
+ await expect(claimDeploymentValidation({}, ctx)).rejects.toThrow(ValidationError);
135
+ });
136
+
137
+ it('should throw error for invalid project_id UUID', async () => {
138
+ const supabase = createMockSupabase();
139
+ const ctx = createMockContext(supabase);
140
+
141
+ await expect(
142
+ claimDeploymentValidation({ project_id: 'invalid' }, ctx)
143
+ ).rejects.toThrow(ValidationError);
144
+ });
145
+
146
+ it('should return error when no pending deployment', async () => {
147
+ const supabase = createMockSupabase({
148
+ selectResult: { data: null, error: { message: 'Not found' } },
149
+ });
150
+ const ctx = createMockContext(supabase);
151
+
152
+ const result = await claimDeploymentValidation(
153
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
154
+ ctx
155
+ );
156
+
157
+ expect(result.result).toMatchObject({
158
+ success: false,
159
+ error: 'No pending deployment found',
160
+ });
161
+ });
162
+ });
163
+
164
+ // ============================================================================
165
+ // reportValidation Tests
166
+ // ============================================================================
167
+
168
+ describe('reportValidation', () => {
169
+ beforeEach(() => vi.clearAllMocks());
170
+
171
+ it('should throw error for missing project_id', async () => {
172
+ const supabase = createMockSupabase();
173
+ const ctx = createMockContext(supabase);
174
+
175
+ await expect(
176
+ reportValidation({ build_passed: true }, ctx)
177
+ ).rejects.toThrow(ValidationError);
178
+ });
179
+
180
+ it('should throw error for missing build_passed', async () => {
181
+ const supabase = createMockSupabase();
182
+ const ctx = createMockContext(supabase);
183
+
184
+ await expect(
185
+ reportValidation({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
186
+ ).rejects.toThrow(ValidationError);
187
+ });
188
+
189
+ it('should return error when no deployment being validated', async () => {
190
+ const supabase = createMockSupabase({
191
+ selectResult: { data: null, error: { message: 'Not found' } },
192
+ });
193
+ const ctx = createMockContext(supabase);
194
+
195
+ const result = await reportValidation(
196
+ { project_id: '123e4567-e89b-12d3-a456-426614174000', build_passed: true },
197
+ ctx
198
+ );
199
+
200
+ expect(result.result).toMatchObject({
201
+ success: false,
202
+ error: 'No deployment being validated. Use claim_deployment_validation first.',
203
+ });
204
+ });
205
+ });
206
+
207
+ // ============================================================================
208
+ // checkDeploymentStatus Tests
209
+ // ============================================================================
210
+
211
+ describe('checkDeploymentStatus', () => {
212
+ beforeEach(() => vi.clearAllMocks());
213
+
214
+ it('should throw error for missing project_id', async () => {
215
+ const supabase = createMockSupabase();
216
+ const ctx = createMockContext(supabase);
217
+
218
+ await expect(checkDeploymentStatus({}, ctx)).rejects.toThrow(ValidationError);
219
+ });
220
+
221
+ it('should return no deployment when none found', async () => {
222
+ const supabase = createMockSupabase({
223
+ selectResult: { data: null, error: { message: 'Not found' } },
224
+ });
225
+ const ctx = createMockContext(supabase);
226
+
227
+ const result = await checkDeploymentStatus(
228
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
229
+ ctx
230
+ );
231
+
232
+ expect(result.result).toMatchObject({
233
+ has_deployment: false,
234
+ message: 'No deployments found for this project',
235
+ });
236
+ });
237
+
238
+ it('should return deployment details when found', async () => {
239
+ const mockDeployment = {
240
+ id: 'deploy-1',
241
+ status: 'deployed',
242
+ environment: 'production',
243
+ requested_by: 'agent',
244
+ build_passed: true,
245
+ tests_passed: true,
246
+ created_at: '2025-01-14T10:00:00Z',
247
+ };
248
+
249
+ const supabase = createMockSupabase({
250
+ selectResult: { data: mockDeployment, error: null },
251
+ });
252
+ const ctx = createMockContext(supabase);
253
+
254
+ const result = await checkDeploymentStatus(
255
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
256
+ ctx
257
+ );
258
+
259
+ expect(result.result).toMatchObject({
260
+ has_deployment: true,
261
+ });
262
+ expect((result.result as { deployment: { id: string } }).deployment.id).toBe('deploy-1');
263
+ });
264
+ });
265
+
266
+ // ============================================================================
267
+ // startDeployment Tests
268
+ // ============================================================================
269
+
270
+ describe('startDeployment', () => {
271
+ beforeEach(() => vi.clearAllMocks());
272
+
273
+ it('should throw error for missing project_id', async () => {
274
+ const supabase = createMockSupabase();
275
+ const ctx = createMockContext(supabase);
276
+
277
+ await expect(startDeployment({}, ctx)).rejects.toThrow(ValidationError);
278
+ });
279
+
280
+ it('should return error when no deployment ready', async () => {
281
+ const supabase = createMockSupabase({
282
+ selectResult: { data: null, error: { message: 'Not found' } },
283
+ });
284
+ const ctx = createMockContext(supabase);
285
+
286
+ const result = await startDeployment(
287
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
288
+ ctx
289
+ );
290
+
291
+ expect(result.result).toMatchObject({
292
+ success: false,
293
+ error: 'No deployment ready. Must pass validation first.',
294
+ });
295
+ });
296
+ });
297
+
298
+ // ============================================================================
299
+ // completeDeployment Tests
300
+ // ============================================================================
301
+
302
+ describe('completeDeployment', () => {
303
+ beforeEach(() => vi.clearAllMocks());
304
+
305
+ it('should throw error for missing project_id', async () => {
306
+ const supabase = createMockSupabase();
307
+ const ctx = createMockContext(supabase);
308
+
309
+ await expect(
310
+ completeDeployment({ success: true }, ctx)
311
+ ).rejects.toThrow(ValidationError);
312
+ });
313
+
314
+ it('should throw error for missing success', async () => {
315
+ const supabase = createMockSupabase();
316
+ const ctx = createMockContext(supabase);
317
+
318
+ await expect(
319
+ completeDeployment({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
320
+ ).rejects.toThrow(ValidationError);
321
+ });
322
+
323
+ it('should return error when no deployment in progress', async () => {
324
+ const supabase = createMockSupabase({
325
+ selectResult: { data: null, error: { message: 'Not found' } },
326
+ });
327
+ const ctx = createMockContext(supabase);
328
+
329
+ const result = await completeDeployment(
330
+ { project_id: '123e4567-e89b-12d3-a456-426614174000', success: true },
331
+ ctx
332
+ );
333
+
334
+ expect(result.result).toMatchObject({
335
+ success: false,
336
+ error: 'No deployment in progress. Use start_deployment first.',
337
+ });
338
+ });
339
+ });
340
+
341
+ // ============================================================================
342
+ // cancelDeployment Tests
343
+ // ============================================================================
344
+
345
+ describe('cancelDeployment', () => {
346
+ beforeEach(() => vi.clearAllMocks());
347
+
348
+ it('should throw error for missing project_id', async () => {
349
+ const supabase = createMockSupabase();
350
+ const ctx = createMockContext(supabase);
351
+
352
+ await expect(cancelDeployment({}, ctx)).rejects.toThrow(ValidationError);
353
+ });
354
+
355
+ it('should return error when no active deployment', async () => {
356
+ const supabase = createMockSupabase({
357
+ selectResult: { data: null, error: { message: 'Not found' } },
358
+ });
359
+ const ctx = createMockContext(supabase);
360
+
361
+ const result = await cancelDeployment(
362
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
363
+ ctx
364
+ );
365
+
366
+ expect(result.result).toMatchObject({
367
+ success: false,
368
+ error: 'No active deployment',
369
+ });
370
+ });
371
+ });
372
+
373
+ // ============================================================================
374
+ // addDeploymentRequirement Tests
375
+ // ============================================================================
376
+
377
+ describe('addDeploymentRequirement', () => {
378
+ beforeEach(() => vi.clearAllMocks());
379
+
380
+ it('should throw error for missing project_id', async () => {
381
+ const supabase = createMockSupabase();
382
+ const ctx = createMockContext(supabase);
383
+
384
+ await expect(
385
+ addDeploymentRequirement({ type: 'migration', title: 'Test' }, ctx)
386
+ ).rejects.toThrow(ValidationError);
387
+ });
388
+
389
+ it('should throw error for missing type', async () => {
390
+ const supabase = createMockSupabase();
391
+ const ctx = createMockContext(supabase);
392
+
393
+ await expect(
394
+ addDeploymentRequirement({
395
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
396
+ title: 'Test',
397
+ }, ctx)
398
+ ).rejects.toThrow(ValidationError);
399
+ });
400
+
401
+ it('should throw error for missing title', async () => {
402
+ const supabase = createMockSupabase();
403
+ const ctx = createMockContext(supabase);
404
+
405
+ await expect(
406
+ addDeploymentRequirement({
407
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
408
+ type: 'migration',
409
+ }, ctx)
410
+ ).rejects.toThrow(ValidationError);
411
+ });
412
+
413
+ it('should throw error for invalid type', async () => {
414
+ const supabase = createMockSupabase();
415
+ const ctx = createMockContext(supabase);
416
+
417
+ await expect(
418
+ addDeploymentRequirement({
419
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
420
+ type: 'invalid_type',
421
+ title: 'Test',
422
+ }, ctx)
423
+ ).rejects.toThrow(ValidationError);
424
+ });
425
+
426
+ it('should throw error for invalid stage', async () => {
427
+ const supabase = createMockSupabase();
428
+ const ctx = createMockContext(supabase);
429
+
430
+ await expect(
431
+ addDeploymentRequirement({
432
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
433
+ type: 'migration',
434
+ title: 'Test',
435
+ stage: 'invalid_stage',
436
+ }, ctx)
437
+ ).rejects.toThrow(ValidationError);
438
+ });
439
+
440
+ it('should add requirement successfully', async () => {
441
+ const supabase = createMockSupabase({
442
+ insertResult: {
443
+ data: { id: 'req-1', type: 'migration', title: 'Test Migration', stage: 'preparation', blocking: false },
444
+ error: null,
445
+ },
446
+ });
447
+ const ctx = createMockContext(supabase);
448
+
449
+ const result = await addDeploymentRequirement(
450
+ {
451
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
452
+ type: 'migration',
453
+ title: 'Test Migration',
454
+ },
455
+ ctx
456
+ );
457
+
458
+ expect(result.result).toMatchObject({
459
+ success: true,
460
+ requirement_id: 'req-1',
461
+ stage: 'preparation',
462
+ });
463
+ });
464
+ });
465
+
466
+ // ============================================================================
467
+ // getDeploymentRequirements Tests
468
+ // ============================================================================
469
+
470
+ describe('getDeploymentRequirements', () => {
471
+ beforeEach(() => vi.clearAllMocks());
472
+
473
+ it('should throw error for missing project_id', async () => {
474
+ const supabase = createMockSupabase();
475
+ const ctx = createMockContext(supabase);
476
+
477
+ await expect(getDeploymentRequirements({}, ctx)).rejects.toThrow(ValidationError);
478
+ });
479
+
480
+ it('should return empty list when no requirements', async () => {
481
+ const supabase = createMockSupabase({
482
+ selectResult: { data: [], error: null },
483
+ });
484
+ const ctx = createMockContext(supabase);
485
+
486
+ // Need to mock the .then() to return the array result
487
+ vi.mocked(supabase.from('').select).mockReturnValue({
488
+ ...supabase,
489
+ then: (resolve: (val: unknown) => void) =>
490
+ Promise.resolve({ data: [], error: null }).then(resolve),
491
+ } as unknown as ReturnType<SupabaseClient['from']>);
492
+
493
+ const result = await getDeploymentRequirements(
494
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
495
+ ctx
496
+ );
497
+
498
+ expect(result.result).toHaveProperty('requirements');
499
+ expect(result.result).toHaveProperty('deployment_blocked');
500
+ });
501
+
502
+ it('should query with correct project_id', async () => {
503
+ const supabase = createMockSupabase({
504
+ selectResult: { data: [], error: null },
505
+ });
506
+ const ctx = createMockContext(supabase);
507
+
508
+ await getDeploymentRequirements(
509
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
510
+ ctx
511
+ );
512
+
513
+ expect(supabase.from).toHaveBeenCalledWith('deployment_requirements');
514
+ expect(supabase.eq).toHaveBeenCalled();
515
+ });
516
+ });