@vibescope/mcp-server 0.0.1 → 0.2.0

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