@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.
- package/README.md +1 -1
- package/dist/api-client.d.ts +120 -2
- package/dist/api-client.js +51 -5
- package/dist/handlers/bodies-of-work.js +84 -50
- package/dist/handlers/cost.js +62 -54
- package/dist/handlers/decisions.js +29 -16
- package/dist/handlers/deployment.js +114 -107
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +55 -657
- package/dist/handlers/fallback.js +42 -28
- package/dist/handlers/file-checkouts.d.ts +18 -0
- package/dist/handlers/file-checkouts.js +101 -0
- package/dist/handlers/findings.d.ts +14 -1
- package/dist/handlers/findings.js +104 -28
- 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 +52 -15
- package/dist/handlers/sprints.js +78 -65
- package/dist/handlers/tasks.js +135 -74
- package/dist/handlers/tool-docs.d.ts +4 -3
- package/dist/handlers/tool-docs.js +252 -5
- package/dist/handlers/validation.js +30 -14
- package/dist/index.js +25 -7
- package/dist/tools.js +417 -4
- package/package.json +1 -1
- package/src/api-client.ts +161 -8
- package/src/handlers/__test-setup__.ts +12 -0
- package/src/handlers/bodies-of-work.ts +127 -111
- 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 +144 -190
- package/src/handlers/discovery.test.ts +4 -5
- package/src/handlers/discovery.ts +60 -746
- package/src/handlers/fallback.test.ts +78 -0
- package/src/handlers/fallback.ts +51 -38
- package/src/handlers/file-checkouts.test.ts +477 -0
- package/src/handlers/file-checkouts.ts +127 -0
- package/src/handlers/findings.test.ts +274 -2
- package/src/handlers/findings.ts +123 -57
- 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.test.ts +37 -2
- package/src/handlers/session.ts +64 -21
- package/src/handlers/sprints.ts +114 -134
- package/src/handlers/tasks.test.ts +61 -0
- package/src/handlers/tasks.ts +170 -139
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/validation.test.ts +53 -1
- package/src/handlers/validation.ts +32 -21
- package/src/index.ts +25 -7
- package/src/tools.ts +417 -4
- 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/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
|
// ============================================================================
|
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}`);
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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,
|
|
90
|
-
const { project_id, activity_type, limit
|
|
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,
|
|
131
|
-
const { project_id } = args
|
|
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
|
+
});
|