@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,322 @@
1
+ /**
2
+ * Cost Handlers
3
+ *
4
+ * Handles cost monitoring and alerts:
5
+ * - get_cost_summary
6
+ * - get_cost_alerts
7
+ * - add_cost_alert
8
+ * - update_cost_alert
9
+ * - delete_cost_alert
10
+ */
11
+
12
+ import type { Handler, HandlerRegistry } from './types.js';
13
+
14
+ /**
15
+ * Get cost summary for a project (daily, weekly, or monthly)
16
+ */
17
+ export const getCostSummary: Handler = async (args, ctx) => {
18
+ const { project_id, period = 'daily', limit = 30 } = args as {
19
+ project_id: string;
20
+ period?: 'daily' | 'weekly' | 'monthly';
21
+ limit?: number;
22
+ };
23
+
24
+ const { supabase } = ctx;
25
+
26
+ if (!project_id) {
27
+ return {
28
+ result: { error: 'project_id is required' },
29
+ isError: true,
30
+ };
31
+ }
32
+
33
+ // Select the appropriate view based on period
34
+ const viewName = `${period}_cost_summary`;
35
+
36
+ const { data, error } = await supabase
37
+ .from(viewName)
38
+ .select('*')
39
+ .eq('project_id', project_id)
40
+ .order(period === 'daily' ? 'date' : period === 'weekly' ? 'week_start' : 'month_start', { ascending: false })
41
+ .limit(limit);
42
+
43
+ if (error) {
44
+ return {
45
+ result: { error: `Failed to get cost summary: ${error.message}` },
46
+ isError: true,
47
+ };
48
+ }
49
+
50
+ // Calculate totals
51
+ const totals = (data || []).reduce(
52
+ (acc, row) => ({
53
+ sessions: acc.sessions + (row.session_count || 0),
54
+ tokens: acc.tokens + (row.total_tokens || 0),
55
+ calls: acc.calls + (row.total_calls || 0),
56
+ cost: acc.cost + parseFloat(row.estimated_cost_usd || '0'),
57
+ }),
58
+ { sessions: 0, tokens: 0, calls: 0, cost: 0 }
59
+ );
60
+
61
+ return {
62
+ result: {
63
+ period,
64
+ project_id,
65
+ summary: data || [],
66
+ totals: {
67
+ ...totals,
68
+ cost: Math.round(totals.cost * 100) / 100,
69
+ },
70
+ },
71
+ };
72
+ };
73
+
74
+ /**
75
+ * Get cost alerts for the current user
76
+ */
77
+ export const getCostAlerts: Handler = async (args, ctx) => {
78
+ const { project_id } = args as { project_id?: string };
79
+ const { supabase, auth } = ctx;
80
+
81
+ let query = supabase
82
+ .from('cost_alerts')
83
+ .select('*')
84
+ .eq('user_id', auth.userId)
85
+ .order('threshold_amount', { ascending: true });
86
+
87
+ if (project_id) {
88
+ query = query.eq('project_id', project_id);
89
+ }
90
+
91
+ const { data, error } = await query;
92
+
93
+ if (error) {
94
+ return {
95
+ result: { error: `Failed to get cost alerts: ${error.message}` },
96
+ isError: true,
97
+ };
98
+ }
99
+
100
+ return {
101
+ result: {
102
+ alerts: data || [],
103
+ count: data?.length || 0,
104
+ },
105
+ };
106
+ };
107
+
108
+ /**
109
+ * Add a cost alert
110
+ */
111
+ export const addCostAlert: Handler = async (args, ctx) => {
112
+ const {
113
+ project_id,
114
+ threshold_amount,
115
+ threshold_period,
116
+ alert_type = 'warning',
117
+ } = args as {
118
+ project_id?: string;
119
+ threshold_amount: number;
120
+ threshold_period: 'daily' | 'weekly' | 'monthly';
121
+ alert_type?: 'warning' | 'critical';
122
+ };
123
+
124
+ const { supabase, auth } = ctx;
125
+
126
+ if (!threshold_amount || threshold_amount <= 0) {
127
+ return {
128
+ result: { error: 'threshold_amount must be a positive number' },
129
+ isError: true,
130
+ };
131
+ }
132
+
133
+ if (!threshold_period || !['daily', 'weekly', 'monthly'].includes(threshold_period)) {
134
+ return {
135
+ result: { error: 'threshold_period must be "daily", "weekly", or "monthly"' },
136
+ isError: true,
137
+ };
138
+ }
139
+
140
+ const { data, error } = await supabase
141
+ .from('cost_alerts')
142
+ .insert({
143
+ user_id: auth.userId,
144
+ project_id: project_id || null,
145
+ threshold_amount,
146
+ threshold_period,
147
+ alert_type,
148
+ })
149
+ .select()
150
+ .single();
151
+
152
+ if (error) {
153
+ return {
154
+ result: { error: `Failed to create cost alert: ${error.message}` },
155
+ isError: true,
156
+ };
157
+ }
158
+
159
+ return {
160
+ result: {
161
+ success: true,
162
+ alert: data,
163
+ message: `Alert created: ${alert_type} when ${threshold_period} cost exceeds $${threshold_amount}`,
164
+ },
165
+ };
166
+ };
167
+
168
+ /**
169
+ * Update a cost alert
170
+ */
171
+ export const updateCostAlert: Handler = async (args, ctx) => {
172
+ const {
173
+ alert_id,
174
+ threshold_amount,
175
+ threshold_period,
176
+ alert_type,
177
+ enabled,
178
+ } = args as {
179
+ alert_id: string;
180
+ threshold_amount?: number;
181
+ threshold_period?: 'daily' | 'weekly' | 'monthly';
182
+ alert_type?: 'warning' | 'critical';
183
+ enabled?: boolean;
184
+ };
185
+
186
+ const { supabase, auth } = ctx;
187
+
188
+ if (!alert_id) {
189
+ return {
190
+ result: { error: 'alert_id is required' },
191
+ isError: true,
192
+ };
193
+ }
194
+
195
+ const updates: Record<string, unknown> = {};
196
+ if (threshold_amount !== undefined) updates.threshold_amount = threshold_amount;
197
+ if (threshold_period !== undefined) updates.threshold_period = threshold_period;
198
+ if (alert_type !== undefined) updates.alert_type = alert_type;
199
+ if (enabled !== undefined) updates.enabled = enabled;
200
+
201
+ if (Object.keys(updates).length === 0) {
202
+ return {
203
+ result: { error: 'No updates provided' },
204
+ isError: true,
205
+ };
206
+ }
207
+
208
+ const { data, error } = await supabase
209
+ .from('cost_alerts')
210
+ .update(updates)
211
+ .eq('id', alert_id)
212
+ .eq('user_id', auth.userId)
213
+ .select()
214
+ .single();
215
+
216
+ if (error) {
217
+ return {
218
+ result: { error: `Failed to update cost alert: ${error.message}` },
219
+ isError: true,
220
+ };
221
+ }
222
+
223
+ return {
224
+ result: {
225
+ success: true,
226
+ alert: data,
227
+ },
228
+ };
229
+ };
230
+
231
+ /**
232
+ * Delete a cost alert
233
+ */
234
+ export const deleteCostAlert: Handler = async (args, ctx) => {
235
+ const { alert_id } = args as { alert_id: string };
236
+ const { supabase, auth } = ctx;
237
+
238
+ if (!alert_id) {
239
+ return {
240
+ result: { error: 'alert_id is required' },
241
+ isError: true,
242
+ };
243
+ }
244
+
245
+ const { error } = await supabase
246
+ .from('cost_alerts')
247
+ .delete()
248
+ .eq('id', alert_id)
249
+ .eq('user_id', auth.userId);
250
+
251
+ if (error) {
252
+ return {
253
+ result: { error: `Failed to delete cost alert: ${error.message}` },
254
+ isError: true,
255
+ };
256
+ }
257
+
258
+ return {
259
+ result: {
260
+ success: true,
261
+ deleted_alert_id: alert_id,
262
+ },
263
+ };
264
+ };
265
+
266
+ /**
267
+ * Get task costs for a project
268
+ */
269
+ export const getTaskCosts: Handler = async (args, ctx) => {
270
+ const { project_id, limit = 20 } = args as {
271
+ project_id: string;
272
+ limit?: number;
273
+ };
274
+
275
+ const { supabase } = ctx;
276
+
277
+ if (!project_id) {
278
+ return {
279
+ result: { error: 'project_id is required' },
280
+ isError: true,
281
+ };
282
+ }
283
+
284
+ const { data, error } = await supabase
285
+ .from('task_costs')
286
+ .select('*')
287
+ .eq('project_id', project_id)
288
+ .order('estimated_cost_usd', { ascending: false })
289
+ .limit(limit);
290
+
291
+ if (error) {
292
+ return {
293
+ result: { error: `Failed to get task costs: ${error.message}` },
294
+ isError: true,
295
+ };
296
+ }
297
+
298
+ const totalCost = (data || []).reduce(
299
+ (sum, task) => sum + parseFloat(task.estimated_cost_usd || '0'),
300
+ 0
301
+ );
302
+
303
+ return {
304
+ result: {
305
+ project_id,
306
+ tasks: data || [],
307
+ total_cost_usd: Math.round(totalCost * 100) / 100,
308
+ },
309
+ };
310
+ };
311
+
312
+ /**
313
+ * Cost handlers registry
314
+ */
315
+ export const costHandlers: HandlerRegistry = {
316
+ get_cost_summary: getCostSummary,
317
+ get_cost_alerts: getCostAlerts,
318
+ add_cost_alert: addCostAlert,
319
+ update_cost_alert: updateCostAlert,
320
+ delete_cost_alert: deleteCostAlert,
321
+ get_task_costs: getTaskCosts,
322
+ };
@@ -0,0 +1,401 @@
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 { logDecision, getDecisions, deleteDecision } from './decisions.js';
5
+ import { ValidationError } from '../validators.js';
6
+
7
+ // ============================================================================
8
+ // Test Utilities
9
+ // ============================================================================
10
+
11
+ function createMockSupabase(overrides: {
12
+ selectResult?: { data: unknown; error: unknown };
13
+ insertResult?: { data: unknown; error: unknown };
14
+ deleteResult?: { data: unknown; error: unknown };
15
+ } = {}) {
16
+ const defaultResult = { data: null, error: null };
17
+ let currentOperation = 'select';
18
+ let insertThenSelect = false;
19
+
20
+ const mock = {
21
+ from: vi.fn().mockReturnThis(),
22
+ select: vi.fn(() => {
23
+ if (currentOperation === 'insert') {
24
+ insertThenSelect = true;
25
+ } else {
26
+ currentOperation = 'select';
27
+ insertThenSelect = false;
28
+ }
29
+ return mock;
30
+ }),
31
+ insert: vi.fn(() => {
32
+ currentOperation = 'insert';
33
+ insertThenSelect = false;
34
+ return mock;
35
+ }),
36
+ delete: vi.fn(() => {
37
+ currentOperation = 'delete';
38
+ insertThenSelect = false;
39
+ return mock;
40
+ }),
41
+ eq: vi.fn().mockReturnThis(),
42
+ order: vi.fn().mockReturnThis(),
43
+ single: vi.fn(() => {
44
+ if (currentOperation === 'insert' || insertThenSelect) {
45
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
46
+ }
47
+ if (currentOperation === 'select') {
48
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
49
+ }
50
+ return Promise.resolve(defaultResult);
51
+ }),
52
+ then: vi.fn((resolve: (value: unknown) => void) => {
53
+ if (currentOperation === 'insert' || insertThenSelect) {
54
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
55
+ }
56
+ if (currentOperation === 'select') {
57
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
58
+ }
59
+ if (currentOperation === 'delete') {
60
+ return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
61
+ }
62
+ return Promise.resolve(defaultResult).then(resolve);
63
+ }),
64
+ };
65
+
66
+ return mock as unknown as SupabaseClient;
67
+ }
68
+
69
+ function createMockContext(
70
+ supabase: SupabaseClient,
71
+ options: { sessionId?: string | null } = {}
72
+ ): HandlerContext {
73
+ const defaultTokenUsage: TokenUsage = {
74
+ callCount: 5,
75
+ totalTokens: 2500,
76
+ byTool: {},
77
+ };
78
+
79
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
80
+
81
+ return {
82
+ supabase,
83
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
84
+ session: {
85
+ instanceId: 'instance-abc',
86
+ currentSessionId: sessionId,
87
+ currentPersona: 'Wave',
88
+ tokenUsage: defaultTokenUsage,
89
+ },
90
+ updateSession: vi.fn(),
91
+ };
92
+ }
93
+
94
+ // ============================================================================
95
+ // logDecision Tests
96
+ // ============================================================================
97
+
98
+ describe('logDecision', () => {
99
+ beforeEach(() => vi.clearAllMocks());
100
+
101
+ it('should throw error for missing project_id', async () => {
102
+ const supabase = createMockSupabase();
103
+ const ctx = createMockContext(supabase);
104
+
105
+ await expect(
106
+ logDecision({ title: 'Decision', description: 'Description' }, ctx)
107
+ ).rejects.toThrow(ValidationError);
108
+ });
109
+
110
+ it('should throw error for invalid project_id UUID', async () => {
111
+ const supabase = createMockSupabase();
112
+ const ctx = createMockContext(supabase);
113
+
114
+ await expect(
115
+ logDecision({ project_id: 'invalid', title: 'Decision', description: 'Description' }, ctx)
116
+ ).rejects.toThrow(ValidationError);
117
+ });
118
+
119
+ it('should throw error for missing title', async () => {
120
+ const supabase = createMockSupabase();
121
+ const ctx = createMockContext(supabase);
122
+
123
+ await expect(
124
+ logDecision({ project_id: '123e4567-e89b-12d3-a456-426614174000', description: 'Description' }, ctx)
125
+ ).rejects.toThrow(ValidationError);
126
+ });
127
+
128
+ it('should throw error for missing description', async () => {
129
+ const supabase = createMockSupabase();
130
+ const ctx = createMockContext(supabase);
131
+
132
+ await expect(
133
+ logDecision({ project_id: '123e4567-e89b-12d3-a456-426614174000', title: 'Decision' }, ctx)
134
+ ).rejects.toThrow(ValidationError);
135
+ });
136
+
137
+ it('should log decision successfully', async () => {
138
+ const supabase = createMockSupabase({
139
+ insertResult: { data: null, error: null },
140
+ });
141
+ const ctx = createMockContext(supabase);
142
+
143
+ const result = await logDecision(
144
+ {
145
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
146
+ title: 'Use PostgreSQL',
147
+ description: 'Decided to use PostgreSQL for the database',
148
+ },
149
+ ctx
150
+ );
151
+
152
+ expect(result.result).toMatchObject({
153
+ success: true,
154
+ title: 'Use PostgreSQL',
155
+ });
156
+ });
157
+
158
+ it('should log decision with rationale and alternatives', async () => {
159
+ const supabase = createMockSupabase({
160
+ insertResult: { data: null, error: null },
161
+ });
162
+ const ctx = createMockContext(supabase);
163
+
164
+ await logDecision(
165
+ {
166
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
167
+ title: 'Use PostgreSQL',
168
+ description: 'Decided to use PostgreSQL for the database',
169
+ rationale: 'Better JSON support and reliability',
170
+ alternatives_considered: ['MySQL', 'MongoDB'],
171
+ },
172
+ ctx
173
+ );
174
+
175
+ expect(supabase.insert).toHaveBeenCalledWith(
176
+ expect.objectContaining({
177
+ title: 'Use PostgreSQL',
178
+ description: 'Decided to use PostgreSQL for the database',
179
+ rationale: 'Better JSON support and reliability',
180
+ alternatives_considered: ['MySQL', 'MongoDB'],
181
+ created_by: 'agent',
182
+ created_by_session_id: 'session-123',
183
+ })
184
+ );
185
+ });
186
+
187
+ it('should include session_id in insert', async () => {
188
+ const supabase = createMockSupabase({
189
+ insertResult: { data: null, error: null },
190
+ });
191
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
192
+
193
+ await logDecision(
194
+ {
195
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
196
+ title: 'Decision',
197
+ description: 'Description',
198
+ },
199
+ ctx
200
+ );
201
+
202
+ expect(supabase.insert).toHaveBeenCalledWith(
203
+ expect.objectContaining({
204
+ created_by: 'agent',
205
+ created_by_session_id: 'my-session',
206
+ })
207
+ );
208
+ });
209
+
210
+ it('should throw error when insert fails', async () => {
211
+ const supabase = createMockSupabase({
212
+ insertResult: { data: null, error: { message: 'Insert failed' } },
213
+ });
214
+ const ctx = createMockContext(supabase);
215
+
216
+ await expect(
217
+ logDecision({
218
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
219
+ title: 'Decision',
220
+ description: 'Description',
221
+ }, ctx)
222
+ ).rejects.toThrow('Failed to log decision: Insert failed');
223
+ });
224
+ });
225
+
226
+ // ============================================================================
227
+ // getDecisions Tests
228
+ // ============================================================================
229
+
230
+ describe('getDecisions', () => {
231
+ beforeEach(() => vi.clearAllMocks());
232
+
233
+ it('should throw error for missing project_id', async () => {
234
+ const supabase = createMockSupabase();
235
+ const ctx = createMockContext(supabase);
236
+
237
+ await expect(getDecisions({}, ctx)).rejects.toThrow(ValidationError);
238
+ });
239
+
240
+ it('should throw error for invalid project_id UUID', async () => {
241
+ const supabase = createMockSupabase();
242
+ const ctx = createMockContext(supabase);
243
+
244
+ await expect(
245
+ getDecisions({ project_id: 'invalid' }, ctx)
246
+ ).rejects.toThrow(ValidationError);
247
+ });
248
+
249
+ it('should return empty list when no decisions', async () => {
250
+ const supabase = createMockSupabase({
251
+ selectResult: { data: [], error: null },
252
+ });
253
+ const ctx = createMockContext(supabase);
254
+
255
+ const result = await getDecisions(
256
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
257
+ ctx
258
+ );
259
+
260
+ expect(result.result).toMatchObject({
261
+ decisions: [],
262
+ });
263
+ });
264
+
265
+ it('should return decisions list', async () => {
266
+ const mockDecisions = [
267
+ { id: 'd1', title: 'Decision 1', description: 'Desc 1', created_at: '2025-01-14T10:00:00Z' },
268
+ { id: 'd2', title: 'Decision 2', description: 'Desc 2', created_at: '2025-01-14T11:00:00Z' },
269
+ ];
270
+
271
+ const supabase = createMockSupabase({
272
+ selectResult: { data: mockDecisions, error: null },
273
+ });
274
+ const ctx = createMockContext(supabase);
275
+
276
+ const result = await getDecisions(
277
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
278
+ ctx
279
+ );
280
+
281
+ expect((result.result as { decisions: unknown[] }).decisions).toHaveLength(2);
282
+ });
283
+
284
+ it('should query decisions table', async () => {
285
+ const supabase = createMockSupabase({
286
+ selectResult: { data: [], error: null },
287
+ });
288
+ const ctx = createMockContext(supabase);
289
+
290
+ await getDecisions(
291
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
292
+ ctx
293
+ );
294
+
295
+ expect(supabase.from).toHaveBeenCalledWith('decisions');
296
+ });
297
+
298
+ it('should order by created_at descending', async () => {
299
+ const supabase = createMockSupabase({
300
+ selectResult: { data: [], error: null },
301
+ });
302
+ const ctx = createMockContext(supabase);
303
+
304
+ await getDecisions(
305
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
306
+ ctx
307
+ );
308
+
309
+ expect(supabase.order).toHaveBeenCalledWith('created_at', { ascending: false });
310
+ });
311
+
312
+ it('should throw error when query fails', async () => {
313
+ const supabase = createMockSupabase();
314
+ const ctx = createMockContext(supabase);
315
+
316
+ // Override to return error
317
+ vi.mocked(supabase.from('').select).mockReturnValue({
318
+ ...supabase,
319
+ then: (resolve: (val: unknown) => void) =>
320
+ Promise.resolve({ data: null, error: { message: 'Query failed' } }).then(resolve),
321
+ } as unknown as ReturnType<SupabaseClient['from']>);
322
+
323
+ await expect(
324
+ getDecisions({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
325
+ ).rejects.toThrow('Failed to fetch decisions');
326
+ });
327
+ });
328
+
329
+ // ============================================================================
330
+ // deleteDecision Tests
331
+ // ============================================================================
332
+
333
+ describe('deleteDecision', () => {
334
+ beforeEach(() => vi.clearAllMocks());
335
+
336
+ it('should throw error for missing decision_id', async () => {
337
+ const supabase = createMockSupabase();
338
+ const ctx = createMockContext(supabase);
339
+
340
+ await expect(deleteDecision({}, ctx)).rejects.toThrow(ValidationError);
341
+ });
342
+
343
+ it('should throw error for invalid decision_id UUID', async () => {
344
+ const supabase = createMockSupabase();
345
+ const ctx = createMockContext(supabase);
346
+
347
+ await expect(
348
+ deleteDecision({ decision_id: 'invalid' }, ctx)
349
+ ).rejects.toThrow(ValidationError);
350
+ });
351
+
352
+ it('should delete decision successfully', async () => {
353
+ const supabase = createMockSupabase({
354
+ deleteResult: { data: null, error: null },
355
+ });
356
+ const ctx = createMockContext(supabase);
357
+
358
+ const result = await deleteDecision(
359
+ { decision_id: '123e4567-e89b-12d3-a456-426614174000' },
360
+ ctx
361
+ );
362
+
363
+ expect(result.result).toMatchObject({
364
+ success: true,
365
+ });
366
+ });
367
+
368
+ it('should call delete on decisions table', async () => {
369
+ const supabase = createMockSupabase({
370
+ deleteResult: { data: null, error: null },
371
+ });
372
+ const ctx = createMockContext(supabase);
373
+
374
+ await deleteDecision(
375
+ { decision_id: '123e4567-e89b-12d3-a456-426614174000' },
376
+ ctx
377
+ );
378
+
379
+ expect(supabase.from).toHaveBeenCalledWith('decisions');
380
+ expect(supabase.delete).toHaveBeenCalled();
381
+ expect(supabase.eq).toHaveBeenCalledWith('id', '123e4567-e89b-12d3-a456-426614174000');
382
+ });
383
+
384
+ it('should throw error when delete fails', async () => {
385
+ const supabase = createMockSupabase();
386
+ const ctx = createMockContext(supabase);
387
+
388
+ // Override delete to return error
389
+ vi.mocked(supabase.from('').delete).mockReturnValue({
390
+ ...supabase,
391
+ eq: vi.fn().mockReturnValue({
392
+ then: (resolve: (val: unknown) => void) =>
393
+ Promise.resolve({ data: null, error: { message: 'Delete failed' } }).then(resolve),
394
+ }),
395
+ } as unknown as ReturnType<SupabaseClient['from']>);
396
+
397
+ await expect(
398
+ deleteDecision({ decision_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
399
+ ).rejects.toThrow('Failed to delete decision');
400
+ });
401
+ });