@vibescope/mcp-server 0.2.0 → 0.2.2
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 +60 -7
- package/dist/api-client.d.ts +251 -1
- package/dist/api-client.js +82 -3
- package/dist/handlers/blockers.js +9 -8
- package/dist/handlers/bodies-of-work.js +96 -63
- package/dist/handlers/connectors.d.ts +45 -0
- package/dist/handlers/connectors.js +183 -0
- package/dist/handlers/cost.d.ts +10 -0
- package/dist/handlers/cost.js +112 -50
- package/dist/handlers/decisions.js +32 -19
- package/dist/handlers/deployment.js +144 -122
- package/dist/handlers/discovery.d.ts +7 -0
- package/dist/handlers/discovery.js +96 -7
- package/dist/handlers/fallback.js +29 -23
- package/dist/handlers/file-checkouts.d.ts +20 -0
- package/dist/handlers/file-checkouts.js +133 -0
- package/dist/handlers/findings.d.ts +6 -0
- package/dist/handlers/findings.js +96 -40
- package/dist/handlers/git-issues.js +40 -36
- package/dist/handlers/ideas.js +49 -31
- package/dist/handlers/index.d.ts +3 -0
- package/dist/handlers/index.js +9 -0
- package/dist/handlers/milestones.js +39 -32
- package/dist/handlers/organizations.js +99 -91
- package/dist/handlers/progress.js +24 -13
- package/dist/handlers/project.js +68 -28
- package/dist/handlers/requests.js +18 -14
- package/dist/handlers/roles.d.ts +18 -0
- package/dist/handlers/roles.js +130 -0
- package/dist/handlers/session.js +58 -17
- package/dist/handlers/sprints.js +93 -81
- package/dist/handlers/tasks.d.ts +2 -0
- package/dist/handlers/tasks.js +189 -91
- package/dist/handlers/types.d.ts +64 -2
- package/dist/handlers/types.js +48 -1
- package/dist/handlers/validation.js +21 -17
- package/dist/index.js +7 -2716
- package/dist/token-tracking.d.ts +74 -0
- package/dist/token-tracking.js +122 -0
- package/dist/tools.js +685 -9
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +17 -0
- package/docs/TOOLS.md +2053 -0
- package/package.json +4 -1
- package/scripts/generate-docs.ts +212 -0
- package/src/api-client.test.ts +718 -0
- package/src/api-client.ts +320 -6
- package/src/handlers/__test-setup__.ts +16 -0
- package/src/handlers/blockers.test.ts +31 -19
- package/src/handlers/blockers.ts +9 -8
- package/src/handlers/bodies-of-work.test.ts +55 -32
- package/src/handlers/bodies-of-work.ts +115 -115
- package/src/handlers/connectors.test.ts +834 -0
- package/src/handlers/connectors.ts +229 -0
- package/src/handlers/cost.test.ts +34 -44
- package/src/handlers/cost.ts +136 -85
- package/src/handlers/decisions.test.ts +37 -27
- package/src/handlers/decisions.ts +35 -30
- package/src/handlers/deployment.ts +180 -208
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +98 -8
- package/src/handlers/fallback.test.ts +26 -22
- package/src/handlers/fallback.ts +36 -33
- package/src/handlers/file-checkouts.test.ts +670 -0
- package/src/handlers/file-checkouts.ts +165 -0
- package/src/handlers/findings.test.ts +178 -19
- package/src/handlers/findings.ts +112 -74
- package/src/handlers/git-issues.test.ts +51 -43
- package/src/handlers/git-issues.ts +44 -84
- package/src/handlers/ideas.test.ts +28 -23
- package/src/handlers/ideas.ts +61 -59
- package/src/handlers/index.ts +9 -0
- package/src/handlers/milestones.test.ts +33 -28
- package/src/handlers/milestones.ts +52 -50
- package/src/handlers/organizations.test.ts +104 -83
- package/src/handlers/organizations.ts +117 -142
- package/src/handlers/progress.test.ts +20 -14
- package/src/handlers/progress.ts +26 -24
- package/src/handlers/project.test.ts +34 -27
- package/src/handlers/project.ts +95 -63
- package/src/handlers/requests.test.ts +27 -18
- package/src/handlers/requests.ts +21 -17
- package/src/handlers/roles.test.ts +303 -0
- package/src/handlers/roles.ts +208 -0
- package/src/handlers/session.test.ts +47 -0
- package/src/handlers/session.ts +71 -26
- package/src/handlers/sprints.test.ts +71 -50
- package/src/handlers/sprints.ts +113 -146
- package/src/handlers/tasks.test.ts +77 -15
- package/src/handlers/tasks.ts +231 -156
- package/src/handlers/tool-categories.test.ts +66 -0
- package/src/handlers/types.ts +81 -2
- package/src/handlers/validation.test.ts +78 -45
- package/src/handlers/validation.ts +23 -25
- package/src/index.ts +12 -2732
- package/src/token-tracking.test.ts +453 -0
- package/src/token-tracking.ts +164 -0
- package/src/tools.ts +685 -9
- package/src/utils.test.ts +2 -2
- package/src/utils.ts +17 -0
- package/dist/config/tool-categories.d.ts +0 -31
- package/dist/config/tool-categories.js +0 -253
- package/dist/knowledge.d.ts +0 -6
- package/dist/knowledge.js +0 -218
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
checkoutFile,
|
|
4
|
+
checkinFile,
|
|
5
|
+
getFileCheckouts,
|
|
6
|
+
abandonCheckout,
|
|
7
|
+
isFileAvailable,
|
|
8
|
+
} from './file-checkouts.js';
|
|
9
|
+
import { ValidationError } from '../validators.js';
|
|
10
|
+
import { createMockContext, testUUID } from './__test-utils__.js';
|
|
11
|
+
import { mockApiClient } from './__test-setup__.js';
|
|
12
|
+
|
|
13
|
+
const VALID_UUID = testUUID();
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// checkoutFile Tests
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
describe('checkoutFile', () => {
|
|
20
|
+
beforeEach(() => vi.clearAllMocks());
|
|
21
|
+
|
|
22
|
+
it('should throw error for missing project_id', async () => {
|
|
23
|
+
const ctx = createMockContext();
|
|
24
|
+
|
|
25
|
+
await expect(
|
|
26
|
+
checkoutFile({ file_path: '/src/index.ts' }, ctx)
|
|
27
|
+
).rejects.toThrow(ValidationError);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
31
|
+
const ctx = createMockContext();
|
|
32
|
+
|
|
33
|
+
await expect(
|
|
34
|
+
checkoutFile({ project_id: 'invalid', file_path: '/src/index.ts' }, ctx)
|
|
35
|
+
).rejects.toThrow(ValidationError);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw error for missing file_path', async () => {
|
|
39
|
+
const ctx = createMockContext();
|
|
40
|
+
|
|
41
|
+
await expect(
|
|
42
|
+
checkoutFile({ project_id: VALID_UUID }, ctx)
|
|
43
|
+
).rejects.toThrow(ValidationError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should checkout file successfully', async () => {
|
|
47
|
+
mockApiClient.checkoutFile.mockResolvedValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
|
|
50
|
+
});
|
|
51
|
+
const ctx = createMockContext();
|
|
52
|
+
|
|
53
|
+
const result = await checkoutFile(
|
|
54
|
+
{
|
|
55
|
+
project_id: VALID_UUID,
|
|
56
|
+
file_path: '/src/index.ts',
|
|
57
|
+
},
|
|
58
|
+
ctx
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.result).toMatchObject({
|
|
62
|
+
success: true,
|
|
63
|
+
checkout_id: 'checkout-1',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should include reason in API call when provided', async () => {
|
|
68
|
+
mockApiClient.checkoutFile.mockResolvedValue({
|
|
69
|
+
ok: true,
|
|
70
|
+
data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
|
|
71
|
+
});
|
|
72
|
+
const ctx = createMockContext({ sessionId: 'my-session' });
|
|
73
|
+
|
|
74
|
+
await checkoutFile(
|
|
75
|
+
{
|
|
76
|
+
project_id: VALID_UUID,
|
|
77
|
+
file_path: '/src/index.ts',
|
|
78
|
+
reason: 'Editing for feature X',
|
|
79
|
+
},
|
|
80
|
+
ctx
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(mockApiClient.checkoutFile).toHaveBeenCalledWith(
|
|
84
|
+
VALID_UUID,
|
|
85
|
+
'/src/index.ts',
|
|
86
|
+
'Editing for feature X',
|
|
87
|
+
'my-session'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return error when API call fails', async () => {
|
|
92
|
+
mockApiClient.checkoutFile.mockResolvedValue({
|
|
93
|
+
ok: false,
|
|
94
|
+
error: 'File already checked out',
|
|
95
|
+
});
|
|
96
|
+
const ctx = createMockContext();
|
|
97
|
+
|
|
98
|
+
const result = await checkoutFile({
|
|
99
|
+
project_id: VALID_UUID,
|
|
100
|
+
file_path: '/src/index.ts',
|
|
101
|
+
}, ctx);
|
|
102
|
+
|
|
103
|
+
expect(result.isError).toBe(true);
|
|
104
|
+
expect(result.result).toMatchObject({ error: 'File already checked out' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should return default error when API fails without message', async () => {
|
|
108
|
+
mockApiClient.checkoutFile.mockResolvedValue({
|
|
109
|
+
ok: false,
|
|
110
|
+
});
|
|
111
|
+
const ctx = createMockContext();
|
|
112
|
+
|
|
113
|
+
const result = await checkoutFile({
|
|
114
|
+
project_id: VALID_UUID,
|
|
115
|
+
file_path: '/src/index.ts',
|
|
116
|
+
}, ctx);
|
|
117
|
+
|
|
118
|
+
expect(result.isError).toBe(true);
|
|
119
|
+
expect(result.result).toMatchObject({ error: 'Failed to checkout file' });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// checkinFile Tests
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
describe('checkinFile', () => {
|
|
128
|
+
beforeEach(() => vi.clearAllMocks());
|
|
129
|
+
|
|
130
|
+
it('should return error when neither checkout_id nor project_id+file_path provided', async () => {
|
|
131
|
+
const ctx = createMockContext();
|
|
132
|
+
|
|
133
|
+
const result = await checkinFile({}, ctx);
|
|
134
|
+
|
|
135
|
+
expect(result.isError).toBe(true);
|
|
136
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should return error when only project_id provided without file_path', async () => {
|
|
140
|
+
const ctx = createMockContext();
|
|
141
|
+
|
|
142
|
+
const result = await checkinFile({ project_id: VALID_UUID }, ctx);
|
|
143
|
+
|
|
144
|
+
expect(result.isError).toBe(true);
|
|
145
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return error when only file_path provided without project_id', async () => {
|
|
149
|
+
const ctx = createMockContext();
|
|
150
|
+
|
|
151
|
+
const result = await checkinFile({ file_path: '/src/index.ts' }, ctx);
|
|
152
|
+
|
|
153
|
+
expect(result.isError).toBe(true);
|
|
154
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should throw error for invalid checkout_id UUID', async () => {
|
|
158
|
+
const ctx = createMockContext();
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
checkinFile({ checkout_id: 'invalid' }, ctx)
|
|
162
|
+
).rejects.toThrow(ValidationError);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should checkin file successfully with checkout_id', async () => {
|
|
166
|
+
mockApiClient.checkinFile.mockResolvedValue({
|
|
167
|
+
ok: true,
|
|
168
|
+
data: { success: true },
|
|
169
|
+
});
|
|
170
|
+
const ctx = createMockContext();
|
|
171
|
+
|
|
172
|
+
const result = await checkinFile(
|
|
173
|
+
{ checkout_id: VALID_UUID },
|
|
174
|
+
ctx
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result.result).toMatchObject({ success: true });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should checkin file successfully with project_id and file_path', async () => {
|
|
181
|
+
mockApiClient.checkinFile.mockResolvedValue({
|
|
182
|
+
ok: true,
|
|
183
|
+
data: { success: true },
|
|
184
|
+
});
|
|
185
|
+
const ctx = createMockContext();
|
|
186
|
+
|
|
187
|
+
const result = await checkinFile(
|
|
188
|
+
{
|
|
189
|
+
project_id: VALID_UUID,
|
|
190
|
+
file_path: '/src/index.ts',
|
|
191
|
+
},
|
|
192
|
+
ctx
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result.result).toMatchObject({ success: true });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should include summary in API call', async () => {
|
|
199
|
+
mockApiClient.checkinFile.mockResolvedValue({
|
|
200
|
+
ok: true,
|
|
201
|
+
data: { success: true },
|
|
202
|
+
});
|
|
203
|
+
const ctx = createMockContext({ sessionId: 'my-session' });
|
|
204
|
+
|
|
205
|
+
await checkinFile(
|
|
206
|
+
{
|
|
207
|
+
checkout_id: VALID_UUID,
|
|
208
|
+
summary: 'Added validation logic',
|
|
209
|
+
},
|
|
210
|
+
ctx
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(mockApiClient.checkinFile).toHaveBeenCalledWith(
|
|
214
|
+
{
|
|
215
|
+
checkout_id: VALID_UUID,
|
|
216
|
+
project_id: undefined,
|
|
217
|
+
file_path: undefined,
|
|
218
|
+
summary: 'Added validation logic',
|
|
219
|
+
},
|
|
220
|
+
'my-session'
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return error when API call fails', async () => {
|
|
225
|
+
mockApiClient.checkinFile.mockResolvedValue({
|
|
226
|
+
ok: false,
|
|
227
|
+
error: 'Checkout not found',
|
|
228
|
+
});
|
|
229
|
+
const ctx = createMockContext();
|
|
230
|
+
|
|
231
|
+
const result = await checkinFile({ checkout_id: VALID_UUID }, ctx);
|
|
232
|
+
|
|
233
|
+
expect(result.isError).toBe(true);
|
|
234
|
+
expect(result.result).toMatchObject({ error: 'Checkout not found' });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// getFileCheckouts Tests
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
describe('getFileCheckouts', () => {
|
|
243
|
+
beforeEach(() => vi.clearAllMocks());
|
|
244
|
+
|
|
245
|
+
it('should throw error for missing project_id', async () => {
|
|
246
|
+
const ctx = createMockContext();
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
getFileCheckouts({}, ctx)
|
|
250
|
+
).rejects.toThrow(ValidationError);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
254
|
+
const ctx = createMockContext();
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
getFileCheckouts({ project_id: 'invalid' }, ctx)
|
|
258
|
+
).rejects.toThrow(ValidationError);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should throw error for invalid status', async () => {
|
|
262
|
+
const ctx = createMockContext();
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
getFileCheckouts({ project_id: VALID_UUID, status: 'invalid_status' }, ctx)
|
|
266
|
+
).rejects.toThrow(ValidationError);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should get file checkouts successfully', async () => {
|
|
270
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
271
|
+
ok: true,
|
|
272
|
+
data: {
|
|
273
|
+
checkouts: [
|
|
274
|
+
{ id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
const ctx = createMockContext();
|
|
279
|
+
|
|
280
|
+
const result = await getFileCheckouts(
|
|
281
|
+
{ project_id: VALID_UUID },
|
|
282
|
+
ctx
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(result.result).toMatchObject({
|
|
286
|
+
checkouts: [
|
|
287
|
+
{ id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should filter by status', async () => {
|
|
293
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
294
|
+
ok: true,
|
|
295
|
+
data: { checkouts: [] },
|
|
296
|
+
});
|
|
297
|
+
const ctx = createMockContext();
|
|
298
|
+
|
|
299
|
+
await getFileCheckouts(
|
|
300
|
+
{ project_id: VALID_UUID, status: 'checked_out' },
|
|
301
|
+
ctx
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
|
|
305
|
+
VALID_UUID,
|
|
306
|
+
{ status: 'checked_out', file_path: undefined, limit: 50 }
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should filter by file_path', async () => {
|
|
311
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
312
|
+
ok: true,
|
|
313
|
+
data: { checkouts: [] },
|
|
314
|
+
});
|
|
315
|
+
const ctx = createMockContext();
|
|
316
|
+
|
|
317
|
+
await getFileCheckouts(
|
|
318
|
+
{ project_id: VALID_UUID, file_path: '/src/utils.ts' },
|
|
319
|
+
ctx
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
|
|
323
|
+
VALID_UUID,
|
|
324
|
+
{ status: undefined, file_path: '/src/utils.ts', limit: 50 }
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should use custom limit', async () => {
|
|
329
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
330
|
+
ok: true,
|
|
331
|
+
data: { checkouts: [] },
|
|
332
|
+
});
|
|
333
|
+
const ctx = createMockContext();
|
|
334
|
+
|
|
335
|
+
await getFileCheckouts(
|
|
336
|
+
{ project_id: VALID_UUID, limit: 10 },
|
|
337
|
+
ctx
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
|
|
341
|
+
VALID_UUID,
|
|
342
|
+
{ status: undefined, file_path: undefined, limit: 10 }
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should accept all valid status values', async () => {
|
|
347
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
348
|
+
ok: true,
|
|
349
|
+
data: { checkouts: [] },
|
|
350
|
+
});
|
|
351
|
+
const ctx = createMockContext();
|
|
352
|
+
|
|
353
|
+
for (const status of ['checked_out', 'checked_in', 'abandoned']) {
|
|
354
|
+
await getFileCheckouts(
|
|
355
|
+
{ project_id: VALID_UUID, status },
|
|
356
|
+
ctx
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledTimes(3);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return error when API call fails', async () => {
|
|
364
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
365
|
+
ok: false,
|
|
366
|
+
error: 'Database error',
|
|
367
|
+
});
|
|
368
|
+
const ctx = createMockContext();
|
|
369
|
+
|
|
370
|
+
const result = await getFileCheckouts({ project_id: VALID_UUID }, ctx);
|
|
371
|
+
|
|
372
|
+
expect(result.isError).toBe(true);
|
|
373
|
+
expect(result.result).toMatchObject({ error: 'Database error' });
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// abandonCheckout Tests
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
describe('abandonCheckout', () => {
|
|
382
|
+
beforeEach(() => vi.clearAllMocks());
|
|
383
|
+
|
|
384
|
+
it('should return error when neither checkout_id nor project_id+file_path provided', async () => {
|
|
385
|
+
const ctx = createMockContext();
|
|
386
|
+
|
|
387
|
+
const result = await abandonCheckout({}, ctx);
|
|
388
|
+
|
|
389
|
+
expect(result.isError).toBe(true);
|
|
390
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should return error when only project_id provided without file_path', async () => {
|
|
394
|
+
const ctx = createMockContext();
|
|
395
|
+
|
|
396
|
+
const result = await abandonCheckout({ project_id: VALID_UUID }, ctx);
|
|
397
|
+
|
|
398
|
+
expect(result.isError).toBe(true);
|
|
399
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should throw error for invalid checkout_id UUID', async () => {
|
|
403
|
+
const ctx = createMockContext();
|
|
404
|
+
|
|
405
|
+
await expect(
|
|
406
|
+
abandonCheckout({ checkout_id: 'invalid' }, ctx)
|
|
407
|
+
).rejects.toThrow(ValidationError);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should abandon checkout successfully with checkout_id', async () => {
|
|
411
|
+
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
412
|
+
ok: true,
|
|
413
|
+
data: { success: true },
|
|
414
|
+
});
|
|
415
|
+
const ctx = createMockContext();
|
|
416
|
+
|
|
417
|
+
const result = await abandonCheckout(
|
|
418
|
+
{ checkout_id: VALID_UUID },
|
|
419
|
+
ctx
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(result.result).toMatchObject({ success: true });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should abandon checkout successfully with project_id and file_path', async () => {
|
|
426
|
+
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
427
|
+
ok: true,
|
|
428
|
+
data: { success: true },
|
|
429
|
+
});
|
|
430
|
+
const ctx = createMockContext();
|
|
431
|
+
|
|
432
|
+
const result = await abandonCheckout(
|
|
433
|
+
{
|
|
434
|
+
project_id: VALID_UUID,
|
|
435
|
+
file_path: '/src/index.ts',
|
|
436
|
+
},
|
|
437
|
+
ctx
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
expect(result.result).toMatchObject({ success: true });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should pass params correctly to API', async () => {
|
|
444
|
+
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
445
|
+
ok: true,
|
|
446
|
+
data: { success: true },
|
|
447
|
+
});
|
|
448
|
+
const ctx = createMockContext();
|
|
449
|
+
|
|
450
|
+
await abandonCheckout(
|
|
451
|
+
{
|
|
452
|
+
project_id: VALID_UUID,
|
|
453
|
+
file_path: '/src/index.ts',
|
|
454
|
+
},
|
|
455
|
+
ctx
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(mockApiClient.abandonCheckout).toHaveBeenCalledWith({
|
|
459
|
+
checkout_id: undefined,
|
|
460
|
+
project_id: VALID_UUID,
|
|
461
|
+
file_path: '/src/index.ts',
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should return error when API call fails', async () => {
|
|
466
|
+
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
467
|
+
ok: false,
|
|
468
|
+
error: 'Checkout not found',
|
|
469
|
+
});
|
|
470
|
+
const ctx = createMockContext();
|
|
471
|
+
|
|
472
|
+
const result = await abandonCheckout({ checkout_id: VALID_UUID }, ctx);
|
|
473
|
+
|
|
474
|
+
expect(result.isError).toBe(true);
|
|
475
|
+
expect(result.result).toMatchObject({ error: 'Checkout not found' });
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should return default error when API fails without message', async () => {
|
|
479
|
+
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
480
|
+
ok: false,
|
|
481
|
+
});
|
|
482
|
+
const ctx = createMockContext();
|
|
483
|
+
|
|
484
|
+
const result = await abandonCheckout({ checkout_id: VALID_UUID }, ctx);
|
|
485
|
+
|
|
486
|
+
expect(result.isError).toBe(true);
|
|
487
|
+
expect(result.result).toMatchObject({ error: 'Failed to abandon checkout' });
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// isFileAvailable Tests
|
|
493
|
+
// ============================================================================
|
|
494
|
+
|
|
495
|
+
describe('isFileAvailable', () => {
|
|
496
|
+
beforeEach(() => vi.clearAllMocks());
|
|
497
|
+
|
|
498
|
+
it('should throw error for missing project_id', async () => {
|
|
499
|
+
const ctx = createMockContext();
|
|
500
|
+
|
|
501
|
+
await expect(
|
|
502
|
+
isFileAvailable({ file_path: '/src/index.ts' }, ctx)
|
|
503
|
+
).rejects.toThrow(ValidationError);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
507
|
+
const ctx = createMockContext();
|
|
508
|
+
|
|
509
|
+
await expect(
|
|
510
|
+
isFileAvailable({ project_id: 'invalid', file_path: '/src/index.ts' }, ctx)
|
|
511
|
+
).rejects.toThrow(ValidationError);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should throw error for missing file_path', async () => {
|
|
515
|
+
const ctx = createMockContext();
|
|
516
|
+
|
|
517
|
+
await expect(
|
|
518
|
+
isFileAvailable({ project_id: VALID_UUID }, ctx)
|
|
519
|
+
).rejects.toThrow(ValidationError);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should return available=true when file has no active checkout', async () => {
|
|
523
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
524
|
+
ok: true,
|
|
525
|
+
data: { checkouts: [] },
|
|
526
|
+
});
|
|
527
|
+
const ctx = createMockContext();
|
|
528
|
+
|
|
529
|
+
const result = await isFileAvailable(
|
|
530
|
+
{
|
|
531
|
+
project_id: VALID_UUID,
|
|
532
|
+
file_path: '/src/index.ts',
|
|
533
|
+
},
|
|
534
|
+
ctx
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(result.result).toMatchObject({
|
|
538
|
+
available: true,
|
|
539
|
+
file_path: '/src/index.ts',
|
|
540
|
+
checked_out_by: null,
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should return available=false with checkout info when file is checked out', async () => {
|
|
545
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
546
|
+
ok: true,
|
|
547
|
+
data: {
|
|
548
|
+
checkouts: [{
|
|
549
|
+
id: 'checkout-123',
|
|
550
|
+
file_path: '/src/index.ts',
|
|
551
|
+
status: 'checked_out',
|
|
552
|
+
checked_out_by: 'Apex',
|
|
553
|
+
checked_out_at: '2026-01-16T10:00:00Z',
|
|
554
|
+
checkout_reason: 'Working on feature X',
|
|
555
|
+
}],
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
const ctx = createMockContext();
|
|
559
|
+
|
|
560
|
+
const result = await isFileAvailable(
|
|
561
|
+
{
|
|
562
|
+
project_id: VALID_UUID,
|
|
563
|
+
file_path: '/src/index.ts',
|
|
564
|
+
},
|
|
565
|
+
ctx
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result.result).toMatchObject({
|
|
569
|
+
available: false,
|
|
570
|
+
file_path: '/src/index.ts',
|
|
571
|
+
checked_out_by: {
|
|
572
|
+
checkout_id: 'checkout-123',
|
|
573
|
+
checked_out_by: 'Apex',
|
|
574
|
+
checked_out_at: '2026-01-16T10:00:00Z',
|
|
575
|
+
reason: 'Working on feature X',
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should query API with correct parameters', async () => {
|
|
581
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
582
|
+
ok: true,
|
|
583
|
+
data: { checkouts: [] },
|
|
584
|
+
});
|
|
585
|
+
const ctx = createMockContext();
|
|
586
|
+
|
|
587
|
+
await isFileAvailable(
|
|
588
|
+
{
|
|
589
|
+
project_id: VALID_UUID,
|
|
590
|
+
file_path: '/src/index.ts',
|
|
591
|
+
},
|
|
592
|
+
ctx
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(VALID_UUID, {
|
|
596
|
+
status: 'checked_out',
|
|
597
|
+
file_path: '/src/index.ts',
|
|
598
|
+
limit: 1,
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should return error when API call fails', async () => {
|
|
603
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
604
|
+
ok: false,
|
|
605
|
+
error: 'Project not found',
|
|
606
|
+
});
|
|
607
|
+
const ctx = createMockContext();
|
|
608
|
+
|
|
609
|
+
const result = await isFileAvailable({
|
|
610
|
+
project_id: VALID_UUID,
|
|
611
|
+
file_path: '/src/index.ts',
|
|
612
|
+
}, ctx);
|
|
613
|
+
|
|
614
|
+
expect(result.isError).toBe(true);
|
|
615
|
+
expect(result.result).toMatchObject({ error: 'Project not found' });
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should return default error when API fails without message', async () => {
|
|
619
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
620
|
+
ok: false,
|
|
621
|
+
});
|
|
622
|
+
const ctx = createMockContext();
|
|
623
|
+
|
|
624
|
+
const result = await isFileAvailable({
|
|
625
|
+
project_id: VALID_UUID,
|
|
626
|
+
file_path: '/src/index.ts',
|
|
627
|
+
}, ctx);
|
|
628
|
+
|
|
629
|
+
expect(result.isError).toBe(true);
|
|
630
|
+
expect(result.result).toMatchObject({ error: 'Failed to check file availability' });
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should handle empty checkouts array gracefully', async () => {
|
|
634
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
635
|
+
ok: true,
|
|
636
|
+
data: { checkouts: [] },
|
|
637
|
+
});
|
|
638
|
+
const ctx = createMockContext();
|
|
639
|
+
|
|
640
|
+
const result = await isFileAvailable(
|
|
641
|
+
{
|
|
642
|
+
project_id: VALID_UUID,
|
|
643
|
+
file_path: '/src/index.ts',
|
|
644
|
+
},
|
|
645
|
+
ctx
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
expect(result.result.available).toBe(true);
|
|
649
|
+
expect(result.result.checked_out_by).toBeNull();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should handle undefined checkouts gracefully', async () => {
|
|
653
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
654
|
+
ok: true,
|
|
655
|
+
data: {},
|
|
656
|
+
});
|
|
657
|
+
const ctx = createMockContext();
|
|
658
|
+
|
|
659
|
+
const result = await isFileAvailable(
|
|
660
|
+
{
|
|
661
|
+
project_id: VALID_UUID,
|
|
662
|
+
file_path: '/src/index.ts',
|
|
663
|
+
},
|
|
664
|
+
ctx
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
expect(result.result.available).toBe(true);
|
|
668
|
+
expect(result.result.checked_out_by).toBeNull();
|
|
669
|
+
});
|
|
670
|
+
});
|