@vibescope/mcp-server 0.0.1 → 0.1.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 (170) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1114 -0
  3. package/dist/api-client.js +698 -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 +106 -476
  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 +112 -828
  16. package/dist/handlers/discovery.js +31 -0
  17. package/dist/handlers/fallback.d.ts +2 -0
  18. package/dist/handlers/fallback.js +39 -134
  19. package/dist/handlers/findings.js +43 -67
  20. package/dist/handlers/git-issues.d.ts +9 -13
  21. package/dist/handlers/git-issues.js +80 -225
  22. package/dist/handlers/ideas.d.ts +3 -0
  23. package/dist/handlers/ideas.js +53 -134
  24. package/dist/handlers/index.d.ts +2 -0
  25. package/dist/handlers/index.js +6 -0
  26. package/dist/handlers/milestones.d.ts +2 -0
  27. package/dist/handlers/milestones.js +51 -98
  28. package/dist/handlers/organizations.js +79 -275
  29. package/dist/handlers/progress.d.ts +2 -0
  30. package/dist/handlers/progress.js +25 -123
  31. package/dist/handlers/project.js +42 -221
  32. package/dist/handlers/requests.d.ts +2 -0
  33. package/dist/handlers/requests.js +23 -83
  34. package/dist/handlers/session.js +99 -585
  35. package/dist/handlers/sprints.d.ts +32 -0
  36. package/dist/handlers/sprints.js +274 -0
  37. package/dist/handlers/tasks.d.ts +7 -10
  38. package/dist/handlers/tasks.js +230 -900
  39. package/dist/handlers/tool-docs.d.ts +8 -0
  40. package/dist/handlers/tool-docs.js +657 -0
  41. package/dist/handlers/types.d.ts +11 -3
  42. package/dist/handlers/validation.d.ts +1 -1
  43. package/dist/handlers/validation.js +26 -153
  44. package/dist/index.js +473 -160
  45. package/dist/knowledge.js +106 -9
  46. package/dist/tools.js +4 -0
  47. package/dist/validators.d.ts +21 -0
  48. package/dist/validators.js +91 -0
  49. package/package.json +2 -3
  50. package/src/api-client.ts +1752 -0
  51. package/src/cli.test.ts +128 -302
  52. package/src/cli.ts +41 -285
  53. package/src/handlers/__test-setup__.ts +210 -0
  54. package/src/handlers/__test-utils__.ts +4 -134
  55. package/src/handlers/blockers.test.ts +114 -124
  56. package/src/handlers/blockers.ts +68 -70
  57. package/src/handlers/bodies-of-work.test.ts +236 -831
  58. package/src/handlers/bodies-of-work.ts +194 -525
  59. package/src/handlers/cost.test.ts +149 -113
  60. package/src/handlers/cost.ts +44 -132
  61. package/src/handlers/decisions.test.ts +111 -209
  62. package/src/handlers/decisions.ts +35 -27
  63. package/src/handlers/deployment.test.ts +193 -239
  64. package/src/handlers/deployment.ts +140 -895
  65. package/src/handlers/discovery.test.ts +20 -67
  66. package/src/handlers/discovery.ts +32 -0
  67. package/src/handlers/fallback.test.ts +128 -361
  68. package/src/handlers/fallback.ts +62 -148
  69. package/src/handlers/findings.test.ts +127 -345
  70. package/src/handlers/findings.ts +49 -66
  71. package/src/handlers/git-issues.test.ts +623 -0
  72. package/src/handlers/git-issues.ts +174 -0
  73. package/src/handlers/ideas.test.ts +229 -343
  74. package/src/handlers/ideas.ts +69 -143
  75. package/src/handlers/index.ts +6 -0
  76. package/src/handlers/milestones.test.ts +167 -281
  77. package/src/handlers/milestones.ts +54 -93
  78. package/src/handlers/organizations.test.ts +275 -467
  79. package/src/handlers/organizations.ts +84 -294
  80. package/src/handlers/progress.test.ts +112 -218
  81. package/src/handlers/progress.ts +29 -142
  82. package/src/handlers/project.test.ts +203 -226
  83. package/src/handlers/project.ts +48 -238
  84. package/src/handlers/requests.test.ts +74 -342
  85. package/src/handlers/requests.ts +25 -83
  86. package/src/handlers/session.test.ts +241 -206
  87. package/src/handlers/session.ts +110 -657
  88. package/src/handlers/sprints.test.ts +711 -0
  89. package/src/handlers/sprints.ts +497 -0
  90. package/src/handlers/tasks.test.ts +608 -353
  91. package/src/handlers/tasks.ts +248 -1025
  92. package/src/handlers/types.ts +12 -4
  93. package/src/handlers/validation.test.ts +189 -572
  94. package/src/handlers/validation.ts +29 -166
  95. package/src/index.ts +473 -184
  96. package/src/knowledge.ts +107 -9
  97. package/src/tools.ts +2506 -0
  98. package/src/validators.test.ts +223 -223
  99. package/src/validators.ts +127 -0
  100. package/tsconfig.json +1 -1
  101. package/vitest.config.ts +14 -13
  102. package/dist/cli.test.d.ts +0 -1
  103. package/dist/cli.test.js +0 -367
  104. package/dist/handlers/__test-utils__.d.ts +0 -72
  105. package/dist/handlers/__test-utils__.js +0 -176
  106. package/dist/handlers/checkouts.d.ts +0 -37
  107. package/dist/handlers/checkouts.js +0 -377
  108. package/dist/handlers/knowledge-query.d.ts +0 -22
  109. package/dist/handlers/knowledge-query.js +0 -253
  110. package/dist/handlers/knowledge.d.ts +0 -12
  111. package/dist/handlers/knowledge.js +0 -108
  112. package/dist/handlers/roles.d.ts +0 -30
  113. package/dist/handlers/roles.js +0 -281
  114. package/dist/handlers/tasks.test.d.ts +0 -1
  115. package/dist/handlers/tasks.test.js +0 -431
  116. package/dist/utils.test.d.ts +0 -1
  117. package/dist/utils.test.js +0 -532
  118. package/dist/validators.test.d.ts +0 -1
  119. package/dist/validators.test.js +0 -176
  120. package/src/tmpclaude-0078-cwd +0 -1
  121. package/src/tmpclaude-0ee1-cwd +0 -1
  122. package/src/tmpclaude-2dd5-cwd +0 -1
  123. package/src/tmpclaude-344c-cwd +0 -1
  124. package/src/tmpclaude-3860-cwd +0 -1
  125. package/src/tmpclaude-4b63-cwd +0 -1
  126. package/src/tmpclaude-5c73-cwd +0 -1
  127. package/src/tmpclaude-5ee3-cwd +0 -1
  128. package/src/tmpclaude-6795-cwd +0 -1
  129. package/src/tmpclaude-709e-cwd +0 -1
  130. package/src/tmpclaude-9839-cwd +0 -1
  131. package/src/tmpclaude-d829-cwd +0 -1
  132. package/src/tmpclaude-e072-cwd +0 -1
  133. package/src/tmpclaude-f6ee-cwd +0 -1
  134. package/tmpclaude-0439-cwd +0 -1
  135. package/tmpclaude-132f-cwd +0 -1
  136. package/tmpclaude-15bb-cwd +0 -1
  137. package/tmpclaude-165a-cwd +0 -1
  138. package/tmpclaude-1ba9-cwd +0 -1
  139. package/tmpclaude-21a3-cwd +0 -1
  140. package/tmpclaude-2a38-cwd +0 -1
  141. package/tmpclaude-2adf-cwd +0 -1
  142. package/tmpclaude-2f56-cwd +0 -1
  143. package/tmpclaude-3626-cwd +0 -1
  144. package/tmpclaude-3727-cwd +0 -1
  145. package/tmpclaude-40bc-cwd +0 -1
  146. package/tmpclaude-436f-cwd +0 -1
  147. package/tmpclaude-4783-cwd +0 -1
  148. package/tmpclaude-4b6d-cwd +0 -1
  149. package/tmpclaude-4ba4-cwd +0 -1
  150. package/tmpclaude-51e6-cwd +0 -1
  151. package/tmpclaude-5ecf-cwd +0 -1
  152. package/tmpclaude-6f97-cwd +0 -1
  153. package/tmpclaude-7fb2-cwd +0 -1
  154. package/tmpclaude-825c-cwd +0 -1
  155. package/tmpclaude-8baf-cwd +0 -1
  156. package/tmpclaude-8d9f-cwd +0 -1
  157. package/tmpclaude-975c-cwd +0 -1
  158. package/tmpclaude-9983-cwd +0 -1
  159. package/tmpclaude-a045-cwd +0 -1
  160. package/tmpclaude-ac4a-cwd +0 -1
  161. package/tmpclaude-b593-cwd +0 -1
  162. package/tmpclaude-b891-cwd +0 -1
  163. package/tmpclaude-c032-cwd +0 -1
  164. package/tmpclaude-cf43-cwd +0 -1
  165. package/tmpclaude-d040-cwd +0 -1
  166. package/tmpclaude-dcdd-cwd +0 -1
  167. package/tmpclaude-dcee-cwd +0 -1
  168. package/tmpclaude-e16b-cwd +0 -1
  169. package/tmpclaude-ecd2-cwd +0 -1
  170. 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,375 @@ 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',
313
327
  },
314
328
  ctx
315
329
  );
316
330
 
317
- // Check that insert was called with priority: 3 (default)
318
- expect(supabase.insert).toHaveBeenCalledWith(
319
- expect.objectContaining({ priority: 3 })
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',
347
+ },
348
+ ctx
320
349
  );
350
+
351
+ expect(result.result).toMatchObject({
352
+ error: 'task_claimed',
353
+ });
321
354
  });
322
355
  });
323
356
 
324
357
  // ============================================================================
325
- // deleteTask Tests
358
+ // completeTask Tests
326
359
  // ============================================================================
327
360
 
328
- describe('deleteTask', () => {
329
- beforeEach(() => {
330
- vi.clearAllMocks();
361
+ describe('completeTask', () => {
362
+ beforeEach(() => vi.clearAllMocks());
363
+
364
+ it('should throw error for missing task_id', async () => {
365
+ const ctx = createMockContext();
366
+ await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
331
367
  });
332
368
 
333
- it('should delete a task successfully', async () => {
334
- const supabase = createMockSupabase({
335
- deleteResult: { data: null, error: null },
369
+ it('should throw error for invalid task_id', async () => {
370
+ const ctx = createMockContext();
371
+ await expect(
372
+ completeTask({ task_id: 'invalid' }, ctx)
373
+ ).rejects.toThrow(ValidationError);
374
+ });
375
+
376
+ it('should complete task successfully', async () => {
377
+ mockApiClient.completeTask.mockResolvedValue({
378
+ ok: true,
379
+ data: {
380
+ success: true,
381
+ directive: 'Start next task',
382
+ auto_continue: true,
383
+ completed_task_id: '123e4567-e89b-12d3-a456-426614174000',
384
+ next_task: { id: 'task-2', title: 'Next task' },
385
+ },
336
386
  });
337
- const ctx = createMockContext(supabase);
338
387
 
339
- const result = await deleteTask(
388
+ const ctx = createMockContext();
389
+ const result = await completeTask(
340
390
  { task_id: '123e4567-e89b-12d3-a456-426614174000' },
341
391
  ctx
342
392
  );
343
393
 
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();
394
+ expect(result.result).toMatchObject({
395
+ success: true,
396
+ auto_continue: true,
397
+ next_task: expect.any(Object),
398
+ });
348
399
  });
349
400
 
350
- it('should throw error for missing task_id', async () => {
351
- const supabase = createMockSupabase();
352
- const ctx = createMockContext(supabase);
401
+ it('should include summary in API call', async () => {
402
+ mockApiClient.completeTask.mockResolvedValue({
403
+ ok: true,
404
+ data: { success: true },
405
+ });
353
406
 
354
- await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
407
+ const ctx = createMockContext();
408
+ await completeTask(
409
+ {
410
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
411
+ summary: 'Completed the feature',
412
+ },
413
+ ctx
414
+ );
415
+
416
+ expect(mockApiClient.completeTask).toHaveBeenCalledWith(
417
+ '123e4567-e89b-12d3-a456-426614174000',
418
+ expect.objectContaining({ summary: 'Completed the feature' })
419
+ );
355
420
  });
356
421
 
357
- it('should throw error for invalid task_id UUID', async () => {
358
- const supabase = createMockSupabase();
359
- const ctx = createMockContext(supabase);
422
+ it('should throw error when API returns error', async () => {
423
+ mockApiClient.completeTask.mockResolvedValue({
424
+ ok: false,
425
+ error: 'Task not found',
426
+ });
360
427
 
361
- await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
428
+ const ctx = createMockContext();
429
+ await expect(
430
+ completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
431
+ ).rejects.toThrow('Failed to complete task: Task not found');
362
432
  });
363
433
  });
364
434
 
365
435
  // ============================================================================
366
- // addTaskReference Tests
436
+ // deleteTask Tests
367
437
  // ============================================================================
368
438
 
369
- describe('addTaskReference', () => {
370
- beforeEach(() => {
371
- vi.clearAllMocks();
439
+ describe('deleteTask', () => {
440
+ beforeEach(() => vi.clearAllMocks());
441
+
442
+ it('should throw error for missing task_id', async () => {
443
+ const ctx = createMockContext();
444
+ await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
372
445
  });
373
446
 
374
- it('should add a reference successfully', async () => {
375
- const supabase = createMockSupabase({
376
- selectResult: { data: { references: [] }, error: null },
377
- updateResult: { data: null, error: null },
447
+ it('should throw error for invalid task_id', async () => {
448
+ const ctx = createMockContext();
449
+ await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
450
+ });
451
+
452
+ it('should delete task successfully', async () => {
453
+ mockApiClient.deleteTask.mockResolvedValue({
454
+ ok: true,
455
+ data: { success: true },
378
456
  });
379
- const ctx = createMockContext(supabase);
380
457
 
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
- },
458
+ const ctx = createMockContext();
459
+ const result = await deleteTask(
460
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
387
461
  ctx
388
462
  );
389
463
 
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',
464
+ expect(result.result).toMatchObject({
465
+ success: true,
466
+ deleted_id: '123e4567-e89b-12d3-a456-426614174000',
394
467
  });
395
- expect(result.result.total_references).toBe(1);
396
468
  });
469
+ });
397
470
 
398
- it('should throw error for missing task_id', async () => {
399
- const supabase = createMockSupabase();
400
- const ctx = createMockContext(supabase);
471
+ // ============================================================================
472
+ // addTaskReference Tests
473
+ // ============================================================================
401
474
 
475
+ describe('addTaskReference', () => {
476
+ beforeEach(() => vi.clearAllMocks());
477
+
478
+ it('should throw error for missing task_id', async () => {
479
+ const ctx = createMockContext();
402
480
  await expect(
403
481
  addTaskReference({ url: 'https://example.com' }, ctx)
404
482
  ).rejects.toThrow(ValidationError);
405
483
  });
406
484
 
407
485
  it('should throw error for missing url', async () => {
408
- const supabase = createMockSupabase();
409
- const ctx = createMockContext(supabase);
410
-
486
+ const ctx = createMockContext();
411
487
  await expect(
412
488
  addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
413
489
  ).rejects.toThrow(ValidationError);
414
490
  });
415
491
 
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 },
492
+ it('should add reference successfully', async () => {
493
+ mockApiClient.addTaskReference.mockResolvedValue({
494
+ ok: true,
495
+ data: { reference: { url: 'https://example.com', label: 'Test' } },
420
496
  });
421
- const ctx = createMockContext(supabase);
422
497
 
498
+ const ctx = createMockContext();
423
499
  const result = await addTaskReference(
424
500
  {
425
501
  task_id: '123e4567-e89b-12d3-a456-426614174000',
426
502
  url: 'https://example.com',
503
+ label: 'Test',
427
504
  },
428
505
  ctx
429
506
  );
430
507
 
431
- expect(result.result.success).toBe(false);
432
- expect(result.result.error).toContain('already exists');
508
+ expect(result.result).toMatchObject({
509
+ success: true,
510
+ reference: expect.any(Object),
511
+ });
512
+ });
513
+
514
+ it('should handle duplicate reference error', async () => {
515
+ mockApiClient.addTaskReference.mockResolvedValue({
516
+ ok: false,
517
+ error: 'Reference already exists',
518
+ });
519
+
520
+ const ctx = createMockContext();
521
+ const result = await addTaskReference(
522
+ {
523
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
524
+ url: 'https://example.com',
525
+ },
526
+ ctx
527
+ );
528
+
529
+ expect(result.result).toMatchObject({
530
+ success: false,
531
+ error: expect.stringContaining('already exists'),
532
+ });
433
533
  });
434
534
  });
435
535
 
@@ -438,18 +538,29 @@ describe('addTaskReference', () => {
438
538
  // ============================================================================
439
539
 
440
540
  describe('removeTaskReference', () => {
441
- beforeEach(() => {
442
- vi.clearAllMocks();
541
+ beforeEach(() => vi.clearAllMocks());
542
+
543
+ it('should throw error for missing task_id', async () => {
544
+ const ctx = createMockContext();
545
+ await expect(
546
+ removeTaskReference({ url: 'https://example.com' }, ctx)
547
+ ).rejects.toThrow(ValidationError);
443
548
  });
444
549
 
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 },
550
+ it('should throw error for missing url', async () => {
551
+ const ctx = createMockContext();
552
+ await expect(
553
+ removeTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
554
+ ).rejects.toThrow(ValidationError);
555
+ });
556
+
557
+ it('should remove reference successfully', async () => {
558
+ mockApiClient.removeTaskReference.mockResolvedValue({
559
+ ok: true,
560
+ data: { success: true },
450
561
  });
451
- const ctx = createMockContext(supabase);
452
562
 
563
+ const ctx = createMockContext();
453
564
  const result = await removeTaskReference(
454
565
  {
455
566
  task_id: '123e4567-e89b-12d3-a456-426614174000',
@@ -458,145 +569,289 @@ describe('removeTaskReference', () => {
458
569
  ctx
459
570
  );
460
571
 
461
- expect(result.result.success).toBe(true);
462
- expect(result.result.remaining_references).toBe(0);
572
+ expect(result.result).toMatchObject({ success: true });
463
573
  });
464
574
 
465
- it('should return error when URL not found', async () => {
466
- const supabase = createMockSupabase({
467
- selectResult: { data: { references: [] }, error: null },
575
+ it('should handle not found error', async () => {
576
+ mockApiClient.removeTaskReference.mockResolvedValue({
577
+ ok: false,
578
+ error: 'Reference not found',
468
579
  });
469
- const ctx = createMockContext(supabase);
470
580
 
581
+ const ctx = createMockContext();
471
582
  const result = await removeTaskReference(
472
583
  {
473
584
  task_id: '123e4567-e89b-12d3-a456-426614174000',
474
- url: 'https://nonexistent.com',
585
+ url: 'https://example.com',
475
586
  },
476
587
  ctx
477
588
  );
478
589
 
479
- expect(result.result.success).toBe(false);
480
- expect(result.result.error).toContain('not found');
590
+ expect(result.result).toMatchObject({
591
+ success: false,
592
+ error: expect.stringContaining('not found'),
593
+ });
481
594
  });
482
595
  });
483
596
 
484
597
  // ============================================================================
485
- // getProjectGitConfig Tests
598
+ // batchUpdateTasks Tests
486
599
  // ============================================================================
487
600
 
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
- };
601
+ describe('batchUpdateTasks', () => {
602
+ beforeEach(() => vi.clearAllMocks());
496
603
 
497
- const supabase = createMockSupabase({
498
- selectResult: { data: mockConfig, error: null },
499
- });
604
+ it('should throw error for missing updates array', async () => {
605
+ const ctx = createMockContext();
606
+ await expect(batchUpdateTasks({}, ctx)).rejects.toThrow(ValidationError);
607
+ });
500
608
 
501
- const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
609
+ it('should throw error for empty updates array', async () => {
610
+ const ctx = createMockContext();
611
+ await expect(batchUpdateTasks({ updates: [] }, ctx)).rejects.toThrow(ValidationError);
612
+ });
502
613
 
503
- expect(result).toEqual(mockConfig);
504
- expect(supabase.from).toHaveBeenCalledWith('projects');
614
+ it('should throw error for too many updates', async () => {
615
+ const ctx = createMockContext();
616
+ const updates = Array(51).fill({
617
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
618
+ });
619
+ await expect(batchUpdateTasks({ updates }, ctx)).rejects.toThrow(ValidationError);
505
620
  });
506
621
 
507
- it('should return null when project not found', async () => {
508
- const supabase = createMockSupabase({
509
- selectResult: { data: null, error: { message: 'Not found' } },
622
+ it('should batch update tasks successfully', async () => {
623
+ mockApiClient.batchUpdateTasks.mockResolvedValue({
624
+ ok: true,
625
+ data: { success: true, updated_count: 2 },
510
626
  });
511
627
 
512
- const result = await getProjectGitConfig(supabase, '123e4567-e89b-12d3-a456-426614174000');
628
+ const ctx = createMockContext();
629
+ const result = await batchUpdateTasks(
630
+ {
631
+ updates: [
632
+ { task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'in_progress' },
633
+ { task_id: '123e4567-e89b-12d3-a456-426614174001', status: 'completed' },
634
+ ],
635
+ },
636
+ ctx
637
+ );
513
638
 
514
- expect(result).toBeNull();
639
+ expect(result.result).toMatchObject({
640
+ success: true,
641
+ total: 2,
642
+ succeeded: 2,
643
+ });
515
644
  });
516
645
  });
517
646
 
518
647
  // ============================================================================
519
- // completeTask Tests
648
+ // batchCompleteTasks Tests
520
649
  // ============================================================================
521
650
 
522
- describe('completeTask', () => {
523
- beforeEach(() => {
524
- vi.clearAllMocks();
651
+ describe('batchCompleteTasks', () => {
652
+ beforeEach(() => vi.clearAllMocks());
653
+
654
+ it('should throw error for missing completions array', async () => {
655
+ const ctx = createMockContext();
656
+ await expect(batchCompleteTasks({}, ctx)).rejects.toThrow(ValidationError);
525
657
  });
526
658
 
527
- it('should throw error for missing task_id', async () => {
528
- const supabase = createMockSupabase();
529
- const ctx = createMockContext(supabase);
659
+ it('should throw error for empty completions array', async () => {
660
+ const ctx = createMockContext();
661
+ await expect(batchCompleteTasks({ completions: [] }, ctx)).rejects.toThrow(ValidationError);
662
+ });
530
663
 
531
- await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
664
+ it('should batch complete tasks successfully', async () => {
665
+ mockApiClient.batchCompleteTasks.mockResolvedValue({
666
+ ok: true,
667
+ data: {
668
+ success: true,
669
+ completed_count: 2,
670
+ next_task: { id: 'task-3', title: 'Next' },
671
+ },
672
+ });
673
+
674
+ const ctx = createMockContext();
675
+ const result = await batchCompleteTasks(
676
+ {
677
+ completions: [
678
+ { task_id: '123e4567-e89b-12d3-a456-426614174000' },
679
+ { task_id: '123e4567-e89b-12d3-a456-426614174001', summary: 'Done' },
680
+ ],
681
+ },
682
+ ctx
683
+ );
684
+
685
+ expect(result.result).toMatchObject({
686
+ success: true,
687
+ total: 2,
688
+ succeeded: 2,
689
+ next_task: expect.any(Object),
690
+ });
532
691
  });
692
+ });
533
693
 
534
- it('should throw error for invalid task_id UUID', async () => {
535
- const supabase = createMockSupabase();
536
- const ctx = createMockContext(supabase);
694
+ // ============================================================================
695
+ // addSubtask Tests
696
+ // ============================================================================
537
697
 
538
- await expect(completeTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
698
+ describe('addSubtask', () => {
699
+ beforeEach(() => vi.clearAllMocks());
700
+
701
+ it('should throw error for missing parent_task_id', async () => {
702
+ const ctx = createMockContext();
703
+ await expect(addSubtask({ title: 'Subtask' }, ctx)).rejects.toThrow(ValidationError);
539
704
  });
540
705
 
541
- it('should throw error when task not found', async () => {
542
- const supabase = createMockSupabase({
543
- selectResult: { data: null, error: { message: 'Not found' } },
706
+ it('should throw error for missing title', async () => {
707
+ const ctx = createMockContext();
708
+ await expect(
709
+ addSubtask({ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
710
+ ).rejects.toThrow(ValidationError);
711
+ });
712
+
713
+ it('should add subtask successfully', async () => {
714
+ mockApiClient.addSubtask.mockResolvedValue({
715
+ ok: true,
716
+ data: {
717
+ subtask_id: 'subtask-1',
718
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
719
+ },
544
720
  });
545
- const ctx = createMockContext(supabase);
546
721
 
547
- await expect(
548
- completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
549
- ).rejects.toThrow('Task not found');
722
+ const ctx = createMockContext();
723
+ const result = await addSubtask(
724
+ {
725
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
726
+ title: 'Subtask 1',
727
+ },
728
+ ctx
729
+ );
730
+
731
+ expect(result.result).toMatchObject({
732
+ success: true,
733
+ subtask_id: 'subtask-1',
734
+ });
735
+ });
736
+
737
+ it('should handle nested subtask error', async () => {
738
+ mockApiClient.addSubtask.mockResolvedValue({
739
+ ok: false,
740
+ error: 'Cannot create subtask of a subtask',
741
+ });
742
+
743
+ const ctx = createMockContext();
744
+ const result = await addSubtask(
745
+ {
746
+ parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
747
+ title: 'Nested subtask',
748
+ },
749
+ ctx
750
+ );
751
+
752
+ expect(result.result).toMatchObject({
753
+ success: false,
754
+ error: 'Cannot create subtask of a subtask',
755
+ });
550
756
  });
551
757
  });
552
758
 
553
759
  // ============================================================================
554
- // updateTask Tests
760
+ // getSubtasks Tests
555
761
  // ============================================================================
556
762
 
557
- describe('updateTask', () => {
558
- beforeEach(() => {
559
- vi.clearAllMocks();
763
+ describe('getSubtasks', () => {
764
+ beforeEach(() => vi.clearAllMocks());
765
+
766
+ it('should throw error for missing parent_task_id', async () => {
767
+ const ctx = createMockContext();
768
+ await expect(getSubtasks({}, ctx)).rejects.toThrow(ValidationError);
560
769
  });
561
770
 
562
- it('should throw error for missing task_id', async () => {
563
- const supabase = createMockSupabase();
564
- const ctx = createMockContext(supabase);
771
+ it('should throw error for invalid parent_task_id', async () => {
772
+ const ctx = createMockContext();
773
+ await expect(
774
+ getSubtasks({ parent_task_id: 'invalid' }, ctx)
775
+ ).rejects.toThrow(ValidationError);
776
+ });
565
777
 
566
- await expect(updateTask({}, ctx)).rejects.toThrow(ValidationError);
778
+ it('should return subtasks successfully', async () => {
779
+ mockApiClient.getSubtasks.mockResolvedValue({
780
+ ok: true,
781
+ data: {
782
+ subtasks: [
783
+ { id: 'sub-1', title: 'Subtask 1', status: 'pending' },
784
+ { id: 'sub-2', title: 'Subtask 2', status: 'completed' },
785
+ ],
786
+ stats: { total: 2, completed: 1, progress_percentage: 50 },
787
+ },
788
+ });
789
+
790
+ const ctx = createMockContext();
791
+ const result = await getSubtasks(
792
+ { parent_task_id: '123e4567-e89b-12d3-a456-426614174000' },
793
+ ctx
794
+ );
795
+
796
+ expect(result.result).toMatchObject({
797
+ subtasks: expect.any(Array),
798
+ stats: expect.objectContaining({ total: 2 }),
799
+ });
567
800
  });
801
+ });
568
802
 
569
- it('should throw error for invalid task_id UUID', async () => {
570
- const supabase = createMockSupabase();
571
- const ctx = createMockContext(supabase);
803
+ // ============================================================================
804
+ // getValidationApprovedGitInstructions Tests
805
+ // ============================================================================
572
806
 
573
- await expect(updateTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
807
+ describe('getValidationApprovedGitInstructions', () => {
808
+ it('should return undefined for none workflow', () => {
809
+ const result = getValidationApprovedGitInstructions(
810
+ { git_workflow: 'none', git_main_branch: 'main' },
811
+ 'feature/test'
812
+ );
813
+ expect(result).toBeUndefined();
574
814
  });
575
815
 
576
- it('should throw error for invalid status', async () => {
577
- const supabase = createMockSupabase();
578
- const ctx = createMockContext(supabase);
816
+ it('should return undefined for trunk-based workflow', () => {
817
+ const result = getValidationApprovedGitInstructions(
818
+ { git_workflow: 'trunk-based', git_main_branch: 'main' },
819
+ 'feature/test'
820
+ );
821
+ expect(result).toBeUndefined();
822
+ });
579
823
 
580
- await expect(
581
- updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)
582
- ).rejects.toThrow(ValidationError);
824
+ it('should return undefined when no task branch', () => {
825
+ const result = getValidationApprovedGitInstructions(
826
+ { git_workflow: 'github-flow', git_main_branch: 'main' },
827
+ undefined
828
+ );
829
+ expect(result).toBeUndefined();
583
830
  });
584
831
 
585
- it('should throw error for invalid priority', async () => {
586
- const supabase = createMockSupabase();
587
- const ctx = createMockContext(supabase);
832
+ it('should return merge instructions for github-flow', () => {
833
+ const result = getValidationApprovedGitInstructions(
834
+ { git_workflow: 'github-flow', git_main_branch: 'main' },
835
+ 'feature/test'
836
+ );
588
837
 
589
- await expect(
590
- updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', priority: 0 }, ctx)
591
- ).rejects.toThrow(ValidationError);
838
+ expect(result).toMatchObject({
839
+ target_branch: 'main',
840
+ feature_branch: 'feature/test',
841
+ steps: expect.any(Array),
842
+ cleanup: expect.any(Array),
843
+ });
592
844
  });
593
845
 
594
- it('should throw error for invalid progress_percentage', async () => {
595
- const supabase = createMockSupabase();
596
- const ctx = createMockContext(supabase);
846
+ it('should return merge instructions for git-flow with develop', () => {
847
+ const result = getValidationApprovedGitInstructions(
848
+ { git_workflow: 'git-flow', git_main_branch: 'main', git_develop_branch: 'develop' },
849
+ 'feature/test'
850
+ );
597
851
 
598
- await expect(
599
- updateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', progress_percentage: 150 }, ctx)
600
- ).rejects.toThrow(ValidationError);
852
+ expect(result).toMatchObject({
853
+ target_branch: 'develop',
854
+ feature_branch: 'feature/test',
855
+ });
601
856
  });
602
857
  });