@vibescope/mcp-server 0.2.5 → 0.2.6

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