@vibescope/mcp-server 0.4.5 → 0.4.7
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/CHANGELOG.md +84 -84
- package/README.md +194 -194
- package/dist/api-client/project.d.ts +1 -0
- package/dist/api-client.d.ts +4 -1
- package/dist/api-client.js +24 -7
- package/dist/cli-init.js +25 -24
- package/dist/cli.js +26 -26
- package/dist/handlers/chat.d.ts +2 -0
- package/dist/handlers/chat.js +25 -0
- package/dist/handlers/discovery.js +12 -0
- package/dist/handlers/project.js +4 -2
- package/dist/handlers/tool-docs.js +1203 -1137
- package/dist/handlers/version.js +1 -1
- package/dist/index.js +159 -87
- package/dist/setup.js +13 -7
- package/dist/templates/agent-guidelines.d.ts +1 -1
- package/dist/templates/agent-guidelines.js +205 -187
- package/dist/templates/help-content.js +1621 -1621
- package/dist/tools/bodies-of-work.js +6 -6
- package/dist/tools/chat.d.ts +1 -0
- package/dist/tools/chat.js +24 -0
- package/dist/tools/cloud-agents.js +22 -22
- package/dist/tools/features.d.ts +13 -0
- package/dist/tools/features.js +151 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +4 -1
- package/dist/tools/milestones.js +2 -2
- package/dist/tools/project.js +4 -0
- package/dist/tools/requests.js +1 -1
- package/dist/tools/session.js +11 -11
- package/dist/tools/sprints.js +9 -9
- package/dist/tools/tasks.js +35 -35
- package/dist/tools/worktrees.js +14 -14
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +3602 -0
- package/dist/utils.js +11 -11
- package/dist/version.d.ts +9 -3
- package/dist/version.js +56 -8
- package/docs/TOOLS.md +2663 -2559
- package/package.json +53 -53
- package/scripts/generate-docs.ts +212 -212
- package/scripts/version-bump.ts +203 -203
- package/src/api-client/blockers.ts +86 -86
- package/src/api-client/bodies-of-work.ts +194 -194
- package/src/api-client/chat.ts +50 -50
- package/src/api-client/connectors.ts +152 -152
- package/src/api-client/cost.ts +185 -185
- package/src/api-client/decisions.ts +87 -87
- package/src/api-client/deployment.ts +313 -313
- package/src/api-client/discovery.ts +81 -81
- package/src/api-client/fallback.ts +52 -52
- package/src/api-client/file-checkouts.ts +115 -115
- package/src/api-client/findings.ts +100 -100
- package/src/api-client/git-issues.ts +88 -88
- package/src/api-client/ideas.ts +112 -112
- package/src/api-client/index.ts +592 -592
- package/src/api-client/milestones.ts +83 -83
- package/src/api-client/organizations.ts +185 -185
- package/src/api-client/progress.ts +94 -94
- package/src/api-client/project.ts +180 -179
- package/src/api-client/requests.ts +54 -54
- package/src/api-client/session.ts +220 -220
- package/src/api-client/sprints.ts +227 -227
- package/src/api-client/subtasks.ts +57 -57
- package/src/api-client/tasks.ts +450 -450
- package/src/api-client/types.ts +32 -32
- package/src/api-client/validation.ts +60 -60
- package/src/api-client/worktrees.ts +53 -53
- package/src/api-client.test.ts +847 -847
- package/src/api-client.ts +2723 -2706
- package/src/cli-init.ts +558 -557
- package/src/cli.test.ts +284 -284
- package/src/cli.ts +204 -204
- package/src/handlers/__test-setup__.ts +240 -240
- package/src/handlers/__test-utils__.ts +89 -89
- package/src/handlers/blockers.test.ts +468 -468
- package/src/handlers/blockers.ts +172 -172
- package/src/handlers/bodies-of-work.test.ts +704 -704
- package/src/handlers/bodies-of-work.ts +526 -526
- package/src/handlers/chat.test.ts +185 -185
- package/src/handlers/chat.ts +101 -69
- package/src/handlers/cloud-agents.test.ts +438 -438
- package/src/handlers/cloud-agents.ts +156 -156
- package/src/handlers/connectors.test.ts +834 -834
- package/src/handlers/connectors.ts +229 -229
- package/src/handlers/cost.test.ts +462 -462
- package/src/handlers/cost.ts +285 -285
- package/src/handlers/decisions.test.ts +382 -382
- package/src/handlers/decisions.ts +153 -153
- package/src/handlers/deployment.test.ts +551 -551
- package/src/handlers/deployment.ts +570 -570
- package/src/handlers/discovery.test.ts +206 -206
- package/src/handlers/discovery.ts +427 -415
- package/src/handlers/fallback.test.ts +537 -537
- package/src/handlers/fallback.ts +194 -194
- package/src/handlers/file-checkouts.test.ts +750 -750
- package/src/handlers/file-checkouts.ts +185 -185
- package/src/handlers/findings.test.ts +633 -633
- package/src/handlers/findings.ts +239 -239
- package/src/handlers/git-issues.test.ts +631 -631
- package/src/handlers/git-issues.ts +136 -136
- package/src/handlers/ideas.test.ts +644 -644
- package/src/handlers/ideas.ts +207 -207
- package/src/handlers/index.ts +93 -93
- package/src/handlers/milestones.test.ts +475 -475
- package/src/handlers/milestones.ts +180 -180
- package/src/handlers/organizations.test.ts +826 -826
- package/src/handlers/organizations.ts +315 -315
- package/src/handlers/progress.test.ts +269 -269
- package/src/handlers/progress.ts +77 -77
- package/src/handlers/project.test.ts +546 -546
- package/src/handlers/project.ts +242 -239
- package/src/handlers/requests.test.ts +303 -303
- package/src/handlers/requests.ts +99 -99
- package/src/handlers/roles.test.ts +305 -305
- package/src/handlers/roles.ts +219 -219
- package/src/handlers/session.test.ts +998 -998
- package/src/handlers/session.ts +1105 -1105
- package/src/handlers/sprints.test.ts +732 -732
- package/src/handlers/sprints.ts +537 -537
- package/src/handlers/tasks.test.ts +931 -931
- package/src/handlers/tasks.ts +1133 -1133
- package/src/handlers/tool-categories.test.ts +66 -66
- package/src/handlers/tool-docs.test.ts +511 -511
- package/src/handlers/tool-docs.ts +1571 -1499
- package/src/handlers/types.test.ts +259 -259
- package/src/handlers/types.ts +176 -176
- package/src/handlers/validation.test.ts +582 -582
- package/src/handlers/validation.ts +164 -164
- package/src/handlers/version.ts +63 -63
- package/src/index.test.ts +674 -674
- package/src/index.ts +884 -807
- package/src/setup.test.ts +243 -233
- package/src/setup.ts +410 -404
- package/src/templates/agent-guidelines.ts +233 -215
- package/src/templates/help-content.ts +1751 -1751
- package/src/token-tracking.test.ts +463 -463
- package/src/token-tracking.ts +167 -167
- package/src/tools/blockers.ts +122 -122
- package/src/tools/bodies-of-work.ts +283 -283
- package/src/tools/chat.ts +72 -46
- package/src/tools/cloud-agents.ts +101 -101
- package/src/tools/connectors.ts +191 -191
- package/src/tools/cost.ts +111 -111
- package/src/tools/decisions.ts +111 -111
- package/src/tools/deployment.ts +455 -455
- package/src/tools/discovery.ts +76 -76
- package/src/tools/fallback.ts +111 -111
- package/src/tools/features.ts +154 -0
- package/src/tools/file-checkouts.ts +145 -145
- package/src/tools/findings.ts +101 -101
- package/src/tools/git-issues.ts +130 -130
- package/src/tools/ideas.ts +162 -162
- package/src/tools/index.ts +141 -137
- package/src/tools/milestones.ts +118 -118
- package/src/tools/organizations.ts +224 -224
- package/src/tools/progress.ts +73 -73
- package/src/tools/project.ts +206 -202
- package/src/tools/requests.ts +68 -68
- package/src/tools/roles.ts +112 -112
- package/src/tools/session.ts +181 -181
- package/src/tools/sprints.ts +298 -298
- package/src/tools/tasks.ts +550 -550
- package/src/tools/tools.test.ts +222 -222
- package/src/tools/types.ts +9 -9
- package/src/tools/validation.ts +75 -75
- package/src/tools/version.ts +34 -34
- package/src/tools/worktrees.ts +66 -66
- package/src/tools.test.ts +416 -416
- package/src/utils.test.ts +1014 -1014
- package/src/utils.ts +586 -586
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +249 -249
- package/src/version.ts +162 -109
- package/tsconfig.json +16 -16
- package/vitest.config.ts +14 -14
|
@@ -1,462 +1,462 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cost Handlers Unit Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
getCostSummary,
|
|
8
|
-
getCostAlerts,
|
|
9
|
-
addCostAlert,
|
|
10
|
-
updateCostAlert,
|
|
11
|
-
deleteCostAlert,
|
|
12
|
-
getTaskCosts,
|
|
13
|
-
} from './cost.js';
|
|
14
|
-
import { createMockContext } from './__test-utils__.js';
|
|
15
|
-
import { mockApiClient } from './__test-setup__.js';
|
|
16
|
-
import { ValidationError } from '../validators.js';
|
|
17
|
-
|
|
18
|
-
const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
19
|
-
|
|
20
|
-
describe('Cost Handlers', () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// ============================================================================
|
|
26
|
-
// getCostSummary
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
describe('getCostSummary', () => {
|
|
30
|
-
it('should throw ValidationError when project_id is missing', async () => {
|
|
31
|
-
const ctx = createMockContext();
|
|
32
|
-
|
|
33
|
-
await expect(getCostSummary({}, ctx)).rejects.toThrow(ValidationError);
|
|
34
|
-
await expect(getCostSummary({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should return daily cost summary with totals', async () => {
|
|
38
|
-
const mockData = [
|
|
39
|
-
{
|
|
40
|
-
date: '2025-01-14',
|
|
41
|
-
session_count: 5,
|
|
42
|
-
total_tokens: 10000,
|
|
43
|
-
total_calls: 50,
|
|
44
|
-
estimated_cost_usd: '0.50',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
date: '2025-01-13',
|
|
48
|
-
session_count: 3,
|
|
49
|
-
total_tokens: 6000,
|
|
50
|
-
total_calls: 30,
|
|
51
|
-
estimated_cost_usd: '0.30',
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
mockApiClient.getCostSummary.mockResolvedValue({
|
|
55
|
-
ok: true,
|
|
56
|
-
data: {
|
|
57
|
-
period: 'daily',
|
|
58
|
-
summary: mockData,
|
|
59
|
-
totals: {
|
|
60
|
-
sessions: 8,
|
|
61
|
-
tokens: 16000,
|
|
62
|
-
calls: 80,
|
|
63
|
-
cost: 0.8,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
const ctx = createMockContext();
|
|
68
|
-
|
|
69
|
-
const result = await getCostSummary({ project_id: VALID_UUID }, ctx);
|
|
70
|
-
|
|
71
|
-
expect(result.isError).toBeUndefined();
|
|
72
|
-
expect(result.result.period).toBe('daily');
|
|
73
|
-
expect(result.result.summary).toEqual(mockData);
|
|
74
|
-
expect(result.result.totals).toEqual({
|
|
75
|
-
sessions: 8,
|
|
76
|
-
tokens: 16000,
|
|
77
|
-
calls: 80,
|
|
78
|
-
cost: 0.8,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should handle weekly period', async () => {
|
|
83
|
-
mockApiClient.getCostSummary.mockResolvedValue({
|
|
84
|
-
ok: true,
|
|
85
|
-
data: { period: 'weekly', summary: [], totals: { sessions: 0, tokens: 0, calls: 0, cost: 0 } },
|
|
86
|
-
});
|
|
87
|
-
const ctx = createMockContext();
|
|
88
|
-
|
|
89
|
-
const result = await getCostSummary(
|
|
90
|
-
{ project_id: VALID_UUID, period: 'weekly' },
|
|
91
|
-
ctx
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
expect(result.result.period).toBe('weekly');
|
|
95
|
-
expect(mockApiClient.getCostSummary).toHaveBeenCalledWith(
|
|
96
|
-
VALID_UUID,
|
|
97
|
-
expect.objectContaining({ period: 'weekly' })
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should handle monthly period', async () => {
|
|
102
|
-
mockApiClient.getCostSummary.mockResolvedValue({
|
|
103
|
-
ok: true,
|
|
104
|
-
data: { period: 'monthly', summary: [], totals: { sessions: 0, tokens: 0, calls: 0, cost: 0 } },
|
|
105
|
-
});
|
|
106
|
-
const ctx = createMockContext();
|
|
107
|
-
|
|
108
|
-
const result = await getCostSummary(
|
|
109
|
-
{ project_id: VALID_UUID, period: 'monthly' },
|
|
110
|
-
ctx
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
expect(result.result.period).toBe('monthly');
|
|
114
|
-
expect(mockApiClient.getCostSummary).toHaveBeenCalledWith(
|
|
115
|
-
VALID_UUID,
|
|
116
|
-
expect.objectContaining({ period: 'monthly' })
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should return error on API failure', async () => {
|
|
121
|
-
mockApiClient.getCostSummary.mockResolvedValue({
|
|
122
|
-
ok: false,
|
|
123
|
-
error: 'Database error',
|
|
124
|
-
});
|
|
125
|
-
const ctx = createMockContext();
|
|
126
|
-
|
|
127
|
-
const result = await getCostSummary({ project_id: VALID_UUID }, ctx);
|
|
128
|
-
|
|
129
|
-
expect(result.isError).toBe(true);
|
|
130
|
-
expect(result.result.error).toBe('Database error');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// ============================================================================
|
|
135
|
-
// getCostAlerts
|
|
136
|
-
// ============================================================================
|
|
137
|
-
|
|
138
|
-
describe('getCostAlerts', () => {
|
|
139
|
-
it('should return all alerts for user', async () => {
|
|
140
|
-
const mockAlerts = [
|
|
141
|
-
{ id: 'alert-1', threshold_amount: 10, threshold_period: 'daily' },
|
|
142
|
-
{ id: 'alert-2', threshold_amount: 50, threshold_period: 'weekly' },
|
|
143
|
-
];
|
|
144
|
-
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
145
|
-
ok: true,
|
|
146
|
-
data: { alerts: mockAlerts, count: 2 },
|
|
147
|
-
});
|
|
148
|
-
const ctx = createMockContext();
|
|
149
|
-
|
|
150
|
-
const result = await getCostAlerts({}, ctx);
|
|
151
|
-
|
|
152
|
-
expect(result.result.alerts).toEqual(mockAlerts);
|
|
153
|
-
expect(result.result.count).toBe(2);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should call getCostAlerts with no args (project filtering done server-side)', async () => {
|
|
157
|
-
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
158
|
-
ok: true,
|
|
159
|
-
data: { alerts: [], count: 0 },
|
|
160
|
-
});
|
|
161
|
-
const ctx = createMockContext();
|
|
162
|
-
|
|
163
|
-
await getCostAlerts({ project_id: VALID_UUID }, ctx);
|
|
164
|
-
|
|
165
|
-
expect(mockApiClient.getCostAlerts).toHaveBeenCalledWith();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should return error on API failure', async () => {
|
|
169
|
-
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
170
|
-
ok: false,
|
|
171
|
-
error: 'Query failed',
|
|
172
|
-
});
|
|
173
|
-
const ctx = createMockContext();
|
|
174
|
-
|
|
175
|
-
const result = await getCostAlerts({}, ctx);
|
|
176
|
-
|
|
177
|
-
expect(result.isError).toBe(true);
|
|
178
|
-
expect(result.result.error).toBe('Query failed');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// addCostAlert
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
describe('addCostAlert', () => {
|
|
187
|
-
it('should throw ValidationError when threshold_amount is missing', async () => {
|
|
188
|
-
const ctx = createMockContext();
|
|
189
|
-
|
|
190
|
-
await expect(
|
|
191
|
-
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
192
|
-
).rejects.toThrow(ValidationError);
|
|
193
|
-
await expect(
|
|
194
|
-
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
195
|
-
).rejects.toThrow('Missing required field: threshold_amount');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should throw ValidationError when threshold_amount is not positive', async () => {
|
|
199
|
-
const ctx = createMockContext();
|
|
200
|
-
|
|
201
|
-
await expect(
|
|
202
|
-
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
203
|
-
).rejects.toThrow(ValidationError);
|
|
204
|
-
await expect(
|
|
205
|
-
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
206
|
-
).rejects.toThrow('threshold_amount must be a positive number');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should throw ValidationError when threshold_period is invalid', async () => {
|
|
210
|
-
const ctx = createMockContext();
|
|
211
|
-
|
|
212
|
-
await expect(
|
|
213
|
-
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
214
|
-
).rejects.toThrow(ValidationError);
|
|
215
|
-
await expect(
|
|
216
|
-
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
217
|
-
).rejects.toThrow('Invalid threshold_period');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should create alert successfully', async () => {
|
|
221
|
-
const mockAlert = {
|
|
222
|
-
id: 'new-alert',
|
|
223
|
-
threshold_amount: 10,
|
|
224
|
-
threshold_period: 'daily',
|
|
225
|
-
alert_type: 'warning',
|
|
226
|
-
};
|
|
227
|
-
mockApiClient.addCostAlert.mockResolvedValue({
|
|
228
|
-
ok: true,
|
|
229
|
-
data: { success: true, alert: mockAlert, message: 'Alert created' },
|
|
230
|
-
});
|
|
231
|
-
const ctx = createMockContext();
|
|
232
|
-
|
|
233
|
-
const result = await addCostAlert(
|
|
234
|
-
{ threshold_amount: 10, threshold_period: 'daily' },
|
|
235
|
-
ctx
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
expect(result.result.success).toBe(true);
|
|
239
|
-
expect(result.result.alert).toEqual(mockAlert);
|
|
240
|
-
expect(result.result.message).toContain('Alert created');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should create alert with project_id', async () => {
|
|
244
|
-
mockApiClient.addCostAlert.mockResolvedValue({
|
|
245
|
-
ok: true,
|
|
246
|
-
data: { success: true, alert: { id: 'new-alert' } },
|
|
247
|
-
});
|
|
248
|
-
const ctx = createMockContext();
|
|
249
|
-
|
|
250
|
-
await addCostAlert(
|
|
251
|
-
{
|
|
252
|
-
project_id: VALID_UUID,
|
|
253
|
-
threshold_amount: 10,
|
|
254
|
-
threshold_period: 'daily',
|
|
255
|
-
},
|
|
256
|
-
ctx
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
expect(mockApiClient.addCostAlert).toHaveBeenCalledWith(
|
|
260
|
-
expect.objectContaining({
|
|
261
|
-
project_id: VALID_UUID,
|
|
262
|
-
threshold_amount: 10,
|
|
263
|
-
threshold_period: 'daily',
|
|
264
|
-
})
|
|
265
|
-
);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('should return error on API failure', async () => {
|
|
269
|
-
mockApiClient.addCostAlert.mockResolvedValue({
|
|
270
|
-
ok: false,
|
|
271
|
-
error: 'Insert failed',
|
|
272
|
-
});
|
|
273
|
-
const ctx = createMockContext();
|
|
274
|
-
|
|
275
|
-
const result = await addCostAlert(
|
|
276
|
-
{ threshold_amount: 10, threshold_period: 'daily' },
|
|
277
|
-
ctx
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
expect(result.isError).toBe(true);
|
|
281
|
-
expect(result.result.error).toBe('Insert failed');
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// ============================================================================
|
|
286
|
-
// updateCostAlert
|
|
287
|
-
// ============================================================================
|
|
288
|
-
|
|
289
|
-
describe('updateCostAlert', () => {
|
|
290
|
-
it('should throw ValidationError when alert_id is missing', async () => {
|
|
291
|
-
const ctx = createMockContext();
|
|
292
|
-
|
|
293
|
-
await expect(updateCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
294
|
-
await expect(updateCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should return error when no updates provided', async () => {
|
|
298
|
-
const ctx = createMockContext();
|
|
299
|
-
|
|
300
|
-
const result = await updateCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
301
|
-
|
|
302
|
-
expect(result.isError).toBe(true);
|
|
303
|
-
expect(result.result.error).toBe('No updates provided');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('should update alert successfully', async () => {
|
|
307
|
-
const mockUpdatedAlert = {
|
|
308
|
-
id: VALID_UUID,
|
|
309
|
-
threshold_amount: 20,
|
|
310
|
-
threshold_period: 'weekly',
|
|
311
|
-
};
|
|
312
|
-
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
313
|
-
ok: true,
|
|
314
|
-
data: { success: true, alert: mockUpdatedAlert },
|
|
315
|
-
});
|
|
316
|
-
const ctx = createMockContext();
|
|
317
|
-
|
|
318
|
-
const result = await updateCostAlert(
|
|
319
|
-
{ alert_id: VALID_UUID, threshold_amount: 20 },
|
|
320
|
-
ctx
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
expect(result.result.success).toBe(true);
|
|
324
|
-
expect(result.result.alert).toEqual(mockUpdatedAlert);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('should update enabled status', async () => {
|
|
328
|
-
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
329
|
-
ok: true,
|
|
330
|
-
data: { success: true, alert: { id: VALID_UUID, enabled: false } },
|
|
331
|
-
});
|
|
332
|
-
const ctx = createMockContext();
|
|
333
|
-
|
|
334
|
-
await updateCostAlert({ alert_id: VALID_UUID, enabled: false }, ctx);
|
|
335
|
-
|
|
336
|
-
expect(mockApiClient.updateCostAlert).toHaveBeenCalledWith(
|
|
337
|
-
VALID_UUID,
|
|
338
|
-
expect.objectContaining({ enabled: false })
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('should return error on API failure', async () => {
|
|
343
|
-
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
344
|
-
ok: false,
|
|
345
|
-
error: 'Update failed',
|
|
346
|
-
});
|
|
347
|
-
const ctx = createMockContext();
|
|
348
|
-
|
|
349
|
-
const result = await updateCostAlert(
|
|
350
|
-
{ alert_id: VALID_UUID, threshold_amount: 20 },
|
|
351
|
-
ctx
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
expect(result.isError).toBe(true);
|
|
355
|
-
expect(result.result.error).toBe('Update failed');
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// ============================================================================
|
|
360
|
-
// deleteCostAlert
|
|
361
|
-
// ============================================================================
|
|
362
|
-
|
|
363
|
-
describe('deleteCostAlert', () => {
|
|
364
|
-
it('should throw ValidationError when alert_id is missing', async () => {
|
|
365
|
-
const ctx = createMockContext();
|
|
366
|
-
|
|
367
|
-
await expect(deleteCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
368
|
-
await expect(deleteCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should delete alert successfully', async () => {
|
|
372
|
-
mockApiClient.deleteCostAlert.mockResolvedValue({
|
|
373
|
-
ok: true,
|
|
374
|
-
data: { success: true, deleted_alert_id: VALID_UUID },
|
|
375
|
-
});
|
|
376
|
-
const ctx = createMockContext();
|
|
377
|
-
|
|
378
|
-
const result = await deleteCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
379
|
-
|
|
380
|
-
expect(result.result.success).toBe(true);
|
|
381
|
-
expect(result.result.deleted_alert_id).toBe(VALID_UUID);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('should return error on API failure', async () => {
|
|
385
|
-
mockApiClient.deleteCostAlert.mockResolvedValue({
|
|
386
|
-
ok: false,
|
|
387
|
-
error: 'Delete failed',
|
|
388
|
-
});
|
|
389
|
-
const ctx = createMockContext();
|
|
390
|
-
|
|
391
|
-
const result = await deleteCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
392
|
-
|
|
393
|
-
expect(result.isError).toBe(true);
|
|
394
|
-
expect(result.result.error).toBe('Delete failed');
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// ============================================================================
|
|
399
|
-
// getTaskCosts
|
|
400
|
-
// ============================================================================
|
|
401
|
-
|
|
402
|
-
describe('getTaskCosts', () => {
|
|
403
|
-
it('should throw ValidationError when project_id is missing', async () => {
|
|
404
|
-
const ctx = createMockContext();
|
|
405
|
-
|
|
406
|
-
await expect(getTaskCosts({}, ctx)).rejects.toThrow(ValidationError);
|
|
407
|
-
await expect(getTaskCosts({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it('should return task costs with total', async () => {
|
|
411
|
-
const mockTasks = [
|
|
412
|
-
{ id: 'task-1', title: 'Task 1', estimated_cost_usd: '0.50' },
|
|
413
|
-
{ id: 'task-2', title: 'Task 2', estimated_cost_usd: '0.30' },
|
|
414
|
-
];
|
|
415
|
-
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
416
|
-
ok: true,
|
|
417
|
-
data: {
|
|
418
|
-
project_id: VALID_UUID,
|
|
419
|
-
tasks: mockTasks,
|
|
420
|
-
total_cost_usd: 0.8,
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
const ctx = createMockContext();
|
|
424
|
-
|
|
425
|
-
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
426
|
-
|
|
427
|
-
expect(result.result.project_id).toBe(VALID_UUID);
|
|
428
|
-
expect(result.result.tasks).toEqual(mockTasks);
|
|
429
|
-
expect(result.result.total_cost_usd).toBe(0.8);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('should handle empty task list', async () => {
|
|
433
|
-
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
434
|
-
ok: true,
|
|
435
|
-
data: {
|
|
436
|
-
project_id: VALID_UUID,
|
|
437
|
-
tasks: [],
|
|
438
|
-
total_cost_usd: 0,
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
const ctx = createMockContext();
|
|
442
|
-
|
|
443
|
-
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
444
|
-
|
|
445
|
-
expect(result.result.tasks).toEqual([]);
|
|
446
|
-
expect(result.result.total_cost_usd).toBe(0);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('should return error on API failure', async () => {
|
|
450
|
-
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
451
|
-
ok: false,
|
|
452
|
-
error: 'Query failed',
|
|
453
|
-
});
|
|
454
|
-
const ctx = createMockContext();
|
|
455
|
-
|
|
456
|
-
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
457
|
-
|
|
458
|
-
expect(result.isError).toBe(true);
|
|
459
|
-
expect(result.result.error).toBe('Query failed');
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Cost Handlers Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
getCostSummary,
|
|
8
|
+
getCostAlerts,
|
|
9
|
+
addCostAlert,
|
|
10
|
+
updateCostAlert,
|
|
11
|
+
deleteCostAlert,
|
|
12
|
+
getTaskCosts,
|
|
13
|
+
} from './cost.js';
|
|
14
|
+
import { createMockContext } from './__test-utils__.js';
|
|
15
|
+
import { mockApiClient } from './__test-setup__.js';
|
|
16
|
+
import { ValidationError } from '../validators.js';
|
|
17
|
+
|
|
18
|
+
const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
19
|
+
|
|
20
|
+
describe('Cost Handlers', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// getCostSummary
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
describe('getCostSummary', () => {
|
|
30
|
+
it('should throw ValidationError when project_id is missing', async () => {
|
|
31
|
+
const ctx = createMockContext();
|
|
32
|
+
|
|
33
|
+
await expect(getCostSummary({}, ctx)).rejects.toThrow(ValidationError);
|
|
34
|
+
await expect(getCostSummary({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return daily cost summary with totals', async () => {
|
|
38
|
+
const mockData = [
|
|
39
|
+
{
|
|
40
|
+
date: '2025-01-14',
|
|
41
|
+
session_count: 5,
|
|
42
|
+
total_tokens: 10000,
|
|
43
|
+
total_calls: 50,
|
|
44
|
+
estimated_cost_usd: '0.50',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
date: '2025-01-13',
|
|
48
|
+
session_count: 3,
|
|
49
|
+
total_tokens: 6000,
|
|
50
|
+
total_calls: 30,
|
|
51
|
+
estimated_cost_usd: '0.30',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
mockApiClient.getCostSummary.mockResolvedValue({
|
|
55
|
+
ok: true,
|
|
56
|
+
data: {
|
|
57
|
+
period: 'daily',
|
|
58
|
+
summary: mockData,
|
|
59
|
+
totals: {
|
|
60
|
+
sessions: 8,
|
|
61
|
+
tokens: 16000,
|
|
62
|
+
calls: 80,
|
|
63
|
+
cost: 0.8,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const ctx = createMockContext();
|
|
68
|
+
|
|
69
|
+
const result = await getCostSummary({ project_id: VALID_UUID }, ctx);
|
|
70
|
+
|
|
71
|
+
expect(result.isError).toBeUndefined();
|
|
72
|
+
expect(result.result.period).toBe('daily');
|
|
73
|
+
expect(result.result.summary).toEqual(mockData);
|
|
74
|
+
expect(result.result.totals).toEqual({
|
|
75
|
+
sessions: 8,
|
|
76
|
+
tokens: 16000,
|
|
77
|
+
calls: 80,
|
|
78
|
+
cost: 0.8,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle weekly period', async () => {
|
|
83
|
+
mockApiClient.getCostSummary.mockResolvedValue({
|
|
84
|
+
ok: true,
|
|
85
|
+
data: { period: 'weekly', summary: [], totals: { sessions: 0, tokens: 0, calls: 0, cost: 0 } },
|
|
86
|
+
});
|
|
87
|
+
const ctx = createMockContext();
|
|
88
|
+
|
|
89
|
+
const result = await getCostSummary(
|
|
90
|
+
{ project_id: VALID_UUID, period: 'weekly' },
|
|
91
|
+
ctx
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.result.period).toBe('weekly');
|
|
95
|
+
expect(mockApiClient.getCostSummary).toHaveBeenCalledWith(
|
|
96
|
+
VALID_UUID,
|
|
97
|
+
expect.objectContaining({ period: 'weekly' })
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle monthly period', async () => {
|
|
102
|
+
mockApiClient.getCostSummary.mockResolvedValue({
|
|
103
|
+
ok: true,
|
|
104
|
+
data: { period: 'monthly', summary: [], totals: { sessions: 0, tokens: 0, calls: 0, cost: 0 } },
|
|
105
|
+
});
|
|
106
|
+
const ctx = createMockContext();
|
|
107
|
+
|
|
108
|
+
const result = await getCostSummary(
|
|
109
|
+
{ project_id: VALID_UUID, period: 'monthly' },
|
|
110
|
+
ctx
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(result.result.period).toBe('monthly');
|
|
114
|
+
expect(mockApiClient.getCostSummary).toHaveBeenCalledWith(
|
|
115
|
+
VALID_UUID,
|
|
116
|
+
expect.objectContaining({ period: 'monthly' })
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return error on API failure', async () => {
|
|
121
|
+
mockApiClient.getCostSummary.mockResolvedValue({
|
|
122
|
+
ok: false,
|
|
123
|
+
error: 'Database error',
|
|
124
|
+
});
|
|
125
|
+
const ctx = createMockContext();
|
|
126
|
+
|
|
127
|
+
const result = await getCostSummary({ project_id: VALID_UUID }, ctx);
|
|
128
|
+
|
|
129
|
+
expect(result.isError).toBe(true);
|
|
130
|
+
expect(result.result.error).toBe('Database error');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// getCostAlerts
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
describe('getCostAlerts', () => {
|
|
139
|
+
it('should return all alerts for user', async () => {
|
|
140
|
+
const mockAlerts = [
|
|
141
|
+
{ id: 'alert-1', threshold_amount: 10, threshold_period: 'daily' },
|
|
142
|
+
{ id: 'alert-2', threshold_amount: 50, threshold_period: 'weekly' },
|
|
143
|
+
];
|
|
144
|
+
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
data: { alerts: mockAlerts, count: 2 },
|
|
147
|
+
});
|
|
148
|
+
const ctx = createMockContext();
|
|
149
|
+
|
|
150
|
+
const result = await getCostAlerts({}, ctx);
|
|
151
|
+
|
|
152
|
+
expect(result.result.alerts).toEqual(mockAlerts);
|
|
153
|
+
expect(result.result.count).toBe(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should call getCostAlerts with no args (project filtering done server-side)', async () => {
|
|
157
|
+
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
158
|
+
ok: true,
|
|
159
|
+
data: { alerts: [], count: 0 },
|
|
160
|
+
});
|
|
161
|
+
const ctx = createMockContext();
|
|
162
|
+
|
|
163
|
+
await getCostAlerts({ project_id: VALID_UUID }, ctx);
|
|
164
|
+
|
|
165
|
+
expect(mockApiClient.getCostAlerts).toHaveBeenCalledWith();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return error on API failure', async () => {
|
|
169
|
+
mockApiClient.getCostAlerts.mockResolvedValue({
|
|
170
|
+
ok: false,
|
|
171
|
+
error: 'Query failed',
|
|
172
|
+
});
|
|
173
|
+
const ctx = createMockContext();
|
|
174
|
+
|
|
175
|
+
const result = await getCostAlerts({}, ctx);
|
|
176
|
+
|
|
177
|
+
expect(result.isError).toBe(true);
|
|
178
|
+
expect(result.result.error).toBe('Query failed');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// addCostAlert
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
describe('addCostAlert', () => {
|
|
187
|
+
it('should throw ValidationError when threshold_amount is missing', async () => {
|
|
188
|
+
const ctx = createMockContext();
|
|
189
|
+
|
|
190
|
+
await expect(
|
|
191
|
+
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
192
|
+
).rejects.toThrow(ValidationError);
|
|
193
|
+
await expect(
|
|
194
|
+
addCostAlert({ threshold_period: 'daily' }, ctx)
|
|
195
|
+
).rejects.toThrow('Missing required field: threshold_amount');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should throw ValidationError when threshold_amount is not positive', async () => {
|
|
199
|
+
const ctx = createMockContext();
|
|
200
|
+
|
|
201
|
+
await expect(
|
|
202
|
+
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
203
|
+
).rejects.toThrow(ValidationError);
|
|
204
|
+
await expect(
|
|
205
|
+
addCostAlert({ threshold_amount: -5, threshold_period: 'daily' }, ctx)
|
|
206
|
+
).rejects.toThrow('threshold_amount must be a positive number');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should throw ValidationError when threshold_period is invalid', async () => {
|
|
210
|
+
const ctx = createMockContext();
|
|
211
|
+
|
|
212
|
+
await expect(
|
|
213
|
+
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
214
|
+
).rejects.toThrow(ValidationError);
|
|
215
|
+
await expect(
|
|
216
|
+
addCostAlert({ threshold_amount: 10, threshold_period: 'yearly' }, ctx)
|
|
217
|
+
).rejects.toThrow('Invalid threshold_period');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should create alert successfully', async () => {
|
|
221
|
+
const mockAlert = {
|
|
222
|
+
id: 'new-alert',
|
|
223
|
+
threshold_amount: 10,
|
|
224
|
+
threshold_period: 'daily',
|
|
225
|
+
alert_type: 'warning',
|
|
226
|
+
};
|
|
227
|
+
mockApiClient.addCostAlert.mockResolvedValue({
|
|
228
|
+
ok: true,
|
|
229
|
+
data: { success: true, alert: mockAlert, message: 'Alert created' },
|
|
230
|
+
});
|
|
231
|
+
const ctx = createMockContext();
|
|
232
|
+
|
|
233
|
+
const result = await addCostAlert(
|
|
234
|
+
{ threshold_amount: 10, threshold_period: 'daily' },
|
|
235
|
+
ctx
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(result.result.success).toBe(true);
|
|
239
|
+
expect(result.result.alert).toEqual(mockAlert);
|
|
240
|
+
expect(result.result.message).toContain('Alert created');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should create alert with project_id', async () => {
|
|
244
|
+
mockApiClient.addCostAlert.mockResolvedValue({
|
|
245
|
+
ok: true,
|
|
246
|
+
data: { success: true, alert: { id: 'new-alert' } },
|
|
247
|
+
});
|
|
248
|
+
const ctx = createMockContext();
|
|
249
|
+
|
|
250
|
+
await addCostAlert(
|
|
251
|
+
{
|
|
252
|
+
project_id: VALID_UUID,
|
|
253
|
+
threshold_amount: 10,
|
|
254
|
+
threshold_period: 'daily',
|
|
255
|
+
},
|
|
256
|
+
ctx
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(mockApiClient.addCostAlert).toHaveBeenCalledWith(
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
project_id: VALID_UUID,
|
|
262
|
+
threshold_amount: 10,
|
|
263
|
+
threshold_period: 'daily',
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return error on API failure', async () => {
|
|
269
|
+
mockApiClient.addCostAlert.mockResolvedValue({
|
|
270
|
+
ok: false,
|
|
271
|
+
error: 'Insert failed',
|
|
272
|
+
});
|
|
273
|
+
const ctx = createMockContext();
|
|
274
|
+
|
|
275
|
+
const result = await addCostAlert(
|
|
276
|
+
{ threshold_amount: 10, threshold_period: 'daily' },
|
|
277
|
+
ctx
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(result.isError).toBe(true);
|
|
281
|
+
expect(result.result.error).toBe('Insert failed');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// updateCostAlert
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
describe('updateCostAlert', () => {
|
|
290
|
+
it('should throw ValidationError when alert_id is missing', async () => {
|
|
291
|
+
const ctx = createMockContext();
|
|
292
|
+
|
|
293
|
+
await expect(updateCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
294
|
+
await expect(updateCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return error when no updates provided', async () => {
|
|
298
|
+
const ctx = createMockContext();
|
|
299
|
+
|
|
300
|
+
const result = await updateCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
301
|
+
|
|
302
|
+
expect(result.isError).toBe(true);
|
|
303
|
+
expect(result.result.error).toBe('No updates provided');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should update alert successfully', async () => {
|
|
307
|
+
const mockUpdatedAlert = {
|
|
308
|
+
id: VALID_UUID,
|
|
309
|
+
threshold_amount: 20,
|
|
310
|
+
threshold_period: 'weekly',
|
|
311
|
+
};
|
|
312
|
+
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
313
|
+
ok: true,
|
|
314
|
+
data: { success: true, alert: mockUpdatedAlert },
|
|
315
|
+
});
|
|
316
|
+
const ctx = createMockContext();
|
|
317
|
+
|
|
318
|
+
const result = await updateCostAlert(
|
|
319
|
+
{ alert_id: VALID_UUID, threshold_amount: 20 },
|
|
320
|
+
ctx
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(result.result.success).toBe(true);
|
|
324
|
+
expect(result.result.alert).toEqual(mockUpdatedAlert);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should update enabled status', async () => {
|
|
328
|
+
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
329
|
+
ok: true,
|
|
330
|
+
data: { success: true, alert: { id: VALID_UUID, enabled: false } },
|
|
331
|
+
});
|
|
332
|
+
const ctx = createMockContext();
|
|
333
|
+
|
|
334
|
+
await updateCostAlert({ alert_id: VALID_UUID, enabled: false }, ctx);
|
|
335
|
+
|
|
336
|
+
expect(mockApiClient.updateCostAlert).toHaveBeenCalledWith(
|
|
337
|
+
VALID_UUID,
|
|
338
|
+
expect.objectContaining({ enabled: false })
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should return error on API failure', async () => {
|
|
343
|
+
mockApiClient.updateCostAlert.mockResolvedValue({
|
|
344
|
+
ok: false,
|
|
345
|
+
error: 'Update failed',
|
|
346
|
+
});
|
|
347
|
+
const ctx = createMockContext();
|
|
348
|
+
|
|
349
|
+
const result = await updateCostAlert(
|
|
350
|
+
{ alert_id: VALID_UUID, threshold_amount: 20 },
|
|
351
|
+
ctx
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
expect(result.isError).toBe(true);
|
|
355
|
+
expect(result.result.error).toBe('Update failed');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// deleteCostAlert
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
describe('deleteCostAlert', () => {
|
|
364
|
+
it('should throw ValidationError when alert_id is missing', async () => {
|
|
365
|
+
const ctx = createMockContext();
|
|
366
|
+
|
|
367
|
+
await expect(deleteCostAlert({}, ctx)).rejects.toThrow(ValidationError);
|
|
368
|
+
await expect(deleteCostAlert({}, ctx)).rejects.toThrow('Missing required field: alert_id');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should delete alert successfully', async () => {
|
|
372
|
+
mockApiClient.deleteCostAlert.mockResolvedValue({
|
|
373
|
+
ok: true,
|
|
374
|
+
data: { success: true, deleted_alert_id: VALID_UUID },
|
|
375
|
+
});
|
|
376
|
+
const ctx = createMockContext();
|
|
377
|
+
|
|
378
|
+
const result = await deleteCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
379
|
+
|
|
380
|
+
expect(result.result.success).toBe(true);
|
|
381
|
+
expect(result.result.deleted_alert_id).toBe(VALID_UUID);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should return error on API failure', async () => {
|
|
385
|
+
mockApiClient.deleteCostAlert.mockResolvedValue({
|
|
386
|
+
ok: false,
|
|
387
|
+
error: 'Delete failed',
|
|
388
|
+
});
|
|
389
|
+
const ctx = createMockContext();
|
|
390
|
+
|
|
391
|
+
const result = await deleteCostAlert({ alert_id: VALID_UUID }, ctx);
|
|
392
|
+
|
|
393
|
+
expect(result.isError).toBe(true);
|
|
394
|
+
expect(result.result.error).toBe('Delete failed');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// getTaskCosts
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
describe('getTaskCosts', () => {
|
|
403
|
+
it('should throw ValidationError when project_id is missing', async () => {
|
|
404
|
+
const ctx = createMockContext();
|
|
405
|
+
|
|
406
|
+
await expect(getTaskCosts({}, ctx)).rejects.toThrow(ValidationError);
|
|
407
|
+
await expect(getTaskCosts({}, ctx)).rejects.toThrow('Missing required field: project_id');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should return task costs with total', async () => {
|
|
411
|
+
const mockTasks = [
|
|
412
|
+
{ id: 'task-1', title: 'Task 1', estimated_cost_usd: '0.50' },
|
|
413
|
+
{ id: 'task-2', title: 'Task 2', estimated_cost_usd: '0.30' },
|
|
414
|
+
];
|
|
415
|
+
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
416
|
+
ok: true,
|
|
417
|
+
data: {
|
|
418
|
+
project_id: VALID_UUID,
|
|
419
|
+
tasks: mockTasks,
|
|
420
|
+
total_cost_usd: 0.8,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
const ctx = createMockContext();
|
|
424
|
+
|
|
425
|
+
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
426
|
+
|
|
427
|
+
expect(result.result.project_id).toBe(VALID_UUID);
|
|
428
|
+
expect(result.result.tasks).toEqual(mockTasks);
|
|
429
|
+
expect(result.result.total_cost_usd).toBe(0.8);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should handle empty task list', async () => {
|
|
433
|
+
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
434
|
+
ok: true,
|
|
435
|
+
data: {
|
|
436
|
+
project_id: VALID_UUID,
|
|
437
|
+
tasks: [],
|
|
438
|
+
total_cost_usd: 0,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
const ctx = createMockContext();
|
|
442
|
+
|
|
443
|
+
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
444
|
+
|
|
445
|
+
expect(result.result.tasks).toEqual([]);
|
|
446
|
+
expect(result.result.total_cost_usd).toBe(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should return error on API failure', async () => {
|
|
450
|
+
mockApiClient.getTaskCosts.mockResolvedValue({
|
|
451
|
+
ok: false,
|
|
452
|
+
error: 'Query failed',
|
|
453
|
+
});
|
|
454
|
+
const ctx = createMockContext();
|
|
455
|
+
|
|
456
|
+
const result = await getTaskCosts({ project_id: VALID_UUID }, ctx);
|
|
457
|
+
|
|
458
|
+
expect(result.isError).toBe(true);
|
|
459
|
+
expect(result.result.error).toBe('Query failed');
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
});
|