@vibescope/mcp-server 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +84 -84
  2. package/README.md +194 -194
  3. package/dist/api-client/bodies-of-work.d.ts +125 -0
  4. package/dist/api-client/bodies-of-work.js +78 -0
  5. package/dist/api-client/chat.d.ts +26 -0
  6. package/dist/api-client/chat.js +20 -0
  7. package/dist/api-client/connectors.d.ts +104 -0
  8. package/dist/api-client/connectors.js +46 -0
  9. package/dist/api-client/deployment.d.ts +190 -0
  10. package/dist/api-client/deployment.js +113 -0
  11. package/dist/api-client/file-checkouts.d.ts +71 -0
  12. package/dist/api-client/file-checkouts.js +43 -0
  13. package/dist/api-client/git-issues.d.ts +55 -0
  14. package/dist/api-client/git-issues.js +34 -0
  15. package/dist/api-client/index.d.ts +619 -1
  16. package/dist/api-client/index.js +148 -0
  17. package/dist/api-client/organizations.d.ts +101 -0
  18. package/dist/api-client/organizations.js +86 -0
  19. package/dist/api-client/progress.d.ts +61 -0
  20. package/dist/api-client/progress.js +34 -0
  21. package/dist/api-client/project.d.ts +1 -0
  22. package/dist/api-client/requests.d.ts +28 -0
  23. package/dist/api-client/requests.js +28 -0
  24. package/dist/api-client/sprints.d.ts +153 -0
  25. package/dist/api-client/sprints.js +82 -0
  26. package/dist/api-client/subtasks.d.ts +37 -0
  27. package/dist/api-client/subtasks.js +23 -0
  28. package/dist/api-client.d.ts +23 -0
  29. package/dist/api-client.js +15 -0
  30. package/dist/cli-init.js +21 -21
  31. package/dist/cli.js +26 -26
  32. package/dist/handlers/blockers.js +4 -0
  33. package/dist/handlers/chat.d.ts +23 -0
  34. package/dist/handlers/chat.js +84 -0
  35. package/dist/handlers/deployment.d.ts +3 -0
  36. package/dist/handlers/deployment.js +23 -0
  37. package/dist/handlers/discovery.js +13 -0
  38. package/dist/handlers/index.d.ts +1 -0
  39. package/dist/handlers/index.js +3 -0
  40. package/dist/handlers/project.js +4 -2
  41. package/dist/handlers/session.js +7 -0
  42. package/dist/handlers/tasks.js +7 -0
  43. package/dist/handlers/tool-docs.js +1204 -1131
  44. package/dist/index.js +73 -73
  45. package/dist/templates/agent-guidelines.d.ts +1 -1
  46. package/dist/templates/agent-guidelines.js +205 -187
  47. package/dist/templates/help-content.js +1621 -1621
  48. package/dist/tools/bodies-of-work.js +6 -6
  49. package/dist/tools/chat.d.ts +1 -0
  50. package/dist/tools/chat.js +24 -0
  51. package/dist/tools/cloud-agents.js +22 -22
  52. package/dist/tools/deployment.js +13 -0
  53. package/dist/tools/features.d.ts +13 -0
  54. package/dist/tools/features.js +151 -0
  55. package/dist/tools/index.d.ts +3 -1
  56. package/dist/tools/index.js +4 -1
  57. package/dist/tools/milestones.js +2 -2
  58. package/dist/tools/project.js +4 -0
  59. package/dist/tools/requests.js +1 -1
  60. package/dist/tools/session.js +11 -11
  61. package/dist/tools/sprints.js +9 -9
  62. package/dist/tools/tasks.js +35 -35
  63. package/dist/tools/worktrees.js +14 -14
  64. package/dist/tools.d.ts +2 -0
  65. package/dist/tools.js +3602 -0
  66. package/dist/utils.js +11 -11
  67. package/docs/TOOLS.md +2663 -2545
  68. package/package.json +53 -53
  69. package/scripts/generate-docs.ts +212 -212
  70. package/scripts/version-bump.ts +203 -203
  71. package/src/api-client/blockers.ts +86 -86
  72. package/src/api-client/bodies-of-work.ts +194 -0
  73. package/src/api-client/chat.ts +50 -0
  74. package/src/api-client/connectors.ts +152 -0
  75. package/src/api-client/cost.ts +185 -185
  76. package/src/api-client/decisions.ts +87 -87
  77. package/src/api-client/deployment.ts +313 -0
  78. package/src/api-client/discovery.ts +81 -81
  79. package/src/api-client/fallback.ts +52 -52
  80. package/src/api-client/file-checkouts.ts +115 -0
  81. package/src/api-client/findings.ts +100 -100
  82. package/src/api-client/git-issues.ts +88 -0
  83. package/src/api-client/ideas.ts +112 -112
  84. package/src/api-client/index.ts +592 -426
  85. package/src/api-client/milestones.ts +83 -83
  86. package/src/api-client/organizations.ts +185 -0
  87. package/src/api-client/progress.ts +94 -0
  88. package/src/api-client/project.ts +180 -179
  89. package/src/api-client/requests.ts +54 -0
  90. package/src/api-client/session.ts +220 -220
  91. package/src/api-client/sprints.ts +227 -0
  92. package/src/api-client/subtasks.ts +57 -0
  93. package/src/api-client/tasks.ts +450 -450
  94. package/src/api-client/types.ts +32 -32
  95. package/src/api-client/validation.ts +60 -60
  96. package/src/api-client/worktrees.ts +53 -53
  97. package/src/api-client.test.ts +847 -850
  98. package/src/api-client.ts +2707 -2672
  99. package/src/cli-init.ts +557 -557
  100. package/src/cli.test.ts +284 -284
  101. package/src/cli.ts +204 -204
  102. package/src/handlers/__test-setup__.ts +240 -236
  103. package/src/handlers/__test-utils__.ts +89 -89
  104. package/src/handlers/blockers.test.ts +468 -468
  105. package/src/handlers/blockers.ts +172 -163
  106. package/src/handlers/bodies-of-work.test.ts +704 -704
  107. package/src/handlers/bodies-of-work.ts +526 -526
  108. package/src/handlers/chat.test.ts +185 -0
  109. package/src/handlers/chat.ts +101 -0
  110. package/src/handlers/cloud-agents.test.ts +438 -438
  111. package/src/handlers/cloud-agents.ts +156 -156
  112. package/src/handlers/connectors.test.ts +834 -834
  113. package/src/handlers/connectors.ts +229 -229
  114. package/src/handlers/cost.test.ts +462 -462
  115. package/src/handlers/cost.ts +285 -285
  116. package/src/handlers/decisions.test.ts +382 -382
  117. package/src/handlers/decisions.ts +153 -153
  118. package/src/handlers/deployment.test.ts +551 -551
  119. package/src/handlers/deployment.ts +570 -541
  120. package/src/handlers/discovery.test.ts +206 -206
  121. package/src/handlers/discovery.ts +427 -414
  122. package/src/handlers/fallback.test.ts +537 -537
  123. package/src/handlers/fallback.ts +194 -194
  124. package/src/handlers/file-checkouts.test.ts +750 -750
  125. package/src/handlers/file-checkouts.ts +185 -185
  126. package/src/handlers/findings.test.ts +633 -633
  127. package/src/handlers/findings.ts +239 -239
  128. package/src/handlers/git-issues.test.ts +631 -631
  129. package/src/handlers/git-issues.ts +136 -136
  130. package/src/handlers/ideas.test.ts +644 -644
  131. package/src/handlers/ideas.ts +207 -207
  132. package/src/handlers/index.ts +93 -90
  133. package/src/handlers/milestones.test.ts +475 -475
  134. package/src/handlers/milestones.ts +180 -180
  135. package/src/handlers/organizations.test.ts +826 -826
  136. package/src/handlers/organizations.ts +315 -315
  137. package/src/handlers/progress.test.ts +269 -269
  138. package/src/handlers/progress.ts +77 -77
  139. package/src/handlers/project.test.ts +546 -546
  140. package/src/handlers/project.ts +242 -239
  141. package/src/handlers/requests.test.ts +303 -303
  142. package/src/handlers/requests.ts +99 -99
  143. package/src/handlers/roles.test.ts +305 -305
  144. package/src/handlers/roles.ts +219 -219
  145. package/src/handlers/session.test.ts +998 -998
  146. package/src/handlers/session.ts +1105 -1093
  147. package/src/handlers/sprints.test.ts +732 -732
  148. package/src/handlers/sprints.ts +537 -537
  149. package/src/handlers/tasks.test.ts +931 -931
  150. package/src/handlers/tasks.ts +1133 -1121
  151. package/src/handlers/tool-categories.test.ts +66 -66
  152. package/src/handlers/tool-docs.test.ts +511 -511
  153. package/src/handlers/tool-docs.ts +1571 -1491
  154. package/src/handlers/types.test.ts +259 -259
  155. package/src/handlers/types.ts +176 -176
  156. package/src/handlers/validation.test.ts +582 -582
  157. package/src/handlers/validation.ts +164 -164
  158. package/src/handlers/version.ts +63 -63
  159. package/src/index.test.ts +674 -674
  160. package/src/index.ts +807 -807
  161. package/src/setup.test.ts +233 -233
  162. package/src/setup.ts +404 -404
  163. package/src/templates/agent-guidelines.ts +233 -215
  164. package/src/templates/help-content.ts +1751 -1751
  165. package/src/token-tracking.test.ts +463 -463
  166. package/src/token-tracking.ts +167 -167
  167. package/src/tools/blockers.ts +122 -122
  168. package/src/tools/bodies-of-work.ts +283 -283
  169. package/src/tools/chat.ts +72 -46
  170. package/src/tools/cloud-agents.ts +101 -101
  171. package/src/tools/connectors.ts +191 -191
  172. package/src/tools/cost.ts +111 -111
  173. package/src/tools/decisions.ts +111 -111
  174. package/src/tools/deployment.ts +455 -442
  175. package/src/tools/discovery.ts +76 -76
  176. package/src/tools/fallback.ts +111 -111
  177. package/src/tools/features.ts +154 -0
  178. package/src/tools/file-checkouts.ts +145 -145
  179. package/src/tools/findings.ts +101 -101
  180. package/src/tools/git-issues.ts +130 -130
  181. package/src/tools/ideas.ts +162 -162
  182. package/src/tools/index.ts +141 -137
  183. package/src/tools/milestones.ts +118 -118
  184. package/src/tools/organizations.ts +224 -224
  185. package/src/tools/progress.ts +73 -73
  186. package/src/tools/project.ts +206 -202
  187. package/src/tools/requests.ts +68 -68
  188. package/src/tools/roles.ts +112 -112
  189. package/src/tools/session.ts +181 -181
  190. package/src/tools/sprints.ts +298 -298
  191. package/src/tools/tasks.ts +550 -550
  192. package/src/tools/tools.test.ts +222 -222
  193. package/src/tools/types.ts +9 -9
  194. package/src/tools/validation.ts +75 -75
  195. package/src/tools/version.ts +34 -34
  196. package/src/tools/worktrees.ts +66 -66
  197. package/src/tools.test.ts +416 -416
  198. package/src/utils.test.ts +1014 -1014
  199. package/src/utils.ts +586 -586
  200. package/src/validators.test.ts +223 -223
  201. package/src/validators.ts +249 -249
  202. package/src/version.ts +109 -109
  203. package/tsconfig.json +16 -16
  204. package/vitest.config.ts +14 -14
@@ -1,931 +1,931 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import {
3
- getNextTask,
4
- addTask,
5
- updateTask,
6
- completeTask,
7
- deleteTask,
8
- addTaskReference,
9
- removeTaskReference,
10
- batchUpdateTasks,
11
- batchCompleteTasks,
12
- addSubtask,
13
- getSubtasks,
14
- getValidationApprovedGitInstructions,
15
- } from './tasks.js';
16
- import { ValidationError } from '../validators.js';
17
- import { createMockContext } from './__test-utils__.js';
18
- import { mockApiClient } from './__test-setup__.js';
19
-
20
- // ============================================================================
21
- // getNextTask Tests
22
- // ============================================================================
23
-
24
- describe('getNextTask', () => {
25
- beforeEach(() => vi.clearAllMocks());
26
-
27
- it('should throw error for missing project_id', async () => {
28
- const ctx = createMockContext();
29
- await expect(getNextTask({}, ctx)).rejects.toThrow(ValidationError);
30
- });
31
-
32
- it('should throw error for invalid project_id', async () => {
33
- const ctx = createMockContext();
34
- await expect(getNextTask({ project_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
35
- });
36
-
37
- it('should return next task when available', async () => {
38
- mockApiClient.getNextTask.mockResolvedValue({
39
- ok: true,
40
- data: {
41
- task: { id: 'task-1', title: 'Next task', priority: 1 },
42
- directive: 'Start working',
43
- },
44
- });
45
-
46
- const ctx = createMockContext();
47
- const result = await getNextTask(
48
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
49
- ctx
50
- );
51
-
52
- expect(result.result.task).toMatchObject({ id: 'task-1' });
53
- });
54
-
55
- it('should return null when no tasks available', async () => {
56
- mockApiClient.getNextTask.mockResolvedValue({
57
- ok: true,
58
- data: { task: null, message: 'No tasks available' },
59
- });
60
-
61
- const ctx = createMockContext();
62
- const result = await getNextTask(
63
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
64
- ctx
65
- );
66
-
67
- expect(result.result.task).toBeNull();
68
- });
69
-
70
- it('should pass session_id to API', async () => {
71
- mockApiClient.getNextTask.mockResolvedValue({
72
- ok: true,
73
- data: { task: null },
74
- });
75
-
76
- const ctx = createMockContext({ sessionId: 'my-session' });
77
- await getNextTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
78
-
79
- expect(mockApiClient.getNextTask).toHaveBeenCalledWith(
80
- '123e4567-e89b-12d3-a456-426614174000',
81
- 'my-session'
82
- );
83
- });
84
- });
85
-
86
- // ============================================================================
87
- // addTask Tests
88
- // ============================================================================
89
-
90
- describe('addTask', () => {
91
- beforeEach(() => vi.clearAllMocks());
92
-
93
- it('should throw error for missing project_id', async () => {
94
- const ctx = createMockContext();
95
- await expect(addTask({ title: 'Test task' }, ctx)).rejects.toThrow(ValidationError);
96
- });
97
-
98
- it('should throw error for invalid project_id', async () => {
99
- const ctx = createMockContext();
100
- await expect(
101
- addTask({ project_id: 'invalid', title: 'Test task' }, ctx)
102
- ).rejects.toThrow(ValidationError);
103
- });
104
-
105
- it('should throw error for missing title', async () => {
106
- const ctx = createMockContext();
107
- await expect(
108
- addTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
109
- ).rejects.toThrow(ValidationError);
110
- });
111
-
112
- it('should throw error for invalid priority', async () => {
113
- const ctx = createMockContext();
114
- await expect(
115
- addTask({
116
- project_id: '123e4567-e89b-12d3-a456-426614174000',
117
- title: 'Test',
118
- priority: 10,
119
- }, ctx)
120
- ).rejects.toThrow(ValidationError);
121
- });
122
-
123
- it('should add task successfully', async () => {
124
- mockApiClient.createTask.mockResolvedValue({
125
- ok: true,
126
- data: { task_id: 'new-task-id', title: 'Test task' },
127
- });
128
-
129
- const ctx = createMockContext();
130
- const result = await addTask(
131
- {
132
- project_id: '123e4567-e89b-12d3-a456-426614174000',
133
- title: 'Test task',
134
- },
135
- ctx
136
- );
137
-
138
- expect(result.result).toMatchObject({
139
- success: true,
140
- task_id: 'new-task-id',
141
- });
142
- });
143
-
144
- it('should include optional fields in API call', async () => {
145
- mockApiClient.createTask.mockResolvedValue({
146
- ok: true,
147
- data: { task_id: 'new-task-id' },
148
- });
149
-
150
- const ctx = createMockContext();
151
- await addTask(
152
- {
153
- project_id: '123e4567-e89b-12d3-a456-426614174000',
154
- title: 'Test task',
155
- description: 'A description',
156
- priority: 2,
157
- estimated_minutes: 30,
158
- blocking: true,
159
- },
160
- ctx
161
- );
162
-
163
- expect(mockApiClient.createTask).toHaveBeenCalledWith(
164
- '123e4567-e89b-12d3-a456-426614174000',
165
- expect.objectContaining({
166
- title: 'Test task',
167
- description: 'A description',
168
- priority: 2,
169
- estimated_minutes: 30,
170
- blocking: true,
171
- })
172
- );
173
- });
174
- });
175
-
176
- // ============================================================================
177
- // updateTask Tests
178
- // ============================================================================
179
-
180
- describe('updateTask', () => {
181
- beforeEach(() => vi.clearAllMocks());
182
-
183
- it('should throw error for missing task_id', async () => {
184
- const ctx = createMockContext();
185
- await expect(updateTask({ status: 'in_progress' }, ctx)).rejects.toThrow(ValidationError);
186
- });
187
-
188
- it('should throw error for invalid task_id', async () => {
189
- const ctx = createMockContext();
190
- await expect(
191
- updateTask({ task_id: 'invalid', status: 'in_progress' }, ctx)
192
- ).rejects.toThrow(ValidationError);
193
- });
194
-
195
- it('should throw error for invalid status', async () => {
196
- const ctx = createMockContext();
197
- await expect(
198
- updateTask({
199
- task_id: '123e4567-e89b-12d3-a456-426614174000',
200
- status: 'invalid',
201
- }, ctx)
202
- ).rejects.toThrow(ValidationError);
203
- });
204
-
205
- it('should throw error for invalid priority', async () => {
206
- const ctx = createMockContext();
207
- await expect(
208
- updateTask({
209
- task_id: '123e4567-e89b-12d3-a456-426614174000',
210
- priority: 0,
211
- }, ctx)
212
- ).rejects.toThrow(ValidationError);
213
- });
214
-
215
- it('should throw error for invalid progress_percentage', async () => {
216
- const ctx = createMockContext();
217
- await expect(
218
- updateTask({
219
- task_id: '123e4567-e89b-12d3-a456-426614174000',
220
- progress_percentage: 150,
221
- }, ctx)
222
- ).rejects.toThrow(ValidationError);
223
- });
224
-
225
- it('should update task successfully', async () => {
226
- mockApiClient.updateTask.mockResolvedValue({
227
- ok: true,
228
- data: { success: true },
229
- });
230
-
231
- const ctx = createMockContext();
232
- const result = await updateTask(
233
- {
234
- task_id: '123e4567-e89b-12d3-a456-426614174000',
235
- status: 'in_progress',
236
- git_branch: 'feature/test-branch',
237
- },
238
- ctx
239
- );
240
-
241
- expect(result.result).toMatchObject({ success: true });
242
- });
243
-
244
- it('should handle agent_task_limit error', async () => {
245
- mockApiClient.updateTask.mockResolvedValue({
246
- ok: false,
247
- error: 'agent_task_limit: Agent already has a task in progress',
248
- });
249
-
250
- const ctx = createMockContext();
251
- const result = await updateTask(
252
- {
253
- task_id: '123e4567-e89b-12d3-a456-426614174000',
254
- status: 'in_progress',
255
- git_branch: 'feature/test-branch',
256
- },
257
- ctx
258
- );
259
-
260
- expect(result.result).toMatchObject({
261
- error: 'agent_task_limit',
262
- });
263
- });
264
-
265
- it('should handle task_claimed error', async () => {
266
- mockApiClient.updateTask.mockResolvedValue({
267
- ok: false,
268
- error: 'task_claimed: Task is being worked on by another agent',
269
- });
270
-
271
- const ctx = createMockContext();
272
- const result = await updateTask(
273
- {
274
- task_id: '123e4567-e89b-12d3-a456-426614174000',
275
- status: 'in_progress',
276
- git_branch: 'feature/test-branch',
277
- },
278
- ctx
279
- );
280
-
281
- expect(result.result).toMatchObject({
282
- error: 'task_already_claimed',
283
- });
284
- });
285
-
286
- it('should return error when setting in_progress without git_branch', async () => {
287
- const ctx = createMockContext();
288
- const result = await updateTask(
289
- {
290
- task_id: '123e4567-e89b-12d3-a456-426614174000',
291
- status: 'in_progress',
292
- },
293
- ctx
294
- );
295
-
296
- expect(result.result).toMatchObject({
297
- error: 'worktree_required',
298
- message: expect.stringContaining('git_branch is required'),
299
- hint: expect.stringContaining('Create a worktree'),
300
- });
301
- // Should NOT call the API when worktree requirement fails
302
- expect(mockApiClient.updateTask).not.toHaveBeenCalled();
303
- });
304
-
305
- it('should return worktree_example in error response', async () => {
306
- const ctx = createMockContext();
307
- const result = await updateTask(
308
- {
309
- task_id: '123e4567-e89b-12d3-a456-426614174000',
310
- status: 'in_progress',
311
- },
312
- ctx
313
- );
314
-
315
- expect(result.result).toHaveProperty('worktree_example');
316
- expect(result.result.worktree_example).toHaveProperty('command');
317
- expect(result.result.worktree_example).toHaveProperty('then');
318
- expect(result.result).toHaveProperty('skip_option');
319
- });
320
-
321
- it('should succeed when setting in_progress with git_branch', async () => {
322
- mockApiClient.updateTask.mockResolvedValue({
323
- ok: true,
324
- data: { success: true },
325
- });
326
-
327
- const ctx = createMockContext();
328
- const result = await updateTask(
329
- {
330
- task_id: '123e4567-e89b-12d3-a456-426614174000',
331
- status: 'in_progress',
332
- git_branch: 'feature/my-task',
333
- },
334
- ctx
335
- );
336
-
337
- expect(result.result).toMatchObject({ success: true });
338
- expect(result.result).not.toHaveProperty('error');
339
- expect(mockApiClient.updateTask).toHaveBeenCalled();
340
- });
341
-
342
- it('should succeed when using skip_worktree_requirement without git_branch', async () => {
343
- mockApiClient.updateTask.mockResolvedValue({
344
- ok: true,
345
- data: { success: true },
346
- });
347
-
348
- const ctx = createMockContext();
349
- const result = await updateTask(
350
- {
351
- task_id: '123e4567-e89b-12d3-a456-426614174000',
352
- status: 'in_progress',
353
- skip_worktree_requirement: true,
354
- },
355
- ctx
356
- );
357
-
358
- expect(result.result).toMatchObject({ success: true });
359
- expect(result.result).not.toHaveProperty('error');
360
- expect(mockApiClient.updateTask).toHaveBeenCalled();
361
- });
362
-
363
- it('should not require git_branch when updating status other than in_progress', async () => {
364
- mockApiClient.updateTask.mockResolvedValue({
365
- ok: true,
366
- data: { success: true },
367
- });
368
-
369
- const ctx = createMockContext();
370
- const result = await updateTask(
371
- {
372
- task_id: '123e4567-e89b-12d3-a456-426614174000',
373
- status: 'completed',
374
- },
375
- ctx
376
- );
377
-
378
- expect(result.result).toMatchObject({ success: true });
379
- expect(result.result).not.toHaveProperty('error');
380
- expect(mockApiClient.updateTask).toHaveBeenCalled();
381
- });
382
-
383
- it('should not require git_branch when updating progress only', async () => {
384
- mockApiClient.updateTask.mockResolvedValue({
385
- ok: true,
386
- data: { success: true },
387
- });
388
-
389
- const ctx = createMockContext();
390
- const result = await updateTask(
391
- {
392
- task_id: '123e4567-e89b-12d3-a456-426614174000',
393
- progress_percentage: 50,
394
- progress_note: 'Halfway done',
395
- },
396
- ctx
397
- );
398
-
399
- expect(result.result).toMatchObject({ success: true });
400
- expect(mockApiClient.updateTask).toHaveBeenCalled();
401
- });
402
-
403
- it('should include FIX_ALREADY_EXISTS_GUIDANCE when setting status to in_progress', async () => {
404
- mockApiClient.updateTask.mockResolvedValue({
405
- ok: true,
406
- data: { success: true },
407
- });
408
-
409
- const ctx = createMockContext();
410
- const result = await updateTask(
411
- {
412
- task_id: '123e4567-e89b-12d3-a456-426614174000',
413
- status: 'in_progress',
414
- git_branch: 'feature/my-task',
415
- },
416
- ctx
417
- );
418
-
419
- expect(result.result).toMatchObject({ success: true });
420
- expect(result.result).toHaveProperty('FIX_ALREADY_EXISTS_GUIDANCE');
421
- expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('message');
422
- expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('steps');
423
- expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('rationale');
424
- expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE.steps).toHaveLength(4);
425
- });
426
- });
427
-
428
- // ============================================================================
429
- // completeTask Tests
430
- // ============================================================================
431
-
432
- describe('completeTask', () => {
433
- beforeEach(() => vi.clearAllMocks());
434
-
435
- it('should throw error for missing task_id', async () => {
436
- const ctx = createMockContext();
437
- await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
438
- });
439
-
440
- it('should throw error for invalid task_id', async () => {
441
- const ctx = createMockContext();
442
- await expect(
443
- completeTask({ task_id: 'invalid' }, ctx)
444
- ).rejects.toThrow(ValidationError);
445
- });
446
-
447
- it('should complete task successfully', async () => {
448
- mockApiClient.completeTask.mockResolvedValue({
449
- ok: true,
450
- data: {
451
- success: true,
452
- directive: 'Start next task',
453
- auto_continue: true,
454
- completed_task_id: '123e4567-e89b-12d3-a456-426614174000',
455
- next_task: { id: 'task-2', title: 'Next task' },
456
- },
457
- });
458
-
459
- const ctx = createMockContext();
460
- const result = await completeTask(
461
- { task_id: '123e4567-e89b-12d3-a456-426614174000' },
462
- ctx
463
- );
464
-
465
- expect(result.result).toMatchObject({
466
- success: true,
467
- auto_continue: true,
468
- next_task: expect.any(Object),
469
- });
470
- });
471
-
472
- it('should include summary in API call', async () => {
473
- mockApiClient.completeTask.mockResolvedValue({
474
- ok: true,
475
- data: { success: true },
476
- });
477
-
478
- const ctx = createMockContext();
479
- await completeTask(
480
- {
481
- task_id: '123e4567-e89b-12d3-a456-426614174000',
482
- summary: 'Completed the feature',
483
- },
484
- ctx
485
- );
486
-
487
- expect(mockApiClient.completeTask).toHaveBeenCalledWith(
488
- '123e4567-e89b-12d3-a456-426614174000',
489
- expect.objectContaining({ summary: 'Completed the feature' })
490
- );
491
- });
492
-
493
- it('should return error when API returns error', async () => {
494
- mockApiClient.completeTask.mockResolvedValue({
495
- ok: false,
496
- error: 'Task not found',
497
- });
498
-
499
- const ctx = createMockContext();
500
- const result = await completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
501
-
502
- expect(result.isError).toBe(true);
503
- expect(result.result).toMatchObject({
504
- error: 'Task not found',
505
- });
506
- });
507
- });
508
-
509
- // ============================================================================
510
- // deleteTask Tests
511
- // ============================================================================
512
-
513
- describe('deleteTask', () => {
514
- beforeEach(() => vi.clearAllMocks());
515
-
516
- it('should throw error for missing task_id', async () => {
517
- const ctx = createMockContext();
518
- await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
519
- });
520
-
521
- it('should throw error for invalid task_id', async () => {
522
- const ctx = createMockContext();
523
- await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
524
- });
525
-
526
- it('should delete task successfully', async () => {
527
- mockApiClient.deleteTask.mockResolvedValue({
528
- ok: true,
529
- data: { success: true },
530
- });
531
-
532
- const ctx = createMockContext();
533
- const result = await deleteTask(
534
- { task_id: '123e4567-e89b-12d3-a456-426614174000' },
535
- ctx
536
- );
537
-
538
- expect(result.result).toMatchObject({
539
- success: true,
540
- deleted_id: '123e4567-e89b-12d3-a456-426614174000',
541
- });
542
- });
543
- });
544
-
545
- // ============================================================================
546
- // addTaskReference Tests
547
- // ============================================================================
548
-
549
- describe('addTaskReference', () => {
550
- beforeEach(() => vi.clearAllMocks());
551
-
552
- it('should throw error for missing task_id', async () => {
553
- const ctx = createMockContext();
554
- await expect(
555
- addTaskReference({ url: 'https://example.com' }, ctx)
556
- ).rejects.toThrow(ValidationError);
557
- });
558
-
559
- it('should throw error for missing url', async () => {
560
- const ctx = createMockContext();
561
- await expect(
562
- addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
563
- ).rejects.toThrow(ValidationError);
564
- });
565
-
566
- it('should add reference successfully', async () => {
567
- mockApiClient.addTaskReference.mockResolvedValue({
568
- ok: true,
569
- data: { reference: { url: 'https://example.com', label: 'Test' } },
570
- });
571
-
572
- const ctx = createMockContext();
573
- const result = await addTaskReference(
574
- {
575
- task_id: '123e4567-e89b-12d3-a456-426614174000',
576
- url: 'https://example.com',
577
- label: 'Test',
578
- },
579
- ctx
580
- );
581
-
582
- expect(result.result).toMatchObject({
583
- success: true,
584
- reference: expect.any(Object),
585
- });
586
- });
587
-
588
- it('should handle duplicate reference error', async () => {
589
- mockApiClient.addTaskReference.mockResolvedValue({
590
- ok: false,
591
- error: 'Reference already exists',
592
- });
593
-
594
- const ctx = createMockContext();
595
- const result = await addTaskReference(
596
- {
597
- task_id: '123e4567-e89b-12d3-a456-426614174000',
598
- url: 'https://example.com',
599
- },
600
- ctx
601
- );
602
-
603
- expect(result.result).toMatchObject({
604
- success: false,
605
- error: expect.stringContaining('already exists'),
606
- });
607
- });
608
- });
609
-
610
- // ============================================================================
611
- // removeTaskReference Tests
612
- // ============================================================================
613
-
614
- describe('removeTaskReference', () => {
615
- beforeEach(() => vi.clearAllMocks());
616
-
617
- it('should throw error for missing task_id', async () => {
618
- const ctx = createMockContext();
619
- await expect(
620
- removeTaskReference({ url: 'https://example.com' }, ctx)
621
- ).rejects.toThrow(ValidationError);
622
- });
623
-
624
- it('should throw error for missing url', async () => {
625
- const ctx = createMockContext();
626
- await expect(
627
- removeTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
628
- ).rejects.toThrow(ValidationError);
629
- });
630
-
631
- it('should remove reference successfully', async () => {
632
- mockApiClient.removeTaskReference.mockResolvedValue({
633
- ok: true,
634
- data: { success: true },
635
- });
636
-
637
- const ctx = createMockContext();
638
- const result = await removeTaskReference(
639
- {
640
- task_id: '123e4567-e89b-12d3-a456-426614174000',
641
- url: 'https://example.com',
642
- },
643
- ctx
644
- );
645
-
646
- expect(result.result).toMatchObject({ success: true });
647
- });
648
-
649
- it('should handle not found error', async () => {
650
- mockApiClient.removeTaskReference.mockResolvedValue({
651
- ok: false,
652
- error: 'Reference not found',
653
- });
654
-
655
- const ctx = createMockContext();
656
- const result = await removeTaskReference(
657
- {
658
- task_id: '123e4567-e89b-12d3-a456-426614174000',
659
- url: 'https://example.com',
660
- },
661
- ctx
662
- );
663
-
664
- expect(result.result).toMatchObject({
665
- success: false,
666
- error: expect.stringContaining('not found'),
667
- });
668
- });
669
- });
670
-
671
- // ============================================================================
672
- // batchUpdateTasks Tests
673
- // ============================================================================
674
-
675
- describe('batchUpdateTasks', () => {
676
- beforeEach(() => vi.clearAllMocks());
677
-
678
- it('should throw error for missing updates array', async () => {
679
- const ctx = createMockContext();
680
- await expect(batchUpdateTasks({}, ctx)).rejects.toThrow(ValidationError);
681
- });
682
-
683
- it('should throw error for empty updates array', async () => {
684
- const ctx = createMockContext();
685
- await expect(batchUpdateTasks({ updates: [] }, ctx)).rejects.toThrow(ValidationError);
686
- });
687
-
688
- it('should throw error for too many updates', async () => {
689
- const ctx = createMockContext();
690
- const updates = Array(51).fill({
691
- task_id: '123e4567-e89b-12d3-a456-426614174000',
692
- });
693
- await expect(batchUpdateTasks({ updates }, ctx)).rejects.toThrow(ValidationError);
694
- });
695
-
696
- it('should batch update tasks successfully', async () => {
697
- mockApiClient.batchUpdateTasks.mockResolvedValue({
698
- ok: true,
699
- data: { success: true, updated_count: 2 },
700
- });
701
-
702
- const ctx = createMockContext();
703
- const result = await batchUpdateTasks(
704
- {
705
- updates: [
706
- { task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'in_progress' },
707
- { task_id: '123e4567-e89b-12d3-a456-426614174001', status: 'completed' },
708
- ],
709
- },
710
- ctx
711
- );
712
-
713
- expect(result.result).toMatchObject({
714
- success: true,
715
- total: 2,
716
- succeeded: 2,
717
- });
718
- });
719
- });
720
-
721
- // ============================================================================
722
- // batchCompleteTasks Tests
723
- // ============================================================================
724
-
725
- describe('batchCompleteTasks', () => {
726
- beforeEach(() => vi.clearAllMocks());
727
-
728
- it('should throw error for missing completions array', async () => {
729
- const ctx = createMockContext();
730
- await expect(batchCompleteTasks({}, ctx)).rejects.toThrow(ValidationError);
731
- });
732
-
733
- it('should throw error for empty completions array', async () => {
734
- const ctx = createMockContext();
735
- await expect(batchCompleteTasks({ completions: [] }, ctx)).rejects.toThrow(ValidationError);
736
- });
737
-
738
- it('should batch complete tasks successfully', async () => {
739
- mockApiClient.batchCompleteTasks.mockResolvedValue({
740
- ok: true,
741
- data: {
742
- success: true,
743
- completed_count: 2,
744
- next_task: { id: 'task-3', title: 'Next' },
745
- },
746
- });
747
-
748
- const ctx = createMockContext();
749
- const result = await batchCompleteTasks(
750
- {
751
- completions: [
752
- { task_id: '123e4567-e89b-12d3-a456-426614174000' },
753
- { task_id: '123e4567-e89b-12d3-a456-426614174001', summary: 'Done' },
754
- ],
755
- },
756
- ctx
757
- );
758
-
759
- expect(result.result).toMatchObject({
760
- success: true,
761
- total: 2,
762
- succeeded: 2,
763
- next_task: expect.any(Object),
764
- });
765
- });
766
- });
767
-
768
- // ============================================================================
769
- // addSubtask Tests
770
- // ============================================================================
771
-
772
- describe('addSubtask', () => {
773
- beforeEach(() => vi.clearAllMocks());
774
-
775
- it('should throw error for missing parent_task_id', async () => {
776
- const ctx = createMockContext();
777
- await expect(addSubtask({ title: 'Subtask' }, ctx)).rejects.toThrow(ValidationError);
778
- });
779
-
780
- it('should throw error for missing title', async () => {
781
- const ctx = createMockContext();
782
- await expect(
783
- addSubtask({ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
784
- ).rejects.toThrow(ValidationError);
785
- });
786
-
787
- it('should add subtask successfully', async () => {
788
- mockApiClient.addSubtask.mockResolvedValue({
789
- ok: true,
790
- data: {
791
- subtask_id: 'subtask-1',
792
- parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
793
- },
794
- });
795
-
796
- const ctx = createMockContext();
797
- const result = await addSubtask(
798
- {
799
- parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
800
- title: 'Subtask 1',
801
- },
802
- ctx
803
- );
804
-
805
- expect(result.result).toMatchObject({
806
- success: true,
807
- subtask_id: 'subtask-1',
808
- });
809
- });
810
-
811
- it('should handle nested subtask error', async () => {
812
- mockApiClient.addSubtask.mockResolvedValue({
813
- ok: false,
814
- error: 'Cannot create subtask of a subtask',
815
- });
816
-
817
- const ctx = createMockContext();
818
- const result = await addSubtask(
819
- {
820
- parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
821
- title: 'Nested subtask',
822
- },
823
- ctx
824
- );
825
-
826
- expect(result.result).toMatchObject({
827
- success: false,
828
- error: 'Cannot create subtask of a subtask',
829
- });
830
- });
831
- });
832
-
833
- // ============================================================================
834
- // getSubtasks Tests
835
- // ============================================================================
836
-
837
- describe('getSubtasks', () => {
838
- beforeEach(() => vi.clearAllMocks());
839
-
840
- it('should throw error for missing parent_task_id', async () => {
841
- const ctx = createMockContext();
842
- await expect(getSubtasks({}, ctx)).rejects.toThrow(ValidationError);
843
- });
844
-
845
- it('should throw error for invalid parent_task_id', async () => {
846
- const ctx = createMockContext();
847
- await expect(
848
- getSubtasks({ parent_task_id: 'invalid' }, ctx)
849
- ).rejects.toThrow(ValidationError);
850
- });
851
-
852
- it('should return subtasks successfully', async () => {
853
- mockApiClient.getSubtasks.mockResolvedValue({
854
- ok: true,
855
- data: {
856
- subtasks: [
857
- { id: 'sub-1', title: 'Subtask 1', status: 'pending' },
858
- { id: 'sub-2', title: 'Subtask 2', status: 'completed' },
859
- ],
860
- stats: { total: 2, completed: 1, progress_percentage: 50 },
861
- },
862
- });
863
-
864
- const ctx = createMockContext();
865
- const result = await getSubtasks(
866
- { parent_task_id: '123e4567-e89b-12d3-a456-426614174000' },
867
- ctx
868
- );
869
-
870
- expect(result.result).toMatchObject({
871
- subtasks: expect.any(Array),
872
- stats: expect.objectContaining({ total: 2 }),
873
- });
874
- });
875
- });
876
-
877
- // ============================================================================
878
- // getValidationApprovedGitInstructions Tests
879
- // ============================================================================
880
-
881
- describe('getValidationApprovedGitInstructions', () => {
882
- it('should return undefined for none workflow', () => {
883
- const result = getValidationApprovedGitInstructions(
884
- { git_workflow: 'none', git_main_branch: 'main' },
885
- 'feature/test'
886
- );
887
- expect(result).toBeUndefined();
888
- });
889
-
890
- it('should return undefined for trunk-based workflow', () => {
891
- const result = getValidationApprovedGitInstructions(
892
- { git_workflow: 'trunk-based', git_main_branch: 'main' },
893
- 'feature/test'
894
- );
895
- expect(result).toBeUndefined();
896
- });
897
-
898
- it('should return undefined when no task branch', () => {
899
- const result = getValidationApprovedGitInstructions(
900
- { git_workflow: 'github-flow', git_main_branch: 'main' },
901
- undefined
902
- );
903
- expect(result).toBeUndefined();
904
- });
905
-
906
- it('should return merge instructions for github-flow', () => {
907
- const result = getValidationApprovedGitInstructions(
908
- { git_workflow: 'github-flow', git_main_branch: 'main' },
909
- 'feature/test'
910
- );
911
-
912
- expect(result).toMatchObject({
913
- target_branch: 'main',
914
- feature_branch: 'feature/test',
915
- steps: expect.any(Array),
916
- cleanup: expect.any(Array),
917
- });
918
- });
919
-
920
- it('should return merge instructions for git-flow with develop', () => {
921
- const result = getValidationApprovedGitInstructions(
922
- { git_workflow: 'git-flow', git_main_branch: 'main', git_develop_branch: 'develop' },
923
- 'feature/test'
924
- );
925
-
926
- expect(result).toMatchObject({
927
- target_branch: 'develop',
928
- feature_branch: 'feature/test',
929
- });
930
- });
931
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ getNextTask,
4
+ addTask,
5
+ updateTask,
6
+ completeTask,
7
+ deleteTask,
8
+ addTaskReference,
9
+ removeTaskReference,
10
+ batchUpdateTasks,
11
+ batchCompleteTasks,
12
+ addSubtask,
13
+ getSubtasks,
14
+ getValidationApprovedGitInstructions,
15
+ } from './tasks.js';
16
+ import { ValidationError } from '../validators.js';
17
+ import { createMockContext } from './__test-utils__.js';
18
+ import { mockApiClient } from './__test-setup__.js';
19
+
20
+ // ============================================================================
21
+ // getNextTask Tests
22
+ // ============================================================================
23
+
24
+ describe('getNextTask', () => {
25
+ beforeEach(() => vi.clearAllMocks());
26
+
27
+ it('should throw error for missing project_id', async () => {
28
+ const ctx = createMockContext();
29
+ await expect(getNextTask({}, ctx)).rejects.toThrow(ValidationError);
30
+ });
31
+
32
+ it('should throw error for invalid project_id', async () => {
33
+ const ctx = createMockContext();
34
+ await expect(getNextTask({ project_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
35
+ });
36
+
37
+ it('should return next task when available', async () => {
38
+ mockApiClient.getNextTask.mockResolvedValue({
39
+ ok: true,
40
+ data: {
41
+ task: { id: 'task-1', title: 'Next task', priority: 1 },
42
+ directive: 'Start working',
43
+ },
44
+ });
45
+
46
+ const ctx = createMockContext();
47
+ const result = await getNextTask(
48
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
49
+ ctx
50
+ );
51
+
52
+ expect(result.result.task).toMatchObject({ id: 'task-1' });
53
+ });
54
+
55
+ it('should return null when no tasks available', async () => {
56
+ mockApiClient.getNextTask.mockResolvedValue({
57
+ ok: true,
58
+ data: { task: null, message: 'No tasks available' },
59
+ });
60
+
61
+ const ctx = createMockContext();
62
+ const result = await getNextTask(
63
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
64
+ ctx
65
+ );
66
+
67
+ expect(result.result.task).toBeNull();
68
+ });
69
+
70
+ it('should pass session_id to API', async () => {
71
+ mockApiClient.getNextTask.mockResolvedValue({
72
+ ok: true,
73
+ data: { task: null },
74
+ });
75
+
76
+ const ctx = createMockContext({ sessionId: 'my-session' });
77
+ await getNextTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
78
+
79
+ expect(mockApiClient.getNextTask).toHaveBeenCalledWith(
80
+ '123e4567-e89b-12d3-a456-426614174000',
81
+ 'my-session'
82
+ );
83
+ });
84
+ });
85
+
86
+ // ============================================================================
87
+ // addTask Tests
88
+ // ============================================================================
89
+
90
+ describe('addTask', () => {
91
+ beforeEach(() => vi.clearAllMocks());
92
+
93
+ it('should throw error for missing project_id', async () => {
94
+ const ctx = createMockContext();
95
+ await expect(addTask({ title: 'Test task' }, ctx)).rejects.toThrow(ValidationError);
96
+ });
97
+
98
+ it('should throw error for invalid project_id', async () => {
99
+ const ctx = createMockContext();
100
+ await expect(
101
+ addTask({ project_id: 'invalid', title: 'Test task' }, ctx)
102
+ ).rejects.toThrow(ValidationError);
103
+ });
104
+
105
+ it('should throw error for missing title', async () => {
106
+ const ctx = createMockContext();
107
+ await expect(
108
+ addTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
109
+ ).rejects.toThrow(ValidationError);
110
+ });
111
+
112
+ it('should throw error for invalid priority', async () => {
113
+ const ctx = createMockContext();
114
+ await expect(
115
+ addTask({
116
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
117
+ title: 'Test',
118
+ priority: 10,
119
+ }, ctx)
120
+ ).rejects.toThrow(ValidationError);
121
+ });
122
+
123
+ it('should add task successfully', async () => {
124
+ mockApiClient.createTask.mockResolvedValue({
125
+ ok: true,
126
+ data: { task_id: 'new-task-id', title: 'Test task' },
127
+ });
128
+
129
+ const ctx = createMockContext();
130
+ const result = await addTask(
131
+ {
132
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
133
+ title: 'Test task',
134
+ },
135
+ ctx
136
+ );
137
+
138
+ expect(result.result).toMatchObject({
139
+ success: true,
140
+ task_id: 'new-task-id',
141
+ });
142
+ });
143
+
144
+ it('should include optional fields in API call', async () => {
145
+ mockApiClient.createTask.mockResolvedValue({
146
+ ok: true,
147
+ data: { task_id: 'new-task-id' },
148
+ });
149
+
150
+ const ctx = createMockContext();
151
+ await addTask(
152
+ {
153
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
154
+ title: 'Test task',
155
+ description: 'A description',
156
+ priority: 2,
157
+ estimated_minutes: 30,
158
+ blocking: true,
159
+ },
160
+ ctx
161
+ );
162
+
163
+ expect(mockApiClient.createTask).toHaveBeenCalledWith(
164
+ '123e4567-e89b-12d3-a456-426614174000',
165
+ expect.objectContaining({
166
+ title: 'Test task',
167
+ description: 'A description',
168
+ priority: 2,
169
+ estimated_minutes: 30,
170
+ blocking: true,
171
+ })
172
+ );
173
+ });
174
+ });
175
+
176
+ // ============================================================================
177
+ // updateTask Tests
178
+ // ============================================================================
179
+
180
+ describe('updateTask', () => {
181
+ beforeEach(() => vi.clearAllMocks());
182
+
183
+ it('should throw error for missing task_id', async () => {
184
+ const ctx = createMockContext();
185
+ await expect(updateTask({ status: 'in_progress' }, ctx)).rejects.toThrow(ValidationError);
186
+ });
187
+
188
+ it('should throw error for invalid task_id', async () => {
189
+ const ctx = createMockContext();
190
+ await expect(
191
+ updateTask({ task_id: 'invalid', status: 'in_progress' }, ctx)
192
+ ).rejects.toThrow(ValidationError);
193
+ });
194
+
195
+ it('should throw error for invalid status', async () => {
196
+ const ctx = createMockContext();
197
+ await expect(
198
+ updateTask({
199
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
200
+ status: 'invalid',
201
+ }, ctx)
202
+ ).rejects.toThrow(ValidationError);
203
+ });
204
+
205
+ it('should throw error for invalid priority', async () => {
206
+ const ctx = createMockContext();
207
+ await expect(
208
+ updateTask({
209
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
210
+ priority: 0,
211
+ }, ctx)
212
+ ).rejects.toThrow(ValidationError);
213
+ });
214
+
215
+ it('should throw error for invalid progress_percentage', async () => {
216
+ const ctx = createMockContext();
217
+ await expect(
218
+ updateTask({
219
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
220
+ progress_percentage: 150,
221
+ }, ctx)
222
+ ).rejects.toThrow(ValidationError);
223
+ });
224
+
225
+ it('should update task successfully', async () => {
226
+ mockApiClient.updateTask.mockResolvedValue({
227
+ ok: true,
228
+ data: { success: true },
229
+ });
230
+
231
+ const ctx = createMockContext();
232
+ const result = await updateTask(
233
+ {
234
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
235
+ status: 'in_progress',
236
+ git_branch: 'feature/test-branch',
237
+ },
238
+ ctx
239
+ );
240
+
241
+ expect(result.result).toMatchObject({ success: true });
242
+ });
243
+
244
+ it('should handle agent_task_limit error', async () => {
245
+ mockApiClient.updateTask.mockResolvedValue({
246
+ ok: false,
247
+ error: 'agent_task_limit: Agent already has a task in progress',
248
+ });
249
+
250
+ const ctx = createMockContext();
251
+ const result = await updateTask(
252
+ {
253
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
254
+ status: 'in_progress',
255
+ git_branch: 'feature/test-branch',
256
+ },
257
+ ctx
258
+ );
259
+
260
+ expect(result.result).toMatchObject({
261
+ error: 'agent_task_limit',
262
+ });
263
+ });
264
+
265
+ it('should handle task_claimed error', async () => {
266
+ mockApiClient.updateTask.mockResolvedValue({
267
+ ok: false,
268
+ error: 'task_claimed: Task is being worked on by another agent',
269
+ });
270
+
271
+ const ctx = createMockContext();
272
+ const result = await updateTask(
273
+ {
274
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
275
+ status: 'in_progress',
276
+ git_branch: 'feature/test-branch',
277
+ },
278
+ ctx
279
+ );
280
+
281
+ expect(result.result).toMatchObject({
282
+ error: 'task_already_claimed',
283
+ });
284
+ });
285
+
286
+ it('should return error when setting in_progress without git_branch', async () => {
287
+ const ctx = createMockContext();
288
+ const result = await updateTask(
289
+ {
290
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
291
+ status: 'in_progress',
292
+ },
293
+ ctx
294
+ );
295
+
296
+ expect(result.result).toMatchObject({
297
+ error: 'worktree_required',
298
+ message: expect.stringContaining('git_branch is required'),
299
+ hint: expect.stringContaining('Create a worktree'),
300
+ });
301
+ // Should NOT call the API when worktree requirement fails
302
+ expect(mockApiClient.updateTask).not.toHaveBeenCalled();
303
+ });
304
+
305
+ it('should return worktree_example in error response', async () => {
306
+ const ctx = createMockContext();
307
+ const result = await updateTask(
308
+ {
309
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
310
+ status: 'in_progress',
311
+ },
312
+ ctx
313
+ );
314
+
315
+ expect(result.result).toHaveProperty('worktree_example');
316
+ expect(result.result.worktree_example).toHaveProperty('command');
317
+ expect(result.result.worktree_example).toHaveProperty('then');
318
+ expect(result.result).toHaveProperty('skip_option');
319
+ });
320
+
321
+ it('should succeed when setting in_progress with git_branch', async () => {
322
+ mockApiClient.updateTask.mockResolvedValue({
323
+ ok: true,
324
+ data: { success: true },
325
+ });
326
+
327
+ const ctx = createMockContext();
328
+ const result = await updateTask(
329
+ {
330
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
331
+ status: 'in_progress',
332
+ git_branch: 'feature/my-task',
333
+ },
334
+ ctx
335
+ );
336
+
337
+ expect(result.result).toMatchObject({ success: true });
338
+ expect(result.result).not.toHaveProperty('error');
339
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
340
+ });
341
+
342
+ it('should succeed when using skip_worktree_requirement without git_branch', async () => {
343
+ mockApiClient.updateTask.mockResolvedValue({
344
+ ok: true,
345
+ data: { success: true },
346
+ });
347
+
348
+ const ctx = createMockContext();
349
+ const result = await updateTask(
350
+ {
351
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
352
+ status: 'in_progress',
353
+ skip_worktree_requirement: true,
354
+ },
355
+ ctx
356
+ );
357
+
358
+ expect(result.result).toMatchObject({ success: true });
359
+ expect(result.result).not.toHaveProperty('error');
360
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
361
+ });
362
+
363
+ it('should not require git_branch when updating status other than in_progress', async () => {
364
+ mockApiClient.updateTask.mockResolvedValue({
365
+ ok: true,
366
+ data: { success: true },
367
+ });
368
+
369
+ const ctx = createMockContext();
370
+ const result = await updateTask(
371
+ {
372
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
373
+ status: 'completed',
374
+ },
375
+ ctx
376
+ );
377
+
378
+ expect(result.result).toMatchObject({ success: true });
379
+ expect(result.result).not.toHaveProperty('error');
380
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
381
+ });
382
+
383
+ it('should not require git_branch when updating progress only', async () => {
384
+ mockApiClient.updateTask.mockResolvedValue({
385
+ ok: true,
386
+ data: { success: true },
387
+ });
388
+
389
+ const ctx = createMockContext();
390
+ const result = await updateTask(
391
+ {
392
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
393
+ progress_percentage: 50,
394
+ progress_note: 'Halfway done',
395
+ },
396
+ ctx
397
+ );
398
+
399
+ expect(result.result).toMatchObject({ success: true });
400
+ expect(mockApiClient.updateTask).toHaveBeenCalled();
401
+ });
402
+
403
+ it('should include FIX_ALREADY_EXISTS_GUIDANCE when setting status to in_progress', async () => {
404
+ mockApiClient.updateTask.mockResolvedValue({
405
+ ok: true,
406
+ data: { success: true },
407
+ });
408
+
409
+ const ctx = createMockContext();
410
+ const result = await updateTask(
411
+ {
412
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
413
+ status: 'in_progress',
414
+ git_branch: 'feature/my-task',
415
+ },
416
+ ctx
417
+ );
418
+
419
+ expect(result.result).toMatchObject({ success: true });
420
+ expect(result.result).toHaveProperty('FIX_ALREADY_EXISTS_GUIDANCE');
421
+ expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('message');
422
+ expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('steps');
423
+ expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE).toHaveProperty('rationale');
424
+ expect(result.result.FIX_ALREADY_EXISTS_GUIDANCE.steps).toHaveLength(4);
425
+ });
426
+ });
427
+
428
+ // ============================================================================
429
+ // completeTask Tests
430
+ // ============================================================================
431
+
432
+ describe('completeTask', () => {
433
+ beforeEach(() => vi.clearAllMocks());
434
+
435
+ it('should throw error for missing task_id', async () => {
436
+ const ctx = createMockContext();
437
+ await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
438
+ });
439
+
440
+ it('should throw error for invalid task_id', async () => {
441
+ const ctx = createMockContext();
442
+ await expect(
443
+ completeTask({ task_id: 'invalid' }, ctx)
444
+ ).rejects.toThrow(ValidationError);
445
+ });
446
+
447
+ it('should complete task successfully', async () => {
448
+ mockApiClient.completeTask.mockResolvedValue({
449
+ ok: true,
450
+ data: {
451
+ success: true,
452
+ directive: 'Start next task',
453
+ auto_continue: true,
454
+ completed_task_id: '123e4567-e89b-12d3-a456-426614174000',
455
+ next_task: { id: 'task-2', title: 'Next task' },
456
+ },
457
+ });
458
+
459
+ const ctx = createMockContext();
460
+ const result = await completeTask(
461
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
462
+ ctx
463
+ );
464
+
465
+ expect(result.result).toMatchObject({
466
+ success: true,
467
+ auto_continue: true,
468
+ next_task: expect.any(Object),
469
+ });
470
+ });
471
+
472
+ it('should include summary in API call', async () => {
473
+ mockApiClient.completeTask.mockResolvedValue({
474
+ ok: true,
475
+ data: { success: true },
476
+ });
477
+
478
+ const ctx = createMockContext();
479
+ await completeTask(
480
+ {
481
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
482
+ summary: 'Completed the feature',
483
+ },
484
+ ctx
485
+ );
486
+
487
+ expect(mockApiClient.completeTask).toHaveBeenCalledWith(
488
+ '123e4567-e89b-12d3-a456-426614174000',
489
+ expect.objectContaining({ summary: 'Completed the feature' })
490
+ );
491
+ });
492
+
493
+ it('should return error when API returns error', async () => {
494
+ mockApiClient.completeTask.mockResolvedValue({
495
+ ok: false,
496
+ error: 'Task not found',
497
+ });
498
+
499
+ const ctx = createMockContext();
500
+ const result = await completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
501
+
502
+ expect(result.isError).toBe(true);
503
+ expect(result.result).toMatchObject({
504
+ error: 'Task not found',
505
+ });
506
+ });
507
+ });
508
+
509
+ // ============================================================================
510
+ // deleteTask Tests
511
+ // ============================================================================
512
+
513
+ describe('deleteTask', () => {
514
+ beforeEach(() => vi.clearAllMocks());
515
+
516
+ it('should throw error for missing task_id', async () => {
517
+ const ctx = createMockContext();
518
+ await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
519
+ });
520
+
521
+ it('should throw error for invalid task_id', async () => {
522
+ const ctx = createMockContext();
523
+ await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
524
+ });
525
+
526
+ it('should delete task successfully', async () => {
527
+ mockApiClient.deleteTask.mockResolvedValue({
528
+ ok: true,
529
+ data: { success: true },
530
+ });
531
+
532
+ const ctx = createMockContext();
533
+ const result = await deleteTask(
534
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
535
+ ctx
536
+ );
537
+
538
+ expect(result.result).toMatchObject({
539
+ success: true,
540
+ deleted_id: '123e4567-e89b-12d3-a456-426614174000',
541
+ });
542
+ });
543
+ });
544
+
545
+ // ============================================================================
546
+ // addTaskReference Tests
547
+ // ============================================================================
548
+
549
+ describe('addTaskReference', () => {
550
+ beforeEach(() => vi.clearAllMocks());
551
+
552
+ it('should throw error for missing task_id', async () => {
553
+ const ctx = createMockContext();
554
+ await expect(
555
+ addTaskReference({ url: 'https://example.com' }, ctx)
556
+ ).rejects.toThrow(ValidationError);
557
+ });
558
+
559
+ it('should throw error for missing url', async () => {
560
+ const ctx = createMockContext();
561
+ await expect(
562
+ addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
563
+ ).rejects.toThrow(ValidationError);
564
+ });
565
+
566
+ it('should add reference successfully', async () => {
567
+ mockApiClient.addTaskReference.mockResolvedValue({
568
+ ok: true,
569
+ data: { reference: { url: 'https://example.com', label: 'Test' } },
570
+ });
571
+
572
+ const ctx = createMockContext();
573
+ const result = await addTaskReference(
574
+ {
575
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
576
+ url: 'https://example.com',
577
+ label: 'Test',
578
+ },
579
+ ctx
580
+ );
581
+
582
+ expect(result.result).toMatchObject({
583
+ success: true,
584
+ reference: expect.any(Object),
585
+ });
586
+ });
587
+
588
+ it('should handle duplicate reference error', async () => {
589
+ mockApiClient.addTaskReference.mockResolvedValue({
590
+ ok: false,
591
+ error: 'Reference already exists',
592
+ });
593
+
594
+ const ctx = createMockContext();
595
+ const result = await addTaskReference(
596
+ {
597
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
598
+ url: 'https://example.com',
599
+ },
600
+ ctx
601
+ );
602
+
603
+ expect(result.result).toMatchObject({
604
+ success: false,
605
+ error: expect.stringContaining('already exists'),
606
+ });
607
+ });
608
+ });
609
+
610
+ // ============================================================================
611
+ // removeTaskReference Tests
612
+ // ============================================================================
613
+
614
+ describe('removeTaskReference', () => {
615
+ beforeEach(() => vi.clearAllMocks());
616
+
617
+ it('should throw error for missing task_id', async () => {
618
+ const ctx = createMockContext();
619
+ await expect(
620
+ removeTaskReference({ url: 'https://example.com' }, ctx)
621
+ ).rejects.toThrow(ValidationError);
622
+ });
623
+
624
+ it('should throw error for missing url', async () => {
625
+ const ctx = createMockContext();
626
+ await expect(
627
+ removeTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
628
+ ).rejects.toThrow(ValidationError);
629
+ });
630
+
631
+ it('should remove reference successfully', async () => {
632
+ mockApiClient.removeTaskReference.mockResolvedValue({
633
+ ok: true,
634
+ data: { success: true },
635
+ });
636
+
637
+ const ctx = createMockContext();
638
+ const result = await removeTaskReference(
639
+ {
640
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
641
+ url: 'https://example.com',
642
+ },
643
+ ctx
644
+ );
645
+
646
+ expect(result.result).toMatchObject({ success: true });
647
+ });
648
+
649
+ it('should handle not found error', async () => {
650
+ mockApiClient.removeTaskReference.mockResolvedValue({
651
+ ok: false,
652
+ error: 'Reference not found',
653
+ });
654
+
655
+ const ctx = createMockContext();
656
+ const result = await removeTaskReference(
657
+ {
658
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
659
+ url: 'https://example.com',
660
+ },
661
+ ctx
662
+ );
663
+
664
+ expect(result.result).toMatchObject({
665
+ success: false,
666
+ error: expect.stringContaining('not found'),
667
+ });
668
+ });
669
+ });
670
+
671
+ // ============================================================================
672
+ // batchUpdateTasks Tests
673
+ // ============================================================================
674
+
675
+ describe('batchUpdateTasks', () => {
676
+ beforeEach(() => vi.clearAllMocks());
677
+
678
+ it('should throw error for missing updates array', async () => {
679
+ const ctx = createMockContext();
680
+ await expect(batchUpdateTasks({}, ctx)).rejects.toThrow(ValidationError);
681
+ });
682
+
683
+ it('should throw error for empty updates array', async () => {
684
+ const ctx = createMockContext();
685
+ await expect(batchUpdateTasks({ updates: [] }, ctx)).rejects.toThrow(ValidationError);
686
+ });
687
+
688
+ it('should throw error for too many updates', async () => {
689
+ const ctx = createMockContext();
690
+ const updates = Array(51).fill({
691
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
692
+ });
693
+ await expect(batchUpdateTasks({ updates }, ctx)).rejects.toThrow(ValidationError);
694
+ });
695
+
696
+ it('should batch update tasks successfully', async () => {
697
+ mockApiClient.batchUpdateTasks.mockResolvedValue({
698
+ ok: true,
699
+ data: { success: true, updated_count: 2 },
700
+ });
701
+
702
+ const ctx = createMockContext();
703
+ const result = await batchUpdateTasks(
704
+ {
705
+ updates: [
706
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'in_progress' },
707
+ { task_id: '123e4567-e89b-12d3-a456-426614174001', status: 'completed' },
708
+ ],
709
+ },
710
+ ctx
711
+ );
712
+
713
+ expect(result.result).toMatchObject({
714
+ success: true,
715
+ total: 2,
716
+ succeeded: 2,
717
+ });
718
+ });
719
+ });
720
+
721
+ // ============================================================================
722
+ // batchCompleteTasks Tests
723
+ // ============================================================================
724
+
725
+ describe('batchCompleteTasks', () => {
726
+ beforeEach(() => vi.clearAllMocks());
727
+
728
+ it('should throw error for missing completions array', async () => {
729
+ const ctx = createMockContext();
730
+ await expect(batchCompleteTasks({}, ctx)).rejects.toThrow(ValidationError);
731
+ });
732
+
733
+ it('should throw error for empty completions array', async () => {
734
+ const ctx = createMockContext();
735
+ await expect(batchCompleteTasks({ completions: [] }, ctx)).rejects.toThrow(ValidationError);
736
+ });
737
+
738
+ it('should batch complete tasks successfully', async () => {
739
+ mockApiClient.batchCompleteTasks.mockResolvedValue({
740
+ ok: true,
741
+ data: {
742
+ success: true,
743
+ completed_count: 2,
744
+ next_task: { id: 'task-3', title: 'Next' },
745
+ },
746
+ });
747
+
748
+ const ctx = createMockContext();
749
+ const result = await batchCompleteTasks(
750
+ {
751
+ completions: [
752
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
753
+ { task_id: '123e4567-e89b-12d3-a456-426614174001', summary: 'Done' },
754
+ ],
755
+ },
756
+ ctx
757
+ );
758
+
759
+ expect(result.result).toMatchObject({
760
+ success: true,
761
+ total: 2,
762
+ succeeded: 2,
763
+ next_task: expect.any(Object),
764
+ });
765
+ });
766
+ });
767
+
768
+ // ============================================================================
769
+ // addSubtask Tests
770
+ // ============================================================================
771
+
772
+ describe('addSubtask', () => {
773
+ beforeEach(() => vi.clearAllMocks());
774
+
775
+ it('should throw error for missing parent_task_id', async () => {
776
+ const ctx = createMockContext();
777
+ await expect(addSubtask({ title: 'Subtask' }, ctx)).rejects.toThrow(ValidationError);
778
+ });
779
+
780
+ it('should throw error for missing title', async () => {
781
+ const ctx = createMockContext();
782
+ await expect(
783
+ addSubtask({ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
784
+ ).rejects.toThrow(ValidationError);
785
+ });
786
+
787
+ it('should add subtask successfully', async () => {
788
+ mockApiClient.addSubtask.mockResolvedValue({
789
+ ok: true,
790
+ data: {
791
+ subtask_id: 'subtask-1',
792
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
793
+ },
794
+ });
795
+
796
+ const ctx = createMockContext();
797
+ const result = await addSubtask(
798
+ {
799
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
800
+ title: 'Subtask 1',
801
+ },
802
+ ctx
803
+ );
804
+
805
+ expect(result.result).toMatchObject({
806
+ success: true,
807
+ subtask_id: 'subtask-1',
808
+ });
809
+ });
810
+
811
+ it('should handle nested subtask error', async () => {
812
+ mockApiClient.addSubtask.mockResolvedValue({
813
+ ok: false,
814
+ error: 'Cannot create subtask of a subtask',
815
+ });
816
+
817
+ const ctx = createMockContext();
818
+ const result = await addSubtask(
819
+ {
820
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
821
+ title: 'Nested subtask',
822
+ },
823
+ ctx
824
+ );
825
+
826
+ expect(result.result).toMatchObject({
827
+ success: false,
828
+ error: 'Cannot create subtask of a subtask',
829
+ });
830
+ });
831
+ });
832
+
833
+ // ============================================================================
834
+ // getSubtasks Tests
835
+ // ============================================================================
836
+
837
+ describe('getSubtasks', () => {
838
+ beforeEach(() => vi.clearAllMocks());
839
+
840
+ it('should throw error for missing parent_task_id', async () => {
841
+ const ctx = createMockContext();
842
+ await expect(getSubtasks({}, ctx)).rejects.toThrow(ValidationError);
843
+ });
844
+
845
+ it('should throw error for invalid parent_task_id', async () => {
846
+ const ctx = createMockContext();
847
+ await expect(
848
+ getSubtasks({ parent_task_id: 'invalid' }, ctx)
849
+ ).rejects.toThrow(ValidationError);
850
+ });
851
+
852
+ it('should return subtasks successfully', async () => {
853
+ mockApiClient.getSubtasks.mockResolvedValue({
854
+ ok: true,
855
+ data: {
856
+ subtasks: [
857
+ { id: 'sub-1', title: 'Subtask 1', status: 'pending' },
858
+ { id: 'sub-2', title: 'Subtask 2', status: 'completed' },
859
+ ],
860
+ stats: { total: 2, completed: 1, progress_percentage: 50 },
861
+ },
862
+ });
863
+
864
+ const ctx = createMockContext();
865
+ const result = await getSubtasks(
866
+ { parent_task_id: '123e4567-e89b-12d3-a456-426614174000' },
867
+ ctx
868
+ );
869
+
870
+ expect(result.result).toMatchObject({
871
+ subtasks: expect.any(Array),
872
+ stats: expect.objectContaining({ total: 2 }),
873
+ });
874
+ });
875
+ });
876
+
877
+ // ============================================================================
878
+ // getValidationApprovedGitInstructions Tests
879
+ // ============================================================================
880
+
881
+ describe('getValidationApprovedGitInstructions', () => {
882
+ it('should return undefined for none workflow', () => {
883
+ const result = getValidationApprovedGitInstructions(
884
+ { git_workflow: 'none', git_main_branch: 'main' },
885
+ 'feature/test'
886
+ );
887
+ expect(result).toBeUndefined();
888
+ });
889
+
890
+ it('should return undefined for trunk-based workflow', () => {
891
+ const result = getValidationApprovedGitInstructions(
892
+ { git_workflow: 'trunk-based', git_main_branch: 'main' },
893
+ 'feature/test'
894
+ );
895
+ expect(result).toBeUndefined();
896
+ });
897
+
898
+ it('should return undefined when no task branch', () => {
899
+ const result = getValidationApprovedGitInstructions(
900
+ { git_workflow: 'github-flow', git_main_branch: 'main' },
901
+ undefined
902
+ );
903
+ expect(result).toBeUndefined();
904
+ });
905
+
906
+ it('should return merge instructions for github-flow', () => {
907
+ const result = getValidationApprovedGitInstructions(
908
+ { git_workflow: 'github-flow', git_main_branch: 'main' },
909
+ 'feature/test'
910
+ );
911
+
912
+ expect(result).toMatchObject({
913
+ target_branch: 'main',
914
+ feature_branch: 'feature/test',
915
+ steps: expect.any(Array),
916
+ cleanup: expect.any(Array),
917
+ });
918
+ });
919
+
920
+ it('should return merge instructions for git-flow with develop', () => {
921
+ const result = getValidationApprovedGitInstructions(
922
+ { git_workflow: 'git-flow', git_main_branch: 'main', git_develop_branch: 'develop' },
923
+ 'feature/test'
924
+ );
925
+
926
+ expect(result).toMatchObject({
927
+ target_branch: 'develop',
928
+ feature_branch: 'feature/test',
929
+ });
930
+ });
931
+ });