@vibescope/mcp-server 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/api-client.d.ts +120 -2
  3. package/dist/api-client.js +51 -5
  4. package/dist/handlers/bodies-of-work.js +84 -50
  5. package/dist/handlers/cost.js +62 -54
  6. package/dist/handlers/decisions.js +29 -16
  7. package/dist/handlers/deployment.js +114 -107
  8. package/dist/handlers/discovery.d.ts +3 -0
  9. package/dist/handlers/discovery.js +55 -657
  10. package/dist/handlers/fallback.js +42 -28
  11. package/dist/handlers/file-checkouts.d.ts +18 -0
  12. package/dist/handlers/file-checkouts.js +101 -0
  13. package/dist/handlers/findings.d.ts +14 -1
  14. package/dist/handlers/findings.js +104 -28
  15. package/dist/handlers/git-issues.js +36 -32
  16. package/dist/handlers/ideas.js +44 -26
  17. package/dist/handlers/index.d.ts +2 -0
  18. package/dist/handlers/index.js +6 -0
  19. package/dist/handlers/milestones.js +34 -27
  20. package/dist/handlers/organizations.js +86 -78
  21. package/dist/handlers/progress.js +22 -11
  22. package/dist/handlers/project.js +62 -22
  23. package/dist/handlers/requests.js +15 -11
  24. package/dist/handlers/roles.d.ts +18 -0
  25. package/dist/handlers/roles.js +130 -0
  26. package/dist/handlers/session.js +52 -15
  27. package/dist/handlers/sprints.js +78 -65
  28. package/dist/handlers/tasks.js +135 -74
  29. package/dist/handlers/tool-docs.d.ts +4 -3
  30. package/dist/handlers/tool-docs.js +252 -5
  31. package/dist/handlers/validation.js +30 -14
  32. package/dist/index.js +25 -7
  33. package/dist/tools.js +417 -4
  34. package/package.json +1 -1
  35. package/src/api-client.ts +161 -8
  36. package/src/handlers/__test-setup__.ts +12 -0
  37. package/src/handlers/bodies-of-work.ts +127 -111
  38. package/src/handlers/cost.test.ts +34 -44
  39. package/src/handlers/cost.ts +77 -92
  40. package/src/handlers/decisions.test.ts +3 -2
  41. package/src/handlers/decisions.ts +32 -27
  42. package/src/handlers/deployment.ts +144 -190
  43. package/src/handlers/discovery.test.ts +4 -5
  44. package/src/handlers/discovery.ts +60 -746
  45. package/src/handlers/fallback.test.ts +78 -0
  46. package/src/handlers/fallback.ts +51 -38
  47. package/src/handlers/file-checkouts.test.ts +477 -0
  48. package/src/handlers/file-checkouts.ts +127 -0
  49. package/src/handlers/findings.test.ts +274 -2
  50. package/src/handlers/findings.ts +123 -57
  51. package/src/handlers/git-issues.ts +40 -80
  52. package/src/handlers/ideas.ts +56 -54
  53. package/src/handlers/index.ts +6 -0
  54. package/src/handlers/milestones.test.ts +1 -1
  55. package/src/handlers/milestones.ts +47 -45
  56. package/src/handlers/organizations.ts +104 -129
  57. package/src/handlers/progress.ts +24 -22
  58. package/src/handlers/project.ts +89 -57
  59. package/src/handlers/requests.ts +18 -14
  60. package/src/handlers/roles.test.ts +303 -0
  61. package/src/handlers/roles.ts +208 -0
  62. package/src/handlers/session.test.ts +37 -2
  63. package/src/handlers/session.ts +64 -21
  64. package/src/handlers/sprints.ts +114 -134
  65. package/src/handlers/tasks.test.ts +61 -0
  66. package/src/handlers/tasks.ts +170 -139
  67. package/src/handlers/tool-docs.ts +1024 -0
  68. package/src/handlers/validation.test.ts +53 -1
  69. package/src/handlers/validation.ts +32 -21
  70. package/src/index.ts +25 -7
  71. package/src/tools.ts +417 -4
  72. package/dist/config/tool-categories.d.ts +0 -31
  73. package/dist/config/tool-categories.js +0 -253
  74. package/dist/knowledge.d.ts +0 -6
  75. package/dist/knowledge.js +0 -218
  76. package/src/knowledge.ts +0 -230
@@ -145,6 +145,84 @@ describe('startFallbackActivity', () => {
145
145
  }, ctx)
146
146
  ).rejects.toThrow('Failed to start fallback activity');
147
147
  });
148
+
149
+ it('should pass through worktree guidance when API returns it', async () => {
150
+ mockApiClient.startFallbackActivity.mockResolvedValue({
151
+ ok: true,
152
+ data: {
153
+ success: true,
154
+ activity: 'code_review',
155
+ message: 'Started code_review',
156
+ git_workflow: {
157
+ workflow: 'git-flow',
158
+ base_branch: 'develop',
159
+ worktree_recommended: true,
160
+ note: 'Fallback activities use the base branch directly (read-only).',
161
+ },
162
+ worktree_setup: {
163
+ message: 'RECOMMENDED: Create a worktree to avoid conflicts.',
164
+ commands: [
165
+ 'git checkout develop',
166
+ 'git pull origin develop',
167
+ 'git worktree add ../Project-code-review develop',
168
+ 'cd ../Project-code-review',
169
+ ],
170
+ worktree_path: '../Project-code-review',
171
+ branch_name: 'develop',
172
+ cleanup_command: 'git worktree remove ../Project-code-review',
173
+ report_worktree: 'heartbeat(current_worktree_path: "../Project-code-review")',
174
+ },
175
+ next_step: 'After setting up worktree: call heartbeat to report your location.',
176
+ },
177
+ });
178
+ const ctx = createMockContext();
179
+
180
+ const result = await startFallbackActivity(
181
+ {
182
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
183
+ activity: 'code_review',
184
+ },
185
+ ctx
186
+ );
187
+
188
+ expect(result.result).toMatchObject({
189
+ success: true,
190
+ activity: 'code_review',
191
+ });
192
+ expect((result.result as { git_workflow?: unknown }).git_workflow).toBeDefined();
193
+ expect((result.result as { git_workflow: { workflow: string } }).git_workflow.workflow).toBe('git-flow');
194
+ expect((result.result as { worktree_setup?: unknown }).worktree_setup).toBeDefined();
195
+ expect((result.result as { worktree_setup: { worktree_path: string } }).worktree_setup.worktree_path).toBe('../Project-code-review');
196
+ expect((result.result as { next_step?: string }).next_step).toContain('heartbeat');
197
+ });
198
+
199
+ it('should not include worktree guidance when API does not return it', async () => {
200
+ mockApiClient.startFallbackActivity.mockResolvedValue({
201
+ ok: true,
202
+ data: {
203
+ success: true,
204
+ activity: 'code_review',
205
+ message: 'Started code_review',
206
+ },
207
+ });
208
+ const ctx = createMockContext();
209
+
210
+ const result = await startFallbackActivity(
211
+ {
212
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
213
+ activity: 'code_review',
214
+ },
215
+ ctx
216
+ );
217
+
218
+ expect(result.result).toMatchObject({
219
+ success: true,
220
+ activity: 'code_review',
221
+ });
222
+ expect((result.result as { git_workflow?: unknown }).git_workflow).toBeUndefined();
223
+ expect((result.result as { worktree_setup?: unknown }).worktree_setup).toBeUndefined();
224
+ expect((result.result as { next_step?: string }).next_step).toBeUndefined();
225
+ });
148
226
  });
149
227
 
150
228
  // ============================================================================
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { Handler, HandlerRegistry } from './types.js';
14
- import { validateRequired, validateUUID } from '../validators.js';
14
+ import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
15
15
  import { FALLBACK_ACTIVITIES } from '../utils.js';
16
16
  import { getApiClient } from '../api-client.js';
17
17
 
@@ -26,23 +26,38 @@ const VALID_ACTIVITIES = [
26
26
  'documentation_review',
27
27
  'dependency_audit',
28
28
  'validate_completed_tasks',
29
- ];
29
+ ] as const;
30
30
 
31
- export const startFallbackActivity: Handler = async (args, ctx) => {
32
- const { project_id, activity } = args as { project_id: string; activity: string };
31
+ type FallbackActivity = typeof VALID_ACTIVITIES[number];
33
32
 
34
- validateRequired(project_id, 'project_id');
35
- validateUUID(project_id, 'project_id');
36
- validateRequired(activity, 'activity');
33
+ // Argument schemas for type-safe parsing
34
+ const startFallbackActivitySchema = {
35
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
36
+ activity: { type: 'string' as const, required: true as const, validate: createEnumValidator(VALID_ACTIVITIES) },
37
+ };
37
38
 
38
- if (!VALID_ACTIVITIES.includes(activity)) {
39
- throw new Error(`Invalid activity. Must be one of: ${VALID_ACTIVITIES.join(', ')}`);
40
- }
39
+ const stopFallbackActivitySchema = {
40
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
41
+ summary: { type: 'string' as const },
42
+ };
43
+
44
+ const getActivityHistorySchema = {
45
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
46
+ activity_type: { type: 'string' as const },
47
+ limit: { type: 'number' as const, default: 50 },
48
+ };
49
+
50
+ const getActivitySchedulesSchema = {
51
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
52
+ };
53
+
54
+ export const startFallbackActivity: Handler = async (args, ctx) => {
55
+ const { project_id, activity } = parseArgs(args, startFallbackActivitySchema);
41
56
 
42
57
  const { session } = ctx;
43
58
  const apiClient = getApiClient();
44
59
 
45
- const response = await apiClient.startFallbackActivity(project_id, activity, session.currentSessionId || undefined);
60
+ const response = await apiClient.startFallbackActivity(project_id, activity as FallbackActivity, session.currentSessionId || undefined);
46
61
 
47
62
  if (!response.ok) {
48
63
  throw new Error(`Failed to start fallback activity: ${response.error}`);
@@ -51,23 +66,31 @@ export const startFallbackActivity: Handler = async (args, ctx) => {
51
66
  // Get the activity details for the response
52
67
  const activityInfo = FALLBACK_ACTIVITIES.find((a) => a.activity === activity);
53
68
 
54
- return {
55
- result: {
56
- success: true,
57
- activity,
58
- title: activityInfo?.title || activity,
59
- description: activityInfo?.description || '',
60
- prompt: activityInfo?.prompt || '',
61
- message: response.data?.message || `Started fallback activity: ${activityInfo?.title || activity}`,
62
- },
69
+ const result: Record<string, unknown> = {
70
+ success: true,
71
+ activity,
72
+ title: activityInfo?.title || activity,
73
+ description: activityInfo?.description || '',
74
+ prompt: activityInfo?.prompt || '',
75
+ message: response.data?.message || `Started fallback activity: ${activityInfo?.title || activity}`,
63
76
  };
77
+
78
+ // Pass through worktree guidance if provided
79
+ if (response.data?.git_workflow) {
80
+ result.git_workflow = response.data.git_workflow;
81
+ }
82
+ if (response.data?.worktree_setup) {
83
+ result.worktree_setup = response.data.worktree_setup;
84
+ }
85
+ if (response.data?.next_step) {
86
+ result.next_step = response.data.next_step;
87
+ }
88
+
89
+ return { result };
64
90
  };
65
91
 
66
92
  export const stopFallbackActivity: Handler = async (args, ctx) => {
67
- const { project_id, summary } = args as { project_id: string; summary?: string };
68
-
69
- validateRequired(project_id, 'project_id');
70
- validateUUID(project_id, 'project_id');
93
+ const { project_id, summary } = parseArgs(args, stopFallbackActivitySchema);
71
94
 
72
95
  const { session } = ctx;
73
96
  const apiClient = getApiClient();
@@ -86,15 +109,8 @@ export const stopFallbackActivity: Handler = async (args, ctx) => {
86
109
  };
87
110
  };
88
111
 
89
- export const getActivityHistory: Handler = async (args, ctx) => {
90
- const { project_id, activity_type, limit = 50 } = args as {
91
- project_id: string;
92
- activity_type?: string;
93
- limit?: number;
94
- };
95
-
96
- validateRequired(project_id, 'project_id');
97
- validateUUID(project_id, 'project_id');
112
+ export const getActivityHistory: Handler = async (args, _ctx) => {
113
+ const { project_id, activity_type, limit } = parseArgs(args, getActivityHistorySchema);
98
114
 
99
115
  const apiClient = getApiClient();
100
116
 
@@ -127,11 +143,8 @@ export const getActivityHistory: Handler = async (args, ctx) => {
127
143
  };
128
144
  };
129
145
 
130
- export const getActivitySchedules: Handler = async (args, ctx) => {
131
- const { project_id } = args as { project_id: string };
132
-
133
- validateRequired(project_id, 'project_id');
134
- validateUUID(project_id, 'project_id');
146
+ export const getActivitySchedules: Handler = async (args, _ctx) => {
147
+ const { project_id } = parseArgs(args, getActivitySchedulesSchema);
135
148
 
136
149
  const apiClient = getApiClient();
137
150
 
@@ -0,0 +1,477 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ checkoutFile,
4
+ checkinFile,
5
+ getFileCheckouts,
6
+ abandonCheckout,
7
+ } from './file-checkouts.js';
8
+ import { ValidationError } from '../validators.js';
9
+ import { createMockContext, testUUID } from './__test-utils__.js';
10
+ import { mockApiClient } from './__test-setup__.js';
11
+
12
+ const VALID_UUID = testUUID();
13
+
14
+ // ============================================================================
15
+ // checkoutFile Tests
16
+ // ============================================================================
17
+
18
+ describe('checkoutFile', () => {
19
+ beforeEach(() => vi.clearAllMocks());
20
+
21
+ it('should throw error for missing project_id', async () => {
22
+ const ctx = createMockContext();
23
+
24
+ await expect(
25
+ checkoutFile({ file_path: '/src/index.ts' }, ctx)
26
+ ).rejects.toThrow(ValidationError);
27
+ });
28
+
29
+ it('should throw error for invalid project_id UUID', async () => {
30
+ const ctx = createMockContext();
31
+
32
+ await expect(
33
+ checkoutFile({ project_id: 'invalid', file_path: '/src/index.ts' }, ctx)
34
+ ).rejects.toThrow(ValidationError);
35
+ });
36
+
37
+ it('should throw error for missing file_path', async () => {
38
+ const ctx = createMockContext();
39
+
40
+ await expect(
41
+ checkoutFile({ project_id: VALID_UUID }, ctx)
42
+ ).rejects.toThrow(ValidationError);
43
+ });
44
+
45
+ it('should checkout file successfully', async () => {
46
+ mockApiClient.checkoutFile.mockResolvedValue({
47
+ ok: true,
48
+ data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
49
+ });
50
+ const ctx = createMockContext();
51
+
52
+ const result = await checkoutFile(
53
+ {
54
+ project_id: VALID_UUID,
55
+ file_path: '/src/index.ts',
56
+ },
57
+ ctx
58
+ );
59
+
60
+ expect(result.result).toMatchObject({
61
+ success: true,
62
+ checkout_id: 'checkout-1',
63
+ });
64
+ });
65
+
66
+ it('should include reason in API call when provided', async () => {
67
+ mockApiClient.checkoutFile.mockResolvedValue({
68
+ ok: true,
69
+ data: { success: true, checkout_id: 'checkout-1', file_path: '/src/index.ts' },
70
+ });
71
+ const ctx = createMockContext({ sessionId: 'my-session' });
72
+
73
+ await checkoutFile(
74
+ {
75
+ project_id: VALID_UUID,
76
+ file_path: '/src/index.ts',
77
+ reason: 'Editing for feature X',
78
+ },
79
+ ctx
80
+ );
81
+
82
+ expect(mockApiClient.checkoutFile).toHaveBeenCalledWith(
83
+ VALID_UUID,
84
+ '/src/index.ts',
85
+ 'Editing for feature X',
86
+ 'my-session'
87
+ );
88
+ });
89
+
90
+ it('should throw error when API call fails', async () => {
91
+ mockApiClient.checkoutFile.mockResolvedValue({
92
+ ok: false,
93
+ error: 'File already checked out',
94
+ });
95
+ const ctx = createMockContext();
96
+
97
+ await expect(
98
+ checkoutFile({
99
+ project_id: VALID_UUID,
100
+ file_path: '/src/index.ts',
101
+ }, ctx)
102
+ ).rejects.toThrow('File already checked out');
103
+ });
104
+
105
+ it('should throw default error when API fails without message', async () => {
106
+ mockApiClient.checkoutFile.mockResolvedValue({
107
+ ok: false,
108
+ });
109
+ const ctx = createMockContext();
110
+
111
+ await expect(
112
+ checkoutFile({
113
+ project_id: VALID_UUID,
114
+ file_path: '/src/index.ts',
115
+ }, ctx)
116
+ ).rejects.toThrow('Failed to checkout file');
117
+ });
118
+ });
119
+
120
+ // ============================================================================
121
+ // checkinFile Tests
122
+ // ============================================================================
123
+
124
+ describe('checkinFile', () => {
125
+ beforeEach(() => vi.clearAllMocks());
126
+
127
+ it('should throw error when neither checkout_id nor project_id+file_path provided', async () => {
128
+ const ctx = createMockContext();
129
+
130
+ await expect(
131
+ checkinFile({}, ctx)
132
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
133
+ });
134
+
135
+ it('should throw error when only project_id provided without file_path', async () => {
136
+ const ctx = createMockContext();
137
+
138
+ await expect(
139
+ checkinFile({ project_id: VALID_UUID }, ctx)
140
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
141
+ });
142
+
143
+ it('should throw error when only file_path provided without project_id', async () => {
144
+ const ctx = createMockContext();
145
+
146
+ await expect(
147
+ checkinFile({ file_path: '/src/index.ts' }, ctx)
148
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
149
+ });
150
+
151
+ it('should throw error for invalid checkout_id UUID', async () => {
152
+ const ctx = createMockContext();
153
+
154
+ await expect(
155
+ checkinFile({ checkout_id: 'invalid' }, ctx)
156
+ ).rejects.toThrow(ValidationError);
157
+ });
158
+
159
+ it('should checkin file successfully with checkout_id', async () => {
160
+ mockApiClient.checkinFile.mockResolvedValue({
161
+ ok: true,
162
+ data: { success: true },
163
+ });
164
+ const ctx = createMockContext();
165
+
166
+ const result = await checkinFile(
167
+ { checkout_id: VALID_UUID },
168
+ ctx
169
+ );
170
+
171
+ expect(result.result).toMatchObject({ success: true });
172
+ });
173
+
174
+ it('should checkin file successfully with project_id and file_path', async () => {
175
+ mockApiClient.checkinFile.mockResolvedValue({
176
+ ok: true,
177
+ data: { success: true },
178
+ });
179
+ const ctx = createMockContext();
180
+
181
+ const result = await checkinFile(
182
+ {
183
+ project_id: VALID_UUID,
184
+ file_path: '/src/index.ts',
185
+ },
186
+ ctx
187
+ );
188
+
189
+ expect(result.result).toMatchObject({ success: true });
190
+ });
191
+
192
+ it('should include summary in API call', async () => {
193
+ mockApiClient.checkinFile.mockResolvedValue({
194
+ ok: true,
195
+ data: { success: true },
196
+ });
197
+ const ctx = createMockContext({ sessionId: 'my-session' });
198
+
199
+ await checkinFile(
200
+ {
201
+ checkout_id: VALID_UUID,
202
+ summary: 'Added validation logic',
203
+ },
204
+ ctx
205
+ );
206
+
207
+ expect(mockApiClient.checkinFile).toHaveBeenCalledWith(
208
+ {
209
+ checkout_id: VALID_UUID,
210
+ project_id: undefined,
211
+ file_path: undefined,
212
+ summary: 'Added validation logic',
213
+ },
214
+ 'my-session'
215
+ );
216
+ });
217
+
218
+ it('should throw error when API call fails', async () => {
219
+ mockApiClient.checkinFile.mockResolvedValue({
220
+ ok: false,
221
+ error: 'Checkout not found',
222
+ });
223
+ const ctx = createMockContext();
224
+
225
+ await expect(
226
+ checkinFile({ checkout_id: VALID_UUID }, ctx)
227
+ ).rejects.toThrow('Checkout not found');
228
+ });
229
+ });
230
+
231
+ // ============================================================================
232
+ // getFileCheckouts Tests
233
+ // ============================================================================
234
+
235
+ describe('getFileCheckouts', () => {
236
+ beforeEach(() => vi.clearAllMocks());
237
+
238
+ it('should throw error for missing project_id', async () => {
239
+ const ctx = createMockContext();
240
+
241
+ await expect(
242
+ getFileCheckouts({}, ctx)
243
+ ).rejects.toThrow(ValidationError);
244
+ });
245
+
246
+ it('should throw error for invalid project_id UUID', async () => {
247
+ const ctx = createMockContext();
248
+
249
+ await expect(
250
+ getFileCheckouts({ project_id: 'invalid' }, ctx)
251
+ ).rejects.toThrow(ValidationError);
252
+ });
253
+
254
+ it('should throw error for invalid status', async () => {
255
+ const ctx = createMockContext();
256
+
257
+ await expect(
258
+ getFileCheckouts({ project_id: VALID_UUID, status: 'invalid_status' }, ctx)
259
+ ).rejects.toThrow(ValidationError);
260
+ });
261
+
262
+ it('should get file checkouts successfully', async () => {
263
+ mockApiClient.getFileCheckouts.mockResolvedValue({
264
+ ok: true,
265
+ data: {
266
+ checkouts: [
267
+ { id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
268
+ ],
269
+ },
270
+ });
271
+ const ctx = createMockContext();
272
+
273
+ const result = await getFileCheckouts(
274
+ { project_id: VALID_UUID },
275
+ ctx
276
+ );
277
+
278
+ expect(result.result).toMatchObject({
279
+ checkouts: [
280
+ { id: 'checkout-1', file_path: '/src/index.ts', status: 'checked_out' },
281
+ ],
282
+ });
283
+ });
284
+
285
+ it('should filter by status', async () => {
286
+ mockApiClient.getFileCheckouts.mockResolvedValue({
287
+ ok: true,
288
+ data: { checkouts: [] },
289
+ });
290
+ const ctx = createMockContext();
291
+
292
+ await getFileCheckouts(
293
+ { project_id: VALID_UUID, status: 'checked_out' },
294
+ ctx
295
+ );
296
+
297
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
298
+ VALID_UUID,
299
+ { status: 'checked_out', file_path: undefined, limit: 50 }
300
+ );
301
+ });
302
+
303
+ it('should filter by file_path', async () => {
304
+ mockApiClient.getFileCheckouts.mockResolvedValue({
305
+ ok: true,
306
+ data: { checkouts: [] },
307
+ });
308
+ const ctx = createMockContext();
309
+
310
+ await getFileCheckouts(
311
+ { project_id: VALID_UUID, file_path: '/src/utils.ts' },
312
+ ctx
313
+ );
314
+
315
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
316
+ VALID_UUID,
317
+ { status: undefined, file_path: '/src/utils.ts', limit: 50 }
318
+ );
319
+ });
320
+
321
+ it('should use custom limit', async () => {
322
+ mockApiClient.getFileCheckouts.mockResolvedValue({
323
+ ok: true,
324
+ data: { checkouts: [] },
325
+ });
326
+ const ctx = createMockContext();
327
+
328
+ await getFileCheckouts(
329
+ { project_id: VALID_UUID, limit: 10 },
330
+ ctx
331
+ );
332
+
333
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(
334
+ VALID_UUID,
335
+ { status: undefined, file_path: undefined, limit: 10 }
336
+ );
337
+ });
338
+
339
+ it('should accept all valid status values', async () => {
340
+ mockApiClient.getFileCheckouts.mockResolvedValue({
341
+ ok: true,
342
+ data: { checkouts: [] },
343
+ });
344
+ const ctx = createMockContext();
345
+
346
+ for (const status of ['checked_out', 'checked_in', 'abandoned']) {
347
+ await getFileCheckouts(
348
+ { project_id: VALID_UUID, status },
349
+ ctx
350
+ );
351
+ }
352
+
353
+ expect(mockApiClient.getFileCheckouts).toHaveBeenCalledTimes(3);
354
+ });
355
+
356
+ it('should throw error when API call fails', async () => {
357
+ mockApiClient.getFileCheckouts.mockResolvedValue({
358
+ ok: false,
359
+ error: 'Database error',
360
+ });
361
+ const ctx = createMockContext();
362
+
363
+ await expect(
364
+ getFileCheckouts({ project_id: VALID_UUID }, ctx)
365
+ ).rejects.toThrow('Database error');
366
+ });
367
+ });
368
+
369
+ // ============================================================================
370
+ // abandonCheckout Tests
371
+ // ============================================================================
372
+
373
+ describe('abandonCheckout', () => {
374
+ beforeEach(() => vi.clearAllMocks());
375
+
376
+ it('should throw error when neither checkout_id nor project_id+file_path provided', async () => {
377
+ const ctx = createMockContext();
378
+
379
+ await expect(
380
+ abandonCheckout({}, ctx)
381
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
382
+ });
383
+
384
+ it('should throw error when only project_id provided without file_path', async () => {
385
+ const ctx = createMockContext();
386
+
387
+ await expect(
388
+ abandonCheckout({ project_id: VALID_UUID }, ctx)
389
+ ).rejects.toThrow('Either checkout_id or both project_id and file_path are required');
390
+ });
391
+
392
+ it('should throw error for invalid checkout_id UUID', async () => {
393
+ const ctx = createMockContext();
394
+
395
+ await expect(
396
+ abandonCheckout({ checkout_id: 'invalid' }, ctx)
397
+ ).rejects.toThrow(ValidationError);
398
+ });
399
+
400
+ it('should abandon checkout successfully with checkout_id', async () => {
401
+ mockApiClient.abandonCheckout.mockResolvedValue({
402
+ ok: true,
403
+ data: { success: true },
404
+ });
405
+ const ctx = createMockContext();
406
+
407
+ const result = await abandonCheckout(
408
+ { checkout_id: VALID_UUID },
409
+ ctx
410
+ );
411
+
412
+ expect(result.result).toMatchObject({ success: true });
413
+ });
414
+
415
+ it('should abandon checkout successfully with project_id and file_path', async () => {
416
+ mockApiClient.abandonCheckout.mockResolvedValue({
417
+ ok: true,
418
+ data: { success: true },
419
+ });
420
+ const ctx = createMockContext();
421
+
422
+ const result = await abandonCheckout(
423
+ {
424
+ project_id: VALID_UUID,
425
+ file_path: '/src/index.ts',
426
+ },
427
+ ctx
428
+ );
429
+
430
+ expect(result.result).toMatchObject({ success: true });
431
+ });
432
+
433
+ it('should pass params correctly to API', async () => {
434
+ mockApiClient.abandonCheckout.mockResolvedValue({
435
+ ok: true,
436
+ data: { success: true },
437
+ });
438
+ const ctx = createMockContext();
439
+
440
+ await abandonCheckout(
441
+ {
442
+ project_id: VALID_UUID,
443
+ file_path: '/src/index.ts',
444
+ },
445
+ ctx
446
+ );
447
+
448
+ expect(mockApiClient.abandonCheckout).toHaveBeenCalledWith({
449
+ checkout_id: undefined,
450
+ project_id: VALID_UUID,
451
+ file_path: '/src/index.ts',
452
+ });
453
+ });
454
+
455
+ it('should throw error when API call fails', async () => {
456
+ mockApiClient.abandonCheckout.mockResolvedValue({
457
+ ok: false,
458
+ error: 'Checkout not found',
459
+ });
460
+ const ctx = createMockContext();
461
+
462
+ await expect(
463
+ abandonCheckout({ checkout_id: VALID_UUID }, ctx)
464
+ ).rejects.toThrow('Checkout not found');
465
+ });
466
+
467
+ it('should throw default error when API fails without message', async () => {
468
+ mockApiClient.abandonCheckout.mockResolvedValue({
469
+ ok: false,
470
+ });
471
+ const ctx = createMockContext();
472
+
473
+ await expect(
474
+ abandonCheckout({ checkout_id: VALID_UUID }, ctx)
475
+ ).rejects.toThrow('Failed to abandon checkout');
476
+ });
477
+ });