@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.
- package/README.md +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- 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
|
+
});
|