@vibescope/mcp-server 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -38
- package/dist/api-client.d.ts +187 -0
- package/dist/api-client.js +53 -1
- package/dist/handlers/blockers.js +9 -8
- package/dist/handlers/bodies-of-work.js +14 -14
- package/dist/handlers/connectors.d.ts +45 -0
- package/dist/handlers/connectors.js +183 -0
- package/dist/handlers/cost.d.ts +10 -0
- package/dist/handlers/cost.js +54 -0
- package/dist/handlers/decisions.js +3 -3
- package/dist/handlers/deployment.js +35 -19
- package/dist/handlers/discovery.d.ts +7 -0
- package/dist/handlers/discovery.js +61 -2
- package/dist/handlers/fallback.js +5 -4
- package/dist/handlers/file-checkouts.d.ts +2 -0
- package/dist/handlers/file-checkouts.js +38 -6
- package/dist/handlers/findings.js +13 -12
- package/dist/handlers/git-issues.js +4 -4
- package/dist/handlers/ideas.js +5 -5
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +3 -0
- package/dist/handlers/milestones.js +5 -5
- package/dist/handlers/organizations.js +13 -13
- package/dist/handlers/progress.js +2 -2
- package/dist/handlers/project.js +6 -6
- package/dist/handlers/requests.js +3 -3
- package/dist/handlers/session.js +28 -9
- package/dist/handlers/sprints.js +17 -17
- package/dist/handlers/tasks.d.ts +2 -0
- package/dist/handlers/tasks.js +78 -20
- package/dist/handlers/types.d.ts +64 -2
- package/dist/handlers/types.js +48 -1
- package/dist/handlers/validation.js +3 -3
- package/dist/index.js +7 -2716
- package/dist/token-tracking.d.ts +74 -0
- package/dist/token-tracking.js +122 -0
- package/dist/tools.js +298 -9
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +17 -0
- package/docs/TOOLS.md +2053 -0
- package/package.json +4 -1
- package/scripts/generate-docs.ts +212 -0
- package/src/api-client.test.ts +723 -0
- package/src/api-client.ts +236 -1
- package/src/handlers/__test-setup__.ts +9 -0
- package/src/handlers/blockers.test.ts +31 -19
- package/src/handlers/blockers.ts +9 -8
- package/src/handlers/bodies-of-work.test.ts +55 -32
- package/src/handlers/bodies-of-work.ts +14 -14
- package/src/handlers/connectors.test.ts +834 -0
- package/src/handlers/connectors.ts +229 -0
- package/src/handlers/cost.ts +66 -0
- package/src/handlers/decisions.test.ts +34 -25
- package/src/handlers/decisions.ts +3 -3
- package/src/handlers/deployment.ts +39 -19
- package/src/handlers/discovery.ts +61 -2
- package/src/handlers/fallback.test.ts +26 -22
- package/src/handlers/fallback.ts +5 -4
- package/src/handlers/file-checkouts.test.ts +242 -49
- package/src/handlers/file-checkouts.ts +44 -6
- package/src/handlers/findings.test.ts +38 -24
- package/src/handlers/findings.ts +13 -12
- package/src/handlers/git-issues.test.ts +51 -43
- package/src/handlers/git-issues.ts +4 -4
- package/src/handlers/ideas.test.ts +28 -23
- package/src/handlers/ideas.ts +5 -5
- package/src/handlers/index.ts +3 -0
- package/src/handlers/milestones.test.ts +33 -28
- package/src/handlers/milestones.ts +5 -5
- package/src/handlers/organizations.test.ts +104 -83
- package/src/handlers/organizations.ts +13 -13
- package/src/handlers/progress.test.ts +20 -14
- package/src/handlers/progress.ts +2 -2
- package/src/handlers/project.test.ts +34 -27
- package/src/handlers/project.ts +6 -6
- package/src/handlers/requests.test.ts +27 -18
- package/src/handlers/requests.ts +3 -3
- package/src/handlers/session.test.ts +47 -0
- package/src/handlers/session.ts +26 -9
- package/src/handlers/sprints.test.ts +71 -50
- package/src/handlers/sprints.ts +17 -17
- package/src/handlers/tasks.test.ts +77 -15
- package/src/handlers/tasks.ts +90 -21
- package/src/handlers/tool-categories.test.ts +66 -0
- package/src/handlers/types.ts +81 -2
- package/src/handlers/validation.test.ts +78 -45
- package/src/handlers/validation.ts +3 -3
- package/src/index.ts +12 -2732
- package/src/token-tracking.test.ts +453 -0
- package/src/token-tracking.ts +164 -0
- package/src/tools.ts +298 -9
- package/src/utils.test.ts +2 -2
- package/src/utils.ts +17 -0
|
@@ -38,8 +38,8 @@ async function getToolDocs(): Promise<Record<string, string>> {
|
|
|
38
38
|
return toolInfoCache;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Tool categories with brief descriptions
|
|
42
|
-
const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name: string; brief: string }> }> = {
|
|
41
|
+
// Tool categories with brief descriptions (exported for documentation generation)
|
|
42
|
+
export const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name: string; brief: string }> }> = {
|
|
43
43
|
session: {
|
|
44
44
|
description: 'Session lifecycle and monitoring',
|
|
45
45
|
tools: [
|
|
@@ -124,6 +124,7 @@ const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name
|
|
|
124
124
|
tools: [
|
|
125
125
|
{ name: 'add_finding', brief: 'Record audit finding' },
|
|
126
126
|
{ name: 'get_findings', brief: 'List findings' },
|
|
127
|
+
{ name: 'get_findings_stats', brief: 'Get findings statistics' },
|
|
127
128
|
{ name: 'update_finding', brief: 'Update finding status' },
|
|
128
129
|
{ name: 'delete_finding', brief: 'Remove finding' },
|
|
129
130
|
],
|
|
@@ -177,6 +178,10 @@ const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name
|
|
|
177
178
|
{ name: 'add_task_to_body_of_work', brief: 'Add task to group' },
|
|
178
179
|
{ name: 'remove_task_from_body_of_work', brief: 'Remove from group' },
|
|
179
180
|
{ name: 'activate_body_of_work', brief: 'Activate for work' },
|
|
181
|
+
{ name: 'add_task_dependency', brief: 'Add task dependency' },
|
|
182
|
+
{ name: 'remove_task_dependency', brief: 'Remove task dependency' },
|
|
183
|
+
{ name: 'get_task_dependencies', brief: 'List task dependencies' },
|
|
184
|
+
{ name: 'get_next_body_of_work_task', brief: 'Get next available task' },
|
|
180
185
|
],
|
|
181
186
|
},
|
|
182
187
|
sprints: {
|
|
@@ -230,6 +235,8 @@ const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name
|
|
|
230
235
|
{ name: 'update_cost_alert', brief: 'Update alert config' },
|
|
231
236
|
{ name: 'delete_cost_alert', brief: 'Remove alert' },
|
|
232
237
|
{ name: 'get_task_costs', brief: 'Cost per task' },
|
|
238
|
+
{ name: 'get_body_of_work_costs', brief: 'Cost per body of work' },
|
|
239
|
+
{ name: 'get_sprint_costs', brief: 'Cost per sprint' },
|
|
233
240
|
],
|
|
234
241
|
},
|
|
235
242
|
git_issues: {
|
|
@@ -247,6 +254,58 @@ const TOOL_CATEGORIES: Record<string, { description: string; tools: Array<{ name
|
|
|
247
254
|
{ name: 'query_knowledge_base', brief: 'Aggregated project knowledge in one call' },
|
|
248
255
|
],
|
|
249
256
|
},
|
|
257
|
+
discovery: {
|
|
258
|
+
description: 'Tool discovery and documentation',
|
|
259
|
+
tools: [
|
|
260
|
+
{ name: 'discover_tools', brief: 'List tools by category' },
|
|
261
|
+
{ name: 'get_tool_info', brief: 'Get detailed tool docs' },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
subtasks: {
|
|
265
|
+
description: 'Break tasks into smaller pieces',
|
|
266
|
+
tools: [
|
|
267
|
+
{ name: 'add_subtask', brief: 'Add subtask to task' },
|
|
268
|
+
{ name: 'get_subtasks', brief: 'List task subtasks' },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
worktrees: {
|
|
272
|
+
description: 'Git worktree management',
|
|
273
|
+
tools: [
|
|
274
|
+
{ name: 'get_stale_worktrees', brief: 'Find orphaned worktrees' },
|
|
275
|
+
{ name: 'clear_worktree_path', brief: 'Clear worktree from task' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
roles: {
|
|
279
|
+
description: 'Agent role management',
|
|
280
|
+
tools: [
|
|
281
|
+
{ name: 'get_role_settings', brief: 'Get project role settings' },
|
|
282
|
+
{ name: 'update_role_settings', brief: 'Configure role behavior' },
|
|
283
|
+
{ name: 'set_session_role', brief: 'Set session role' },
|
|
284
|
+
{ name: 'get_agents_by_role', brief: 'List agents by role' },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
file_locks: {
|
|
288
|
+
description: 'File checkout/locking for multi-agent',
|
|
289
|
+
tools: [
|
|
290
|
+
{ name: 'checkout_file', brief: 'Lock file for editing' },
|
|
291
|
+
{ name: 'checkin_file', brief: 'Release file lock' },
|
|
292
|
+
{ name: 'get_file_checkouts', brief: 'List file locks' },
|
|
293
|
+
{ name: 'abandon_checkout', brief: 'Force-release lock' },
|
|
294
|
+
{ name: 'is_file_available', brief: 'Check if file is free' },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
connectors: {
|
|
298
|
+
description: 'External integration connectors',
|
|
299
|
+
tools: [
|
|
300
|
+
{ name: 'get_connectors', brief: 'List project connectors' },
|
|
301
|
+
{ name: 'get_connector', brief: 'Get connector details' },
|
|
302
|
+
{ name: 'add_connector', brief: 'Create new connector' },
|
|
303
|
+
{ name: 'update_connector', brief: 'Update connector config' },
|
|
304
|
+
{ name: 'delete_connector', brief: 'Remove connector' },
|
|
305
|
+
{ name: 'test_connector', brief: 'Send test event' },
|
|
306
|
+
{ name: 'get_connector_events', brief: 'Event history' },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
250
309
|
};
|
|
251
310
|
|
|
252
311
|
export const discoverTools: Handler = async (args) => {
|
|
@@ -48,7 +48,7 @@ describe('startFallbackActivity', () => {
|
|
|
48
48
|
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
49
49
|
activity: 'invalid_activity',
|
|
50
50
|
}, ctx)
|
|
51
|
-
).rejects.toThrow(
|
|
51
|
+
).rejects.toThrow(ValidationError);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it('should start fallback activity successfully', async () => {
|
|
@@ -131,19 +131,20 @@ describe('startFallbackActivity', () => {
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
it('should
|
|
134
|
+
it('should return error when API call fails', async () => {
|
|
135
135
|
mockApiClient.startFallbackActivity.mockResolvedValue({
|
|
136
136
|
ok: false,
|
|
137
137
|
error: 'Failed to start activity',
|
|
138
138
|
});
|
|
139
139
|
const ctx = createMockContext();
|
|
140
140
|
|
|
141
|
-
await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
).
|
|
141
|
+
const result = await startFallbackActivity({
|
|
142
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
143
|
+
activity: 'code_review',
|
|
144
|
+
}, ctx);
|
|
145
|
+
|
|
146
|
+
expect(result.isError).toBe(true);
|
|
147
|
+
expect(result.result).toMatchObject({ error: 'Failed to start activity' });
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
it('should pass through worktree guidance when API returns it', async () => {
|
|
@@ -286,18 +287,19 @@ describe('stopFallbackActivity', () => {
|
|
|
286
287
|
);
|
|
287
288
|
});
|
|
288
289
|
|
|
289
|
-
it('should
|
|
290
|
+
it('should return error when API call fails', async () => {
|
|
290
291
|
mockApiClient.stopFallbackActivity.mockResolvedValue({
|
|
291
292
|
ok: false,
|
|
292
293
|
error: 'Failed to stop activity',
|
|
293
294
|
});
|
|
294
295
|
const ctx = createMockContext();
|
|
295
296
|
|
|
296
|
-
await
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
).
|
|
297
|
+
const result = await stopFallbackActivity({
|
|
298
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
299
|
+
}, ctx);
|
|
300
|
+
|
|
301
|
+
expect(result.isError).toBe(true);
|
|
302
|
+
expect(result.result).toMatchObject({ error: 'Failed to stop activity' });
|
|
301
303
|
});
|
|
302
304
|
});
|
|
303
305
|
|
|
@@ -405,16 +407,17 @@ describe('getActivityHistory', () => {
|
|
|
405
407
|
);
|
|
406
408
|
});
|
|
407
409
|
|
|
408
|
-
it('should
|
|
410
|
+
it('should return error when API call fails', async () => {
|
|
409
411
|
mockApiClient.proxy.mockResolvedValue({
|
|
410
412
|
ok: false,
|
|
411
413
|
error: 'Query failed',
|
|
412
414
|
});
|
|
413
415
|
const ctx = createMockContext();
|
|
414
416
|
|
|
415
|
-
await
|
|
416
|
-
|
|
417
|
-
).
|
|
417
|
+
const result = await getActivityHistory({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
|
|
418
|
+
|
|
419
|
+
expect(result.isError).toBe(true);
|
|
420
|
+
expect(result.result).toMatchObject({ error: 'Query failed' });
|
|
418
421
|
});
|
|
419
422
|
});
|
|
420
423
|
|
|
@@ -518,15 +521,16 @@ describe('getActivitySchedules', () => {
|
|
|
518
521
|
);
|
|
519
522
|
});
|
|
520
523
|
|
|
521
|
-
it('should
|
|
524
|
+
it('should return error when API call fails', async () => {
|
|
522
525
|
mockApiClient.proxy.mockResolvedValue({
|
|
523
526
|
ok: false,
|
|
524
527
|
error: 'Query failed',
|
|
525
528
|
});
|
|
526
529
|
const ctx = createMockContext();
|
|
527
530
|
|
|
528
|
-
await
|
|
529
|
-
|
|
530
|
-
).
|
|
531
|
+
const result = await getActivitySchedules({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
|
|
532
|
+
|
|
533
|
+
expect(result.isError).toBe(true);
|
|
534
|
+
expect(result.result).toMatchObject({ error: 'Query failed' });
|
|
531
535
|
});
|
|
532
536
|
});
|
package/src/handlers/fallback.ts
CHANGED
|
@@ -26,6 +26,7 @@ const VALID_ACTIVITIES = [
|
|
|
26
26
|
'documentation_review',
|
|
27
27
|
'dependency_audit',
|
|
28
28
|
'validate_completed_tasks',
|
|
29
|
+
'worktree_cleanup',
|
|
29
30
|
] as const;
|
|
30
31
|
|
|
31
32
|
type FallbackActivity = typeof VALID_ACTIVITIES[number];
|
|
@@ -60,7 +61,7 @@ export const startFallbackActivity: Handler = async (args, ctx) => {
|
|
|
60
61
|
const response = await apiClient.startFallbackActivity(project_id, activity as FallbackActivity, session.currentSessionId || undefined);
|
|
61
62
|
|
|
62
63
|
if (!response.ok) {
|
|
63
|
-
|
|
64
|
+
return { result: { error: response.error || 'Failed to start fallback activity' }, isError: true };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// Get the activity details for the response
|
|
@@ -98,7 +99,7 @@ export const stopFallbackActivity: Handler = async (args, ctx) => {
|
|
|
98
99
|
const response = await apiClient.stopFallbackActivity(project_id, summary, session.currentSessionId || undefined);
|
|
99
100
|
|
|
100
101
|
if (!response.ok) {
|
|
101
|
-
|
|
102
|
+
return { result: { error: response.error || 'Failed to stop fallback activity' }, isError: true };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
return {
|
|
@@ -131,7 +132,7 @@ export const getActivityHistory: Handler = async (args, _ctx) => {
|
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
if (!response.ok) {
|
|
134
|
-
|
|
135
|
+
return { result: { error: response.error || 'Failed to get activity history' }, isError: true };
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
return {
|
|
@@ -164,7 +165,7 @@ export const getActivitySchedules: Handler = async (args, _ctx) => {
|
|
|
164
165
|
});
|
|
165
166
|
|
|
166
167
|
if (!response.ok) {
|
|
167
|
-
|
|
168
|
+
return { result: { error: response.error || 'Failed to get activity schedules' }, isError: true };
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
return {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
checkinFile,
|
|
5
5
|
getFileCheckouts,
|
|
6
6
|
abandonCheckout,
|
|
7
|
+
isFileAvailable,
|
|
7
8
|
} from './file-checkouts.js';
|
|
8
9
|
import { ValidationError } from '../validators.js';
|
|
9
10
|
import { createMockContext, testUUID } from './__test-utils__.js';
|
|
@@ -87,33 +88,35 @@ describe('checkoutFile', () => {
|
|
|
87
88
|
);
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
it('should
|
|
91
|
+
it('should return error when API call fails', async () => {
|
|
91
92
|
mockApiClient.checkoutFile.mockResolvedValue({
|
|
92
93
|
ok: false,
|
|
93
94
|
error: 'File already checked out',
|
|
94
95
|
});
|
|
95
96
|
const ctx = createMockContext();
|
|
96
97
|
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
).
|
|
98
|
+
const result = await checkoutFile({
|
|
99
|
+
project_id: VALID_UUID,
|
|
100
|
+
file_path: '/src/index.ts',
|
|
101
|
+
}, ctx);
|
|
102
|
+
|
|
103
|
+
expect(result.isError).toBe(true);
|
|
104
|
+
expect(result.result).toMatchObject({ error: 'File already checked out' });
|
|
103
105
|
});
|
|
104
106
|
|
|
105
|
-
it('should
|
|
107
|
+
it('should return default error when API fails without message', async () => {
|
|
106
108
|
mockApiClient.checkoutFile.mockResolvedValue({
|
|
107
109
|
ok: false,
|
|
108
110
|
});
|
|
109
111
|
const ctx = createMockContext();
|
|
110
112
|
|
|
111
|
-
await
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
).
|
|
113
|
+
const result = await checkoutFile({
|
|
114
|
+
project_id: VALID_UUID,
|
|
115
|
+
file_path: '/src/index.ts',
|
|
116
|
+
}, ctx);
|
|
117
|
+
|
|
118
|
+
expect(result.isError).toBe(true);
|
|
119
|
+
expect(result.result).toMatchObject({ error: 'Failed to checkout file' });
|
|
117
120
|
});
|
|
118
121
|
});
|
|
119
122
|
|
|
@@ -124,28 +127,31 @@ describe('checkoutFile', () => {
|
|
|
124
127
|
describe('checkinFile', () => {
|
|
125
128
|
beforeEach(() => vi.clearAllMocks());
|
|
126
129
|
|
|
127
|
-
it('should
|
|
130
|
+
it('should return error when neither checkout_id nor project_id+file_path provided', async () => {
|
|
128
131
|
const ctx = createMockContext();
|
|
129
132
|
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
).
|
|
133
|
+
const result = await checkinFile({}, ctx);
|
|
134
|
+
|
|
135
|
+
expect(result.isError).toBe(true);
|
|
136
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
133
137
|
});
|
|
134
138
|
|
|
135
|
-
it('should
|
|
139
|
+
it('should return error when only project_id provided without file_path', async () => {
|
|
136
140
|
const ctx = createMockContext();
|
|
137
141
|
|
|
138
|
-
await
|
|
139
|
-
|
|
140
|
-
).
|
|
142
|
+
const result = await checkinFile({ project_id: VALID_UUID }, ctx);
|
|
143
|
+
|
|
144
|
+
expect(result.isError).toBe(true);
|
|
145
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
141
146
|
});
|
|
142
147
|
|
|
143
|
-
it('should
|
|
148
|
+
it('should return error when only file_path provided without project_id', async () => {
|
|
144
149
|
const ctx = createMockContext();
|
|
145
150
|
|
|
146
|
-
await
|
|
147
|
-
|
|
148
|
-
).
|
|
151
|
+
const result = await checkinFile({ file_path: '/src/index.ts' }, ctx);
|
|
152
|
+
|
|
153
|
+
expect(result.isError).toBe(true);
|
|
154
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
149
155
|
});
|
|
150
156
|
|
|
151
157
|
it('should throw error for invalid checkout_id UUID', async () => {
|
|
@@ -215,16 +221,17 @@ describe('checkinFile', () => {
|
|
|
215
221
|
);
|
|
216
222
|
});
|
|
217
223
|
|
|
218
|
-
it('should
|
|
224
|
+
it('should return error when API call fails', async () => {
|
|
219
225
|
mockApiClient.checkinFile.mockResolvedValue({
|
|
220
226
|
ok: false,
|
|
221
227
|
error: 'Checkout not found',
|
|
222
228
|
});
|
|
223
229
|
const ctx = createMockContext();
|
|
224
230
|
|
|
225
|
-
await
|
|
226
|
-
|
|
227
|
-
).
|
|
231
|
+
const result = await checkinFile({ checkout_id: VALID_UUID }, ctx);
|
|
232
|
+
|
|
233
|
+
expect(result.isError).toBe(true);
|
|
234
|
+
expect(result.result).toMatchObject({ error: 'Checkout not found' });
|
|
228
235
|
});
|
|
229
236
|
});
|
|
230
237
|
|
|
@@ -353,16 +360,17 @@ describe('getFileCheckouts', () => {
|
|
|
353
360
|
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledTimes(3);
|
|
354
361
|
});
|
|
355
362
|
|
|
356
|
-
it('should
|
|
363
|
+
it('should return error when API call fails', async () => {
|
|
357
364
|
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
358
365
|
ok: false,
|
|
359
366
|
error: 'Database error',
|
|
360
367
|
});
|
|
361
368
|
const ctx = createMockContext();
|
|
362
369
|
|
|
363
|
-
await
|
|
364
|
-
|
|
365
|
-
).
|
|
370
|
+
const result = await getFileCheckouts({ project_id: VALID_UUID }, ctx);
|
|
371
|
+
|
|
372
|
+
expect(result.isError).toBe(true);
|
|
373
|
+
expect(result.result).toMatchObject({ error: 'Database error' });
|
|
366
374
|
});
|
|
367
375
|
});
|
|
368
376
|
|
|
@@ -373,20 +381,22 @@ describe('getFileCheckouts', () => {
|
|
|
373
381
|
describe('abandonCheckout', () => {
|
|
374
382
|
beforeEach(() => vi.clearAllMocks());
|
|
375
383
|
|
|
376
|
-
it('should
|
|
384
|
+
it('should return error when neither checkout_id nor project_id+file_path provided', async () => {
|
|
377
385
|
const ctx = createMockContext();
|
|
378
386
|
|
|
379
|
-
await
|
|
380
|
-
|
|
381
|
-
).
|
|
387
|
+
const result = await abandonCheckout({}, ctx);
|
|
388
|
+
|
|
389
|
+
expect(result.isError).toBe(true);
|
|
390
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
382
391
|
});
|
|
383
392
|
|
|
384
|
-
it('should
|
|
393
|
+
it('should return error when only project_id provided without file_path', async () => {
|
|
385
394
|
const ctx = createMockContext();
|
|
386
395
|
|
|
387
|
-
await
|
|
388
|
-
|
|
389
|
-
).
|
|
396
|
+
const result = await abandonCheckout({ project_id: VALID_UUID }, ctx);
|
|
397
|
+
|
|
398
|
+
expect(result.isError).toBe(true);
|
|
399
|
+
expect(result.result).toMatchObject({ error: 'Either checkout_id or both project_id and file_path are required' });
|
|
390
400
|
});
|
|
391
401
|
|
|
392
402
|
it('should throw error for invalid checkout_id UUID', async () => {
|
|
@@ -452,26 +462,209 @@ describe('abandonCheckout', () => {
|
|
|
452
462
|
});
|
|
453
463
|
});
|
|
454
464
|
|
|
455
|
-
it('should
|
|
465
|
+
it('should return error when API call fails', async () => {
|
|
456
466
|
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
457
467
|
ok: false,
|
|
458
468
|
error: 'Checkout not found',
|
|
459
469
|
});
|
|
460
470
|
const ctx = createMockContext();
|
|
461
471
|
|
|
462
|
-
await
|
|
463
|
-
|
|
464
|
-
).
|
|
472
|
+
const result = await abandonCheckout({ checkout_id: VALID_UUID }, ctx);
|
|
473
|
+
|
|
474
|
+
expect(result.isError).toBe(true);
|
|
475
|
+
expect(result.result).toMatchObject({ error: 'Checkout not found' });
|
|
465
476
|
});
|
|
466
477
|
|
|
467
|
-
it('should
|
|
478
|
+
it('should return default error when API fails without message', async () => {
|
|
468
479
|
mockApiClient.abandonCheckout.mockResolvedValue({
|
|
469
480
|
ok: false,
|
|
470
481
|
});
|
|
471
482
|
const ctx = createMockContext();
|
|
472
483
|
|
|
484
|
+
const result = await abandonCheckout({ checkout_id: VALID_UUID }, ctx);
|
|
485
|
+
|
|
486
|
+
expect(result.isError).toBe(true);
|
|
487
|
+
expect(result.result).toMatchObject({ error: 'Failed to abandon checkout' });
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// ============================================================================
|
|
492
|
+
// isFileAvailable Tests
|
|
493
|
+
// ============================================================================
|
|
494
|
+
|
|
495
|
+
describe('isFileAvailable', () => {
|
|
496
|
+
beforeEach(() => vi.clearAllMocks());
|
|
497
|
+
|
|
498
|
+
it('should throw error for missing project_id', async () => {
|
|
499
|
+
const ctx = createMockContext();
|
|
500
|
+
|
|
473
501
|
await expect(
|
|
474
|
-
|
|
475
|
-
).rejects.toThrow(
|
|
502
|
+
isFileAvailable({ file_path: '/src/index.ts' }, ctx)
|
|
503
|
+
).rejects.toThrow(ValidationError);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
507
|
+
const ctx = createMockContext();
|
|
508
|
+
|
|
509
|
+
await expect(
|
|
510
|
+
isFileAvailable({ project_id: 'invalid', file_path: '/src/index.ts' }, ctx)
|
|
511
|
+
).rejects.toThrow(ValidationError);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should throw error for missing file_path', async () => {
|
|
515
|
+
const ctx = createMockContext();
|
|
516
|
+
|
|
517
|
+
await expect(
|
|
518
|
+
isFileAvailable({ project_id: VALID_UUID }, ctx)
|
|
519
|
+
).rejects.toThrow(ValidationError);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should return available=true when file has no active checkout', async () => {
|
|
523
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
524
|
+
ok: true,
|
|
525
|
+
data: { checkouts: [] },
|
|
526
|
+
});
|
|
527
|
+
const ctx = createMockContext();
|
|
528
|
+
|
|
529
|
+
const result = await isFileAvailable(
|
|
530
|
+
{
|
|
531
|
+
project_id: VALID_UUID,
|
|
532
|
+
file_path: '/src/index.ts',
|
|
533
|
+
},
|
|
534
|
+
ctx
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(result.result).toMatchObject({
|
|
538
|
+
available: true,
|
|
539
|
+
file_path: '/src/index.ts',
|
|
540
|
+
checked_out_by: null,
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should return available=false with checkout info when file is checked out', async () => {
|
|
545
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
546
|
+
ok: true,
|
|
547
|
+
data: {
|
|
548
|
+
checkouts: [{
|
|
549
|
+
id: 'checkout-123',
|
|
550
|
+
file_path: '/src/index.ts',
|
|
551
|
+
status: 'checked_out',
|
|
552
|
+
checked_out_by: 'Apex',
|
|
553
|
+
checked_out_at: '2026-01-16T10:00:00Z',
|
|
554
|
+
checkout_reason: 'Working on feature X',
|
|
555
|
+
}],
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
const ctx = createMockContext();
|
|
559
|
+
|
|
560
|
+
const result = await isFileAvailable(
|
|
561
|
+
{
|
|
562
|
+
project_id: VALID_UUID,
|
|
563
|
+
file_path: '/src/index.ts',
|
|
564
|
+
},
|
|
565
|
+
ctx
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result.result).toMatchObject({
|
|
569
|
+
available: false,
|
|
570
|
+
file_path: '/src/index.ts',
|
|
571
|
+
checked_out_by: {
|
|
572
|
+
checkout_id: 'checkout-123',
|
|
573
|
+
checked_out_by: 'Apex',
|
|
574
|
+
checked_out_at: '2026-01-16T10:00:00Z',
|
|
575
|
+
reason: 'Working on feature X',
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should query API with correct parameters', async () => {
|
|
581
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
582
|
+
ok: true,
|
|
583
|
+
data: { checkouts: [] },
|
|
584
|
+
});
|
|
585
|
+
const ctx = createMockContext();
|
|
586
|
+
|
|
587
|
+
await isFileAvailable(
|
|
588
|
+
{
|
|
589
|
+
project_id: VALID_UUID,
|
|
590
|
+
file_path: '/src/index.ts',
|
|
591
|
+
},
|
|
592
|
+
ctx
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(mockApiClient.getFileCheckouts).toHaveBeenCalledWith(VALID_UUID, {
|
|
596
|
+
status: 'checked_out',
|
|
597
|
+
file_path: '/src/index.ts',
|
|
598
|
+
limit: 1,
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should return error when API call fails', async () => {
|
|
603
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
604
|
+
ok: false,
|
|
605
|
+
error: 'Project not found',
|
|
606
|
+
});
|
|
607
|
+
const ctx = createMockContext();
|
|
608
|
+
|
|
609
|
+
const result = await isFileAvailable({
|
|
610
|
+
project_id: VALID_UUID,
|
|
611
|
+
file_path: '/src/index.ts',
|
|
612
|
+
}, ctx);
|
|
613
|
+
|
|
614
|
+
expect(result.isError).toBe(true);
|
|
615
|
+
expect(result.result).toMatchObject({ error: 'Project not found' });
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should return default error when API fails without message', async () => {
|
|
619
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
620
|
+
ok: false,
|
|
621
|
+
});
|
|
622
|
+
const ctx = createMockContext();
|
|
623
|
+
|
|
624
|
+
const result = await isFileAvailable({
|
|
625
|
+
project_id: VALID_UUID,
|
|
626
|
+
file_path: '/src/index.ts',
|
|
627
|
+
}, ctx);
|
|
628
|
+
|
|
629
|
+
expect(result.isError).toBe(true);
|
|
630
|
+
expect(result.result).toMatchObject({ error: 'Failed to check file availability' });
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should handle empty checkouts array gracefully', async () => {
|
|
634
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
635
|
+
ok: true,
|
|
636
|
+
data: { checkouts: [] },
|
|
637
|
+
});
|
|
638
|
+
const ctx = createMockContext();
|
|
639
|
+
|
|
640
|
+
const result = await isFileAvailable(
|
|
641
|
+
{
|
|
642
|
+
project_id: VALID_UUID,
|
|
643
|
+
file_path: '/src/index.ts',
|
|
644
|
+
},
|
|
645
|
+
ctx
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
expect(result.result.available).toBe(true);
|
|
649
|
+
expect(result.result.checked_out_by).toBeNull();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should handle undefined checkouts gracefully', async () => {
|
|
653
|
+
mockApiClient.getFileCheckouts.mockResolvedValue({
|
|
654
|
+
ok: true,
|
|
655
|
+
data: {},
|
|
656
|
+
});
|
|
657
|
+
const ctx = createMockContext();
|
|
658
|
+
|
|
659
|
+
const result = await isFileAvailable(
|
|
660
|
+
{
|
|
661
|
+
project_id: VALID_UUID,
|
|
662
|
+
file_path: '/src/index.ts',
|
|
663
|
+
},
|
|
664
|
+
ctx
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
expect(result.result.available).toBe(true);
|
|
668
|
+
expect(result.result.checked_out_by).toBeNull();
|
|
476
669
|
});
|
|
477
670
|
});
|