@vibescope/mcp-server 0.2.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.
- package/dist/api-client.d.ts +64 -1
- package/dist/api-client.js +34 -3
- package/dist/handlers/bodies-of-work.js +82 -49
- package/dist/handlers/cost.js +62 -54
- package/dist/handlers/decisions.js +29 -16
- package/dist/handlers/deployment.js +112 -106
- package/dist/handlers/discovery.js +35 -5
- package/dist/handlers/fallback.js +24 -19
- package/dist/handlers/file-checkouts.d.ts +18 -0
- package/dist/handlers/file-checkouts.js +101 -0
- package/dist/handlers/findings.d.ts +6 -0
- package/dist/handlers/findings.js +85 -30
- package/dist/handlers/git-issues.js +36 -32
- package/dist/handlers/ideas.js +44 -26
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.js +34 -27
- package/dist/handlers/organizations.js +86 -78
- package/dist/handlers/progress.js +22 -11
- package/dist/handlers/project.js +62 -22
- package/dist/handlers/requests.js +15 -11
- package/dist/handlers/roles.d.ts +18 -0
- package/dist/handlers/roles.js +130 -0
- package/dist/handlers/session.js +30 -8
- package/dist/handlers/sprints.js +76 -64
- package/dist/handlers/tasks.js +113 -73
- package/dist/handlers/validation.js +18 -14
- package/dist/tools.js +387 -0
- package/package.json +1 -1
- package/src/api-client.ts +89 -6
- package/src/handlers/__test-setup__.ts +7 -0
- package/src/handlers/bodies-of-work.ts +101 -101
- package/src/handlers/cost.test.ts +34 -44
- package/src/handlers/cost.ts +77 -92
- package/src/handlers/decisions.test.ts +3 -2
- package/src/handlers/decisions.ts +32 -27
- package/src/handlers/deployment.ts +142 -190
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +37 -6
- package/src/handlers/fallback.ts +31 -29
- package/src/handlers/file-checkouts.test.ts +477 -0
- package/src/handlers/file-checkouts.ts +127 -0
- package/src/handlers/findings.test.ts +145 -0
- package/src/handlers/findings.ts +101 -64
- package/src/handlers/git-issues.ts +40 -80
- package/src/handlers/ideas.ts +56 -54
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +1 -1
- package/src/handlers/milestones.ts +47 -45
- package/src/handlers/organizations.ts +104 -129
- package/src/handlers/progress.ts +24 -22
- package/src/handlers/project.ts +89 -57
- package/src/handlers/requests.ts +18 -14
- package/src/handlers/roles.test.ts +303 -0
- package/src/handlers/roles.ts +208 -0
- package/src/handlers/session.ts +39 -17
- package/src/handlers/sprints.ts +96 -129
- package/src/handlers/tasks.ts +144 -138
- package/src/handlers/validation.test.ts +1 -1
- package/src/handlers/validation.ts +20 -22
- package/src/tools.ts +387 -0
- package/dist/config/tool-categories.d.ts +0 -31
- package/dist/config/tool-categories.js +0 -253
- package/dist/knowledge.d.ts +0 -6
- package/dist/knowledge.js +0 -218
package/src/handlers/fallback.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
14
|
-
import {
|
|
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
|
-
|
|
32
|
-
const { project_id, activity } = args as { project_id: string; activity: string };
|
|
31
|
+
type FallbackActivity = typeof VALID_ACTIVITIES[number];
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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}`);
|
|
@@ -75,10 +90,7 @@ export const startFallbackActivity: Handler = async (args, ctx) => {
|
|
|
75
90
|
};
|
|
76
91
|
|
|
77
92
|
export const stopFallbackActivity: Handler = async (args, ctx) => {
|
|
78
|
-
const { project_id, summary } = args
|
|
79
|
-
|
|
80
|
-
validateRequired(project_id, 'project_id');
|
|
81
|
-
validateUUID(project_id, 'project_id');
|
|
93
|
+
const { project_id, summary } = parseArgs(args, stopFallbackActivitySchema);
|
|
82
94
|
|
|
83
95
|
const { session } = ctx;
|
|
84
96
|
const apiClient = getApiClient();
|
|
@@ -97,15 +109,8 @@ export const stopFallbackActivity: Handler = async (args, ctx) => {
|
|
|
97
109
|
};
|
|
98
110
|
};
|
|
99
111
|
|
|
100
|
-
export const getActivityHistory: Handler = async (args,
|
|
101
|
-
const { project_id, activity_type, limit
|
|
102
|
-
project_id: string;
|
|
103
|
-
activity_type?: string;
|
|
104
|
-
limit?: number;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
validateRequired(project_id, 'project_id');
|
|
108
|
-
validateUUID(project_id, 'project_id');
|
|
112
|
+
export const getActivityHistory: Handler = async (args, _ctx) => {
|
|
113
|
+
const { project_id, activity_type, limit } = parseArgs(args, getActivityHistorySchema);
|
|
109
114
|
|
|
110
115
|
const apiClient = getApiClient();
|
|
111
116
|
|
|
@@ -138,11 +143,8 @@ export const getActivityHistory: Handler = async (args, ctx) => {
|
|
|
138
143
|
};
|
|
139
144
|
};
|
|
140
145
|
|
|
141
|
-
export const getActivitySchedules: Handler = async (args,
|
|
142
|
-
const { project_id } = args
|
|
143
|
-
|
|
144
|
-
validateRequired(project_id, 'project_id');
|
|
145
|
-
validateUUID(project_id, 'project_id');
|
|
146
|
+
export const getActivitySchedules: Handler = async (args, _ctx) => {
|
|
147
|
+
const { project_id } = parseArgs(args, getActivitySchedulesSchema);
|
|
146
148
|
|
|
147
149
|
const apiClient = getApiClient();
|
|
148
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
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Checkouts Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles file checkout/checkin for multi-agent coordination:
|
|
5
|
+
* - checkout_file: Check out a file before editing
|
|
6
|
+
* - checkin_file: Check in a file after editing
|
|
7
|
+
* - get_file_checkouts: Get active checkouts for a project
|
|
8
|
+
* - abandon_checkout: Force release a checkout
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
12
|
+
import { parseArgs, uuidValidator, createEnumValidator } from '../validators.js';
|
|
13
|
+
import { getApiClient } from '../api-client.js';
|
|
14
|
+
|
|
15
|
+
const VALID_CHECKOUT_STATUSES = ['checked_out', 'checked_in', 'abandoned'] as const;
|
|
16
|
+
|
|
17
|
+
// Argument schemas for type-safe parsing
|
|
18
|
+
const checkoutFileSchema = {
|
|
19
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
20
|
+
file_path: { type: 'string' as const, required: true as const },
|
|
21
|
+
reason: { type: 'string' as const },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const checkinFileSchema = {
|
|
25
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
26
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
27
|
+
file_path: { type: 'string' as const },
|
|
28
|
+
summary: { type: 'string' as const },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getFileCheckoutsSchema = {
|
|
32
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
33
|
+
status: { type: 'string' as const, validate: createEnumValidator(VALID_CHECKOUT_STATUSES) },
|
|
34
|
+
file_path: { type: 'string' as const },
|
|
35
|
+
limit: { type: 'number' as const, default: 50 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const abandonCheckoutSchema = {
|
|
39
|
+
checkout_id: { type: 'string' as const, validate: uuidValidator },
|
|
40
|
+
project_id: { type: 'string' as const, validate: uuidValidator },
|
|
41
|
+
file_path: { type: 'string' as const },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const checkoutFile: Handler = async (args, ctx) => {
|
|
45
|
+
const { project_id, file_path, reason } = parseArgs(args, checkoutFileSchema);
|
|
46
|
+
|
|
47
|
+
const apiClient = getApiClient();
|
|
48
|
+
const response = await apiClient.checkoutFile(project_id, file_path, reason, ctx.session.currentSessionId || undefined);
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(response.error || 'Failed to checkout file');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { result: response.data };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const checkinFile: Handler = async (args, ctx) => {
|
|
58
|
+
const { checkout_id, project_id, file_path, summary } = parseArgs(args, checkinFileSchema);
|
|
59
|
+
|
|
60
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
61
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
62
|
+
throw new Error('Either checkout_id or both project_id and file_path are required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const apiClient = getApiClient();
|
|
66
|
+
const response = await apiClient.checkinFile({
|
|
67
|
+
checkout_id,
|
|
68
|
+
project_id,
|
|
69
|
+
file_path,
|
|
70
|
+
summary
|
|
71
|
+
}, ctx.session.currentSessionId || undefined);
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(response.error || 'Failed to checkin file');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { result: response.data };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getFileCheckouts: Handler = async (args, _ctx) => {
|
|
81
|
+
const { project_id, status, file_path, limit } = parseArgs(args, getFileCheckoutsSchema);
|
|
82
|
+
|
|
83
|
+
const apiClient = getApiClient();
|
|
84
|
+
const response = await apiClient.getFileCheckouts(project_id, {
|
|
85
|
+
status,
|
|
86
|
+
file_path,
|
|
87
|
+
limit
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(response.error || 'Failed to get file checkouts');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { result: response.data };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const abandonCheckout: Handler = async (args, _ctx) => {
|
|
98
|
+
const { checkout_id, project_id, file_path } = parseArgs(args, abandonCheckoutSchema);
|
|
99
|
+
|
|
100
|
+
// Validate that either checkout_id or both project_id and file_path are provided
|
|
101
|
+
if (!checkout_id && (!project_id || !file_path)) {
|
|
102
|
+
throw new Error('Either checkout_id or both project_id and file_path are required');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const apiClient = getApiClient();
|
|
106
|
+
const response = await apiClient.abandonCheckout({
|
|
107
|
+
checkout_id,
|
|
108
|
+
project_id,
|
|
109
|
+
file_path
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(response.error || 'Failed to abandon checkout');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { result: response.data };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* File Checkouts handlers registry
|
|
121
|
+
*/
|
|
122
|
+
export const fileCheckoutHandlers: HandlerRegistry = {
|
|
123
|
+
checkout_file: checkoutFile,
|
|
124
|
+
checkin_file: checkinFile,
|
|
125
|
+
get_file_checkouts: getFileCheckouts,
|
|
126
|
+
abandon_checkout: abandonCheckout,
|
|
127
|
+
};
|