@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.
Files changed (104) hide show
  1. package/README.md +60 -7
  2. package/dist/api-client.d.ts +251 -1
  3. package/dist/api-client.js +82 -3
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +96 -63
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +112 -50
  10. package/dist/handlers/decisions.js +32 -19
  11. package/dist/handlers/deployment.js +144 -122
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +96 -7
  14. package/dist/handlers/fallback.js +29 -23
  15. package/dist/handlers/file-checkouts.d.ts +20 -0
  16. package/dist/handlers/file-checkouts.js +133 -0
  17. package/dist/handlers/findings.d.ts +6 -0
  18. package/dist/handlers/findings.js +96 -40
  19. package/dist/handlers/git-issues.js +40 -36
  20. package/dist/handlers/ideas.js +49 -31
  21. package/dist/handlers/index.d.ts +3 -0
  22. package/dist/handlers/index.js +9 -0
  23. package/dist/handlers/milestones.js +39 -32
  24. package/dist/handlers/organizations.js +99 -91
  25. package/dist/handlers/progress.js +24 -13
  26. package/dist/handlers/project.js +68 -28
  27. package/dist/handlers/requests.js +18 -14
  28. package/dist/handlers/roles.d.ts +18 -0
  29. package/dist/handlers/roles.js +130 -0
  30. package/dist/handlers/session.js +58 -17
  31. package/dist/handlers/sprints.js +93 -81
  32. package/dist/handlers/tasks.d.ts +2 -0
  33. package/dist/handlers/tasks.js +189 -91
  34. package/dist/handlers/types.d.ts +64 -2
  35. package/dist/handlers/types.js +48 -1
  36. package/dist/handlers/validation.js +21 -17
  37. package/dist/index.js +7 -2716
  38. package/dist/token-tracking.d.ts +74 -0
  39. package/dist/token-tracking.js +122 -0
  40. package/dist/tools.js +685 -9
  41. package/dist/utils.d.ts +5 -0
  42. package/dist/utils.js +17 -0
  43. package/docs/TOOLS.md +2053 -0
  44. package/package.json +4 -1
  45. package/scripts/generate-docs.ts +212 -0
  46. package/src/api-client.test.ts +718 -0
  47. package/src/api-client.ts +320 -6
  48. package/src/handlers/__test-setup__.ts +16 -0
  49. package/src/handlers/blockers.test.ts +31 -19
  50. package/src/handlers/blockers.ts +9 -8
  51. package/src/handlers/bodies-of-work.test.ts +55 -32
  52. package/src/handlers/bodies-of-work.ts +115 -115
  53. package/src/handlers/connectors.test.ts +834 -0
  54. package/src/handlers/connectors.ts +229 -0
  55. package/src/handlers/cost.test.ts +34 -44
  56. package/src/handlers/cost.ts +136 -85
  57. package/src/handlers/decisions.test.ts +37 -27
  58. package/src/handlers/decisions.ts +35 -30
  59. package/src/handlers/deployment.ts +180 -208
  60. package/src/handlers/discovery.test.ts +4 -5
  61. package/src/handlers/discovery.ts +98 -8
  62. package/src/handlers/fallback.test.ts +26 -22
  63. package/src/handlers/fallback.ts +36 -33
  64. package/src/handlers/file-checkouts.test.ts +670 -0
  65. package/src/handlers/file-checkouts.ts +165 -0
  66. package/src/handlers/findings.test.ts +178 -19
  67. package/src/handlers/findings.ts +112 -74
  68. package/src/handlers/git-issues.test.ts +51 -43
  69. package/src/handlers/git-issues.ts +44 -84
  70. package/src/handlers/ideas.test.ts +28 -23
  71. package/src/handlers/ideas.ts +61 -59
  72. package/src/handlers/index.ts +9 -0
  73. package/src/handlers/milestones.test.ts +33 -28
  74. package/src/handlers/milestones.ts +52 -50
  75. package/src/handlers/organizations.test.ts +104 -83
  76. package/src/handlers/organizations.ts +117 -142
  77. package/src/handlers/progress.test.ts +20 -14
  78. package/src/handlers/progress.ts +26 -24
  79. package/src/handlers/project.test.ts +34 -27
  80. package/src/handlers/project.ts +95 -63
  81. package/src/handlers/requests.test.ts +27 -18
  82. package/src/handlers/requests.ts +21 -17
  83. package/src/handlers/roles.test.ts +303 -0
  84. package/src/handlers/roles.ts +208 -0
  85. package/src/handlers/session.test.ts +47 -0
  86. package/src/handlers/session.ts +71 -26
  87. package/src/handlers/sprints.test.ts +71 -50
  88. package/src/handlers/sprints.ts +113 -146
  89. package/src/handlers/tasks.test.ts +77 -15
  90. package/src/handlers/tasks.ts +231 -156
  91. package/src/handlers/tool-categories.test.ts +66 -0
  92. package/src/handlers/types.ts +81 -2
  93. package/src/handlers/validation.test.ts +78 -45
  94. package/src/handlers/validation.ts +23 -25
  95. package/src/index.ts +12 -2732
  96. package/src/token-tracking.test.ts +453 -0
  97. package/src/token-tracking.ts +164 -0
  98. package/src/tools.ts +685 -9
  99. package/src/utils.test.ts +2 -2
  100. package/src/utils.ts +17 -0
  101. package/dist/config/tool-categories.d.ts +0 -31
  102. package/dist/config/tool-categories.js +0 -253
  103. package/dist/knowledge.d.ts +0 -6
  104. 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
+ });