@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,6 +1,4 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { SupabaseClient } from '@supabase/supabase-js';
3
- import type { HandlerContext, TokenUsage } from './types.js';
4
2
  import {
5
3
  createBodyOfWork,
6
4
  updateBodyOfWork,
@@ -16,123 +14,8 @@ import {
16
14
  getNextBodyOfWorkTask,
17
15
  } from './bodies-of-work.js';
18
16
  import { ValidationError } from '../validators.js';
19
-
20
- // ============================================================================
21
- // Test Utilities
22
- // ============================================================================
23
-
24
- function createMockSupabase(overrides: {
25
- selectResult?: { data: unknown; error: unknown };
26
- insertResult?: { data: unknown; error: unknown };
27
- updateResult?: { data: unknown; error: unknown };
28
- deleteResult?: { data: unknown; error: unknown };
29
- countResult?: { count: number | null; error: unknown };
30
- } = {}) {
31
- const defaultResult = { data: null, error: null };
32
- let currentOperation = 'select';
33
- let insertThenSelect = false;
34
-
35
- const mock = {
36
- from: vi.fn().mockReturnThis(),
37
- select: vi.fn(() => {
38
- if (currentOperation === 'insert') {
39
- insertThenSelect = true;
40
- } else {
41
- currentOperation = 'select';
42
- insertThenSelect = false;
43
- }
44
- return mock;
45
- }),
46
- insert: vi.fn(() => {
47
- currentOperation = 'insert';
48
- insertThenSelect = false;
49
- return mock;
50
- }),
51
- update: vi.fn(() => {
52
- currentOperation = 'update';
53
- insertThenSelect = false;
54
- return mock;
55
- }),
56
- delete: vi.fn(() => {
57
- currentOperation = 'delete';
58
- insertThenSelect = false;
59
- return mock;
60
- }),
61
- eq: vi.fn().mockReturnThis(),
62
- neq: vi.fn().mockReturnThis(),
63
- in: vi.fn().mockReturnThis(),
64
- is: vi.fn().mockReturnThis(),
65
- not: vi.fn().mockReturnThis(),
66
- or: vi.fn().mockReturnThis(),
67
- gte: vi.fn().mockReturnThis(),
68
- lte: vi.fn().mockReturnThis(),
69
- lt: vi.fn().mockReturnThis(),
70
- order: vi.fn().mockReturnThis(),
71
- limit: vi.fn().mockReturnThis(),
72
- head: vi.fn().mockReturnThis(),
73
- single: vi.fn(() => {
74
- if (currentOperation === 'insert' || insertThenSelect) {
75
- return Promise.resolve(overrides.insertResult ?? defaultResult);
76
- }
77
- if (currentOperation === 'select') {
78
- return Promise.resolve(overrides.selectResult ?? defaultResult);
79
- }
80
- if (currentOperation === 'update') {
81
- return Promise.resolve(overrides.updateResult ?? defaultResult);
82
- }
83
- if (currentOperation === 'delete') {
84
- return Promise.resolve(overrides.deleteResult ?? defaultResult);
85
- }
86
- return Promise.resolve(defaultResult);
87
- }),
88
- maybeSingle: vi.fn(() => {
89
- return Promise.resolve(overrides.selectResult ?? defaultResult);
90
- }),
91
- then: vi.fn((resolve: (value: unknown) => void) => {
92
- if (currentOperation === 'insert' || insertThenSelect) {
93
- return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
94
- }
95
- if (currentOperation === 'select') {
96
- const result = overrides.countResult ?? overrides.selectResult ?? defaultResult;
97
- return Promise.resolve(result).then(resolve);
98
- }
99
- if (currentOperation === 'update') {
100
- return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
101
- }
102
- if (currentOperation === 'delete') {
103
- return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
104
- }
105
- return Promise.resolve(defaultResult).then(resolve);
106
- }),
107
- };
108
-
109
- return mock as unknown as SupabaseClient;
110
- }
111
-
112
- function createMockContext(
113
- supabase: SupabaseClient,
114
- options: { sessionId?: string | null } = {}
115
- ): HandlerContext {
116
- const defaultTokenUsage: TokenUsage = {
117
- callCount: 5,
118
- totalTokens: 2500,
119
- byTool: {},
120
- };
121
-
122
- const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
123
-
124
- return {
125
- supabase,
126
- auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
127
- session: {
128
- instanceId: 'instance-abc',
129
- currentSessionId: sessionId,
130
- currentPersona: 'Wave',
131
- tokenUsage: defaultTokenUsage,
132
- },
133
- updateSession: vi.fn(),
134
- };
135
- }
17
+ import { createMockContext } from './__test-utils__.js';
18
+ import { mockApiClient } from './__test-setup__.js';
136
19
 
137
20
  const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
138
21
  const VALID_UUID_2 = '223e4567-e89b-12d3-a456-426614174001';
@@ -146,35 +29,30 @@ describe('createBodyOfWork', () => {
146
29
  beforeEach(() => vi.clearAllMocks());
147
30
 
148
31
  it('should throw error for missing project_id', async () => {
149
- const supabase = createMockSupabase();
150
- const ctx = createMockContext(supabase);
151
-
32
+ const ctx = createMockContext();
152
33
  await expect(createBodyOfWork({ title: 'Test' }, ctx)).rejects.toThrow(ValidationError);
153
34
  });
154
35
 
155
36
  it('should throw error for invalid project_id UUID', async () => {
156
- const supabase = createMockSupabase();
157
- const ctx = createMockContext(supabase);
158
-
37
+ const ctx = createMockContext();
159
38
  await expect(
160
39
  createBodyOfWork({ project_id: 'invalid', title: 'Test' }, ctx)
161
40
  ).rejects.toThrow(ValidationError);
162
41
  });
163
42
 
164
43
  it('should throw error for missing title', async () => {
165
- const supabase = createMockSupabase();
166
- const ctx = createMockContext(supabase);
167
-
44
+ const ctx = createMockContext();
168
45
  await expect(
169
46
  createBodyOfWork({ project_id: VALID_UUID }, ctx)
170
47
  ).rejects.toThrow(ValidationError);
171
48
  });
172
49
 
173
50
  it('should create body of work with required fields', async () => {
174
- const supabase = createMockSupabase({
175
- insertResult: { data: { id: 'bow-1' }, error: null },
51
+ const ctx = createMockContext();
52
+ mockApiClient.proxy.mockResolvedValue({
53
+ ok: true,
54
+ data: { body_of_work_id: 'bow-1' },
176
55
  });
177
- const ctx = createMockContext(supabase);
178
56
 
179
57
  const result = await createBodyOfWork(
180
58
  { project_id: VALID_UUID, title: 'Sprint 1' },
@@ -187,24 +65,22 @@ describe('createBodyOfWork', () => {
187
65
  title: 'Sprint 1',
188
66
  status: 'draft',
189
67
  });
190
- expect(supabase.from).toHaveBeenCalledWith('bodies_of_work');
191
- expect(supabase.insert).toHaveBeenCalledWith(
68
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
69
+ 'create_body_of_work',
192
70
  expect.objectContaining({
193
71
  project_id: VALID_UUID,
194
72
  title: 'Sprint 1',
195
- auto_deploy_on_completion: false,
196
- deploy_environment: 'production',
197
- deploy_version_bump: 'minor',
198
- deploy_trigger: 'all_completed_validated',
199
- })
73
+ }),
74
+ expect.any(Object)
200
75
  );
201
76
  });
202
77
 
203
78
  it('should create body of work with all optional fields', async () => {
204
- const supabase = createMockSupabase({
205
- insertResult: { data: { id: 'bow-2' }, error: null },
79
+ const ctx = createMockContext();
80
+ mockApiClient.proxy.mockResolvedValue({
81
+ ok: true,
82
+ data: { body_of_work_id: 'bow-2' },
206
83
  });
207
- const ctx = createMockContext(supabase);
208
84
 
209
85
  await createBodyOfWork(
210
86
  {
@@ -219,22 +95,26 @@ describe('createBodyOfWork', () => {
219
95
  ctx
220
96
  );
221
97
 
222
- expect(supabase.insert).toHaveBeenCalledWith(
98
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
99
+ 'create_body_of_work',
223
100
  expect.objectContaining({
101
+ title: 'Release 2.0',
224
102
  description: 'Major release',
225
103
  auto_deploy_on_completion: true,
226
104
  deploy_environment: 'staging',
227
105
  deploy_version_bump: 'major',
228
106
  deploy_trigger: 'all_completed',
229
- })
107
+ }),
108
+ expect.any(Object)
230
109
  );
231
110
  });
232
111
 
233
- it('should throw error when database insert fails', async () => {
234
- const supabase = createMockSupabase({
235
- insertResult: { data: null, error: { message: 'Insert failed' } },
112
+ it('should throw error when API call fails', async () => {
113
+ const ctx = createMockContext();
114
+ mockApiClient.proxy.mockResolvedValue({
115
+ ok: false,
116
+ error: 'Insert failed',
236
117
  });
237
- const ctx = createMockContext(supabase);
238
118
 
239
119
  await expect(
240
120
  createBodyOfWork({ project_id: VALID_UUID, title: 'Test' }, ctx)
@@ -250,30 +130,27 @@ describe('updateBodyOfWork', () => {
250
130
  beforeEach(() => vi.clearAllMocks());
251
131
 
252
132
  it('should throw error for missing body_of_work_id', async () => {
253
- const supabase = createMockSupabase();
254
- const ctx = createMockContext(supabase);
255
-
133
+ const ctx = createMockContext();
256
134
  await expect(updateBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
257
135
  });
258
136
 
259
137
  it('should return success when no updates provided', async () => {
260
- const supabase = createMockSupabase();
261
- const ctx = createMockContext(supabase);
262
-
138
+ const ctx = createMockContext();
263
139
  const result = await updateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
264
140
 
265
141
  expect(result.result).toMatchObject({
266
142
  success: true,
267
143
  message: 'No updates provided',
268
144
  });
269
- expect(supabase.update).not.toHaveBeenCalled();
145
+ expect(mockApiClient.proxy).not.toHaveBeenCalled();
270
146
  });
271
147
 
272
148
  it('should update title', async () => {
273
- const supabase = createMockSupabase({
274
- updateResult: { data: null, error: null },
149
+ const ctx = createMockContext();
150
+ mockApiClient.proxy.mockResolvedValue({
151
+ ok: true,
152
+ data: { success: true },
275
153
  });
276
- const ctx = createMockContext(supabase);
277
154
 
278
155
  const result = await updateBodyOfWork(
279
156
  { body_of_work_id: VALID_UUID, title: 'New Title' },
@@ -281,14 +158,18 @@ describe('updateBodyOfWork', () => {
281
158
  );
282
159
 
283
160
  expect(result.result).toMatchObject({ success: true, body_of_work_id: VALID_UUID });
284
- expect(supabase.update).toHaveBeenCalledWith({ title: 'New Title' });
161
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('update_body_of_work', expect.objectContaining({
162
+ body_of_work_id: VALID_UUID,
163
+ title: 'New Title',
164
+ }));
285
165
  });
286
166
 
287
167
  it('should update multiple fields', async () => {
288
- const supabase = createMockSupabase({
289
- updateResult: { data: null, error: null },
168
+ const ctx = createMockContext();
169
+ mockApiClient.proxy.mockResolvedValue({
170
+ ok: true,
171
+ data: { success: true },
290
172
  });
291
- const ctx = createMockContext(supabase);
292
173
 
293
174
  await updateBodyOfWork(
294
175
  {
@@ -300,11 +181,11 @@ describe('updateBodyOfWork', () => {
300
181
  ctx
301
182
  );
302
183
 
303
- expect(supabase.update).toHaveBeenCalledWith({
184
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('update_body_of_work', expect.objectContaining({
304
185
  title: 'Updated',
305
186
  description: 'New desc',
306
187
  auto_deploy_on_completion: true,
307
- });
188
+ }));
308
189
  });
309
190
  });
310
191
 
@@ -316,76 +197,39 @@ describe('getBodyOfWork', () => {
316
197
  beforeEach(() => vi.clearAllMocks());
317
198
 
318
199
  it('should throw error for missing body_of_work_id', async () => {
319
- const supabase = createMockSupabase();
320
- const ctx = createMockContext(supabase);
321
-
200
+ const ctx = createMockContext();
322
201
  await expect(getBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
323
202
  });
324
203
 
325
204
  it('should throw error when body of work not found', async () => {
326
- const supabase = createMockSupabase({
327
- selectResult: { data: null, error: { message: 'Not found' } },
205
+ const ctx = createMockContext();
206
+ mockApiClient.proxy.mockResolvedValue({
207
+ ok: false,
208
+ error: 'Not found',
328
209
  });
329
- const ctx = createMockContext(supabase);
330
210
 
331
211
  await expect(
332
212
  getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
333
- ).rejects.toThrow('Body of work not found');
213
+ ).rejects.toThrow('Failed to get body of work');
334
214
  });
335
215
 
336
216
  it('should return body of work with tasks organized by phase', async () => {
337
- const mockBow = {
338
- id: 'bow-1',
339
- title: 'Sprint 1',
340
- status: 'active',
341
- };
342
- const mockTaskLinks = [
343
- { phase: 'pre', order_index: 0, tasks: { id: 't1', title: 'Setup', status: 'completed', priority: 1, progress_percentage: 100 } },
344
- { phase: 'core', order_index: 0, tasks: { id: 't2', title: 'Feature A', status: 'in_progress', priority: 2, progress_percentage: 50 } },
345
- { phase: 'core', order_index: 1, tasks: { id: 't3', title: 'Feature B', status: 'pending', priority: 2, progress_percentage: 0 } },
346
- { phase: 'post', order_index: 0, tasks: { id: 't4', title: 'Cleanup', status: 'pending', priority: 3, progress_percentage: 0 } },
347
- ];
348
-
349
- const supabase = createMockSupabase({
350
- selectResult: { data: mockBow, error: null },
351
- });
352
-
353
- // Override for task links query
354
- let queryCount = 0;
355
- vi.mocked(supabase.from).mockImplementation((table: string) => {
356
- queryCount++;
357
- if (table === 'bodies_of_work') {
358
- return {
359
- ...supabase,
360
- select: vi.fn().mockReturnValue({
361
- ...supabase,
362
- eq: vi.fn().mockReturnValue({
363
- ...supabase,
364
- single: vi.fn().mockResolvedValue({ data: mockBow, error: null }),
365
- }),
366
- }),
367
- } as unknown as ReturnType<SupabaseClient['from']>;
368
- }
369
- if (table === 'body_of_work_tasks') {
370
- return {
371
- ...supabase,
372
- select: vi.fn().mockReturnValue({
373
- ...supabase,
374
- eq: vi.fn().mockReturnValue({
375
- ...supabase,
376
- order: vi.fn().mockReturnValue({
377
- then: (resolve: (val: unknown) => void) =>
378
- Promise.resolve({ data: mockTaskLinks, error: null }).then(resolve),
379
- }),
380
- }),
381
- }),
382
- } as unknown as ReturnType<SupabaseClient['from']>;
383
- }
384
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
217
+ const ctx = createMockContext();
218
+ mockApiClient.proxy.mockResolvedValue({
219
+ ok: true,
220
+ data: {
221
+ id: 'bow-1',
222
+ title: 'Sprint 1',
223
+ status: 'active',
224
+ pre_tasks: [{ id: 't1', title: 'Setup' }],
225
+ core_tasks: [{ id: 't2', title: 'Feature A' }],
226
+ post_tasks: [{ id: 't3', title: 'Cleanup' }],
227
+ total_tasks: 3,
228
+ completed_tasks: 1,
229
+ progress_percentage: 33,
230
+ },
385
231
  });
386
232
 
387
- const ctx = createMockContext(supabase);
388
-
389
233
  const result = await getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
390
234
 
391
235
  expect(result.result).toHaveProperty('pre_tasks');
@@ -403,30 +247,16 @@ describe('getBodiesOfWork', () => {
403
247
  beforeEach(() => vi.clearAllMocks());
404
248
 
405
249
  it('should throw error for missing project_id', async () => {
406
- const supabase = createMockSupabase();
407
- const ctx = createMockContext(supabase);
408
-
250
+ const ctx = createMockContext();
409
251
  await expect(getBodiesOfWork({}, ctx)).rejects.toThrow(ValidationError);
410
252
  });
411
253
 
412
254
  it('should return empty array when no bodies of work exist', async () => {
413
- const supabase = createMockSupabase();
414
-
415
- vi.mocked(supabase.from).mockReturnValue({
416
- ...supabase,
417
- select: vi.fn().mockReturnValue({
418
- ...supabase,
419
- eq: vi.fn().mockReturnValue({
420
- ...supabase,
421
- order: vi.fn().mockReturnValue({
422
- then: (resolve: (val: unknown) => void) =>
423
- Promise.resolve({ data: [], error: null }).then(resolve),
424
- }),
425
- }),
426
- }),
427
- } as unknown as ReturnType<SupabaseClient['from']>);
428
-
429
- const ctx = createMockContext(supabase);
255
+ const ctx = createMockContext();
256
+ mockApiClient.proxy.mockResolvedValue({
257
+ ok: true,
258
+ data: { bodies_of_work: [], total_count: 0 },
259
+ });
430
260
 
431
261
  const result = await getBodiesOfWork({ project_id: VALID_UUID }, ctx);
432
262
 
@@ -434,13 +264,18 @@ describe('getBodiesOfWork', () => {
434
264
  });
435
265
 
436
266
  it('should filter by status when provided', async () => {
437
- const supabase = createMockSupabase();
438
- const ctx = createMockContext(supabase);
267
+ const ctx = createMockContext();
268
+ mockApiClient.proxy.mockResolvedValue({
269
+ ok: true,
270
+ data: { bodies_of_work: [], total_count: 0 },
271
+ });
439
272
 
440
273
  await getBodiesOfWork({ project_id: VALID_UUID, status: 'active' }, ctx);
441
274
 
442
- expect(supabase.eq).toHaveBeenCalledWith('project_id', VALID_UUID);
443
- expect(supabase.eq).toHaveBeenCalledWith('status', 'active');
275
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('get_bodies_of_work', expect.objectContaining({
276
+ project_id: VALID_UUID,
277
+ status: 'active',
278
+ }));
444
279
  });
445
280
  });
446
281
 
@@ -452,17 +287,16 @@ describe('deleteBodyOfWork', () => {
452
287
  beforeEach(() => vi.clearAllMocks());
453
288
 
454
289
  it('should throw error for missing body_of_work_id', async () => {
455
- const supabase = createMockSupabase();
456
- const ctx = createMockContext(supabase);
457
-
290
+ const ctx = createMockContext();
458
291
  await expect(deleteBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
459
292
  });
460
293
 
461
294
  it('should delete body of work successfully', async () => {
462
- const supabase = createMockSupabase({
463
- deleteResult: { data: null, error: null },
295
+ const ctx = createMockContext();
296
+ mockApiClient.proxy.mockResolvedValue({
297
+ ok: true,
298
+ data: { success: true },
464
299
  });
465
- const ctx = createMockContext(supabase);
466
300
 
467
301
  const result = await deleteBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
468
302
 
@@ -470,8 +304,9 @@ describe('deleteBodyOfWork', () => {
470
304
  success: true,
471
305
  message: 'Body of work deleted. Tasks are preserved.',
472
306
  });
473
- expect(supabase.from).toHaveBeenCalledWith('bodies_of_work');
474
- expect(supabase.delete).toHaveBeenCalled();
307
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('delete_body_of_work', expect.objectContaining({
308
+ body_of_work_id: VALID_UUID,
309
+ }));
475
310
  });
476
311
  });
477
312
 
@@ -483,158 +318,44 @@ describe('addTaskToBodyOfWork', () => {
483
318
  beforeEach(() => vi.clearAllMocks());
484
319
 
485
320
  it('should throw error for missing body_of_work_id', async () => {
486
- const supabase = createMockSupabase();
487
- const ctx = createMockContext(supabase);
488
-
321
+ const ctx = createMockContext();
489
322
  await expect(
490
323
  addTaskToBodyOfWork({ task_id: VALID_UUID }, ctx)
491
324
  ).rejects.toThrow(ValidationError);
492
325
  });
493
326
 
494
327
  it('should throw error for missing task_id', async () => {
495
- const supabase = createMockSupabase();
496
- const ctx = createMockContext(supabase);
497
-
328
+ const ctx = createMockContext();
498
329
  await expect(
499
330
  addTaskToBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
500
331
  ).rejects.toThrow(ValidationError);
501
332
  });
502
333
 
503
- it('should throw error when body of work not found', async () => {
504
- const supabase = createMockSupabase({
505
- selectResult: { data: null, error: { message: 'Not found' } },
334
+ it('should throw error when API returns error', async () => {
335
+ const ctx = createMockContext();
336
+ mockApiClient.proxy.mockResolvedValue({
337
+ ok: false,
338
+ error: 'Body of work not found',
506
339
  });
507
- const ctx = createMockContext(supabase);
508
340
 
509
341
  await expect(
510
342
  addTaskToBodyOfWork({ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 }, ctx)
511
- ).rejects.toThrow('Body of work not found');
512
- });
513
-
514
- it('should throw error when body of work is completed', async () => {
515
- const supabase = createMockSupabase({
516
- selectResult: { data: { status: 'completed' }, error: null },
517
- });
518
- const ctx = createMockContext(supabase);
519
-
520
- await expect(
521
- addTaskToBodyOfWork({ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 }, ctx)
522
- ).rejects.toThrow('Cannot add tasks to completed body of work');
523
- });
524
-
525
- it('should throw error when task is already in a body of work', async () => {
526
- const supabase = createMockSupabase();
527
- let callCount = 0;
528
-
529
- vi.mocked(supabase.from).mockImplementation((table: string) => {
530
- if (table === 'bodies_of_work') {
531
- return {
532
- ...supabase,
533
- select: vi.fn().mockReturnValue({
534
- ...supabase,
535
- eq: vi.fn().mockReturnValue({
536
- ...supabase,
537
- single: vi.fn().mockResolvedValue({ data: { status: 'draft' }, error: null }),
538
- }),
539
- }),
540
- } as unknown as ReturnType<SupabaseClient['from']>;
541
- }
542
- if (table === 'body_of_work_tasks') {
543
- callCount++;
544
- if (callCount === 1) {
545
- // First call - check existing link
546
- return {
547
- ...supabase,
548
- select: vi.fn().mockReturnValue({
549
- ...supabase,
550
- eq: vi.fn().mockReturnValue({
551
- ...supabase,
552
- single: vi.fn().mockResolvedValue({ data: { body_of_work_id: 'other-bow' }, error: null }),
553
- }),
554
- }),
555
- } as unknown as ReturnType<SupabaseClient['from']>;
556
- }
557
- }
558
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
559
- });
560
-
561
- const ctx = createMockContext(supabase);
562
-
563
- await expect(
564
- addTaskToBodyOfWork({ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 }, ctx)
565
- ).rejects.toThrow('Task is already assigned to a body of work');
343
+ ).rejects.toThrow('Failed to add task to body of work');
566
344
  });
567
345
 
568
346
  it('should add task with default phase "core"', async () => {
569
- const supabase = createMockSupabase();
570
- let bowCallCount = 0;
571
- let taskLinksCallCount = 0;
572
-
573
- vi.mocked(supabase.from).mockImplementation((table: string) => {
574
- if (table === 'bodies_of_work') {
575
- bowCallCount++;
576
- return {
577
- ...supabase,
578
- select: vi.fn().mockReturnValue({
579
- ...supabase,
580
- eq: vi.fn().mockReturnValue({
581
- ...supabase,
582
- single: vi.fn().mockResolvedValue({ data: { status: 'draft' }, error: null }),
583
- }),
584
- }),
585
- } as unknown as ReturnType<SupabaseClient['from']>;
586
- }
587
- if (table === 'body_of_work_tasks') {
588
- taskLinksCallCount++;
589
- if (taskLinksCallCount === 1) {
590
- // Check existing link - none
591
- return {
592
- ...supabase,
593
- select: vi.fn().mockReturnValue({
594
- ...supabase,
595
- eq: vi.fn().mockReturnValue({
596
- ...supabase,
597
- single: vi.fn().mockResolvedValue({ data: null, error: null }),
598
- }),
599
- }),
600
- } as unknown as ReturnType<SupabaseClient['from']>;
601
- }
602
- if (taskLinksCallCount === 2) {
603
- // Get max order - none
604
- return {
605
- ...supabase,
606
- select: vi.fn().mockReturnValue({
607
- ...supabase,
608
- eq: vi.fn().mockReturnValue({
609
- ...supabase,
610
- eq: vi.fn().mockReturnValue({
611
- ...supabase,
612
- order: vi.fn().mockReturnValue({
613
- ...supabase,
614
- limit: vi.fn().mockReturnValue({
615
- ...supabase,
616
- single: vi.fn().mockResolvedValue({ data: null, error: null }),
617
- }),
618
- }),
619
- }),
620
- }),
621
- }),
622
- } as unknown as ReturnType<SupabaseClient['from']>;
623
- }
624
- // Insert
625
- return {
626
- ...supabase,
627
- insert: vi.fn().mockReturnValue({
628
- then: (resolve: (val: unknown) => void) =>
629
- Promise.resolve({ data: null, error: null }).then(resolve),
630
- }),
631
- } as unknown as ReturnType<SupabaseClient['from']>;
632
- }
633
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
347
+ const ctx = createMockContext();
348
+ mockApiClient.proxy.mockResolvedValue({
349
+ ok: true,
350
+ data: {
351
+ success: true,
352
+ body_of_work_id: VALID_UUID,
353
+ task_id: VALID_UUID_2,
354
+ phase: 'core',
355
+ order_index: 0,
356
+ },
634
357
  });
635
358
 
636
- const ctx = createMockContext(supabase);
637
-
638
359
  const result = await addTaskToBodyOfWork(
639
360
  { body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 },
640
361
  ctx
@@ -648,6 +369,30 @@ describe('addTaskToBodyOfWork', () => {
648
369
  order_index: 0,
649
370
  });
650
371
  });
372
+
373
+ it('should add task with specified phase', async () => {
374
+ const ctx = createMockContext();
375
+ mockApiClient.proxy.mockResolvedValue({
376
+ ok: true,
377
+ data: {
378
+ body_of_work_id: VALID_UUID,
379
+ task_id: VALID_UUID_2,
380
+ phase: 'pre',
381
+ order_index: 0,
382
+ },
383
+ });
384
+
385
+ await addTaskToBodyOfWork(
386
+ { body_of_work_id: VALID_UUID, task_id: VALID_UUID_2, phase: 'pre' },
387
+ ctx
388
+ );
389
+
390
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('add_task_to_body_of_work', expect.objectContaining({
391
+ body_of_work_id: VALID_UUID,
392
+ task_id: VALID_UUID_2,
393
+ phase: 'pre',
394
+ }));
395
+ });
651
396
  });
652
397
 
653
398
  // ============================================================================
@@ -658,64 +403,23 @@ describe('removeTaskFromBodyOfWork', () => {
658
403
  beforeEach(() => vi.clearAllMocks());
659
404
 
660
405
  it('should throw error for missing task_id', async () => {
661
- const supabase = createMockSupabase();
662
- const ctx = createMockContext(supabase);
663
-
406
+ const ctx = createMockContext();
664
407
  await expect(removeTaskFromBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
665
408
  });
666
409
 
667
- it('should return success when task is not in any body of work', async () => {
668
- const supabase = createMockSupabase({
669
- selectResult: { data: null, error: null },
410
+ it('should remove task successfully', async () => {
411
+ const ctx = createMockContext();
412
+ mockApiClient.proxy.mockResolvedValue({
413
+ ok: true,
414
+ data: { success: true },
670
415
  });
671
- const ctx = createMockContext(supabase);
672
416
 
673
417
  const result = await removeTaskFromBodyOfWork({ task_id: VALID_UUID }, ctx);
674
418
 
675
- expect(result.result).toMatchObject({
676
- success: true,
677
- message: 'Task is not in any body of work',
678
- });
679
- });
680
-
681
- it('should throw error when body of work is completed', async () => {
682
- const supabase = createMockSupabase();
683
- let callCount = 0;
684
-
685
- vi.mocked(supabase.from).mockImplementation((table: string) => {
686
- callCount++;
687
- if (table === 'body_of_work_tasks' && callCount === 1) {
688
- return {
689
- ...supabase,
690
- select: vi.fn().mockReturnValue({
691
- ...supabase,
692
- eq: vi.fn().mockReturnValue({
693
- ...supabase,
694
- single: vi.fn().mockResolvedValue({ data: { body_of_work_id: 'bow-1' }, error: null }),
695
- }),
696
- }),
697
- } as unknown as ReturnType<SupabaseClient['from']>;
698
- }
699
- if (table === 'bodies_of_work') {
700
- return {
701
- ...supabase,
702
- select: vi.fn().mockReturnValue({
703
- ...supabase,
704
- eq: vi.fn().mockReturnValue({
705
- ...supabase,
706
- single: vi.fn().mockResolvedValue({ data: { status: 'completed' }, error: null }),
707
- }),
708
- }),
709
- } as unknown as ReturnType<SupabaseClient['from']>;
710
- }
711
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
712
- });
713
-
714
- const ctx = createMockContext(supabase);
715
-
716
- await expect(
717
- removeTaskFromBodyOfWork({ task_id: VALID_UUID }, ctx)
718
- ).rejects.toThrow('Cannot remove tasks from a completed body of work');
419
+ expect(result.result).toMatchObject({ success: true });
420
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('remove_task_from_body_of_work', expect.objectContaining({
421
+ task_id: VALID_UUID,
422
+ }));
719
423
  });
720
424
  });
721
425
 
@@ -727,121 +431,34 @@ describe('activateBodyOfWork', () => {
727
431
  beforeEach(() => vi.clearAllMocks());
728
432
 
729
433
  it('should throw error for missing body_of_work_id', async () => {
730
- const supabase = createMockSupabase();
731
- const ctx = createMockContext(supabase);
732
-
434
+ const ctx = createMockContext();
733
435
  await expect(activateBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
734
436
  });
735
437
 
736
- it('should throw error when body of work not found', async () => {
737
- const supabase = createMockSupabase({
738
- selectResult: { data: null, error: { message: 'Not found' } },
739
- });
740
- const ctx = createMockContext(supabase);
741
-
742
- await expect(
743
- activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
744
- ).rejects.toThrow('Body of work not found');
745
- });
746
-
747
- it('should throw error when body of work is not draft', async () => {
748
- const supabase = createMockSupabase({
749
- selectResult: { data: { status: 'active', title: 'Test' }, error: null },
750
- });
751
- const ctx = createMockContext(supabase);
752
-
753
- await expect(
754
- activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
755
- ).rejects.toThrow('Can only activate draft bodies of work');
756
- });
757
-
758
- it('should throw error when body of work has no tasks', async () => {
759
- const supabase = createMockSupabase();
760
- let callCount = 0;
761
-
762
- vi.mocked(supabase.from).mockImplementation((table: string) => {
763
- callCount++;
764
- if (table === 'bodies_of_work' && callCount === 1) {
765
- return {
766
- ...supabase,
767
- select: vi.fn().mockReturnValue({
768
- ...supabase,
769
- eq: vi.fn().mockReturnValue({
770
- ...supabase,
771
- single: vi.fn().mockResolvedValue({ data: { status: 'draft', title: 'Test' }, error: null }),
772
- }),
773
- }),
774
- } as unknown as ReturnType<SupabaseClient['from']>;
775
- }
776
- if (table === 'body_of_work_tasks') {
777
- return {
778
- ...supabase,
779
- select: vi.fn().mockReturnValue({
780
- ...supabase,
781
- eq: vi.fn().mockReturnValue({
782
- then: (resolve: (val: unknown) => void) =>
783
- Promise.resolve({ count: 0, error: null }).then(resolve),
784
- }),
785
- }),
786
- } as unknown as ReturnType<SupabaseClient['from']>;
787
- }
788
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
438
+ it('should throw error when API returns error', async () => {
439
+ const ctx = createMockContext();
440
+ mockApiClient.proxy.mockResolvedValue({
441
+ ok: false,
442
+ error: 'Body of work not found',
789
443
  });
790
444
 
791
- const ctx = createMockContext(supabase);
792
-
793
445
  await expect(
794
446
  activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
795
- ).rejects.toThrow('Cannot activate body of work with no tasks');
447
+ ).rejects.toThrow('Failed to activate body of work');
796
448
  });
797
449
 
798
- it('should activate body of work with tasks', async () => {
799
- const supabase = createMockSupabase();
800
- let callCount = 0;
801
-
802
- vi.mocked(supabase.from).mockImplementation((table: string) => {
803
- callCount++;
804
- if (table === 'bodies_of_work' && callCount === 1) {
805
- return {
806
- ...supabase,
807
- select: vi.fn().mockReturnValue({
808
- ...supabase,
809
- eq: vi.fn().mockReturnValue({
810
- ...supabase,
811
- single: vi.fn().mockResolvedValue({ data: { status: 'draft', title: 'Sprint 1' }, error: null }),
812
- }),
813
- }),
814
- } as unknown as ReturnType<SupabaseClient['from']>;
815
- }
816
- if (table === 'body_of_work_tasks') {
817
- return {
818
- ...supabase,
819
- select: vi.fn().mockReturnValue({
820
- ...supabase,
821
- eq: vi.fn().mockReturnValue({
822
- then: (resolve: (val: unknown) => void) =>
823
- Promise.resolve({ count: 3, error: null }).then(resolve),
824
- }),
825
- }),
826
- } as unknown as ReturnType<SupabaseClient['from']>;
827
- }
828
- if (table === 'bodies_of_work' && callCount > 2) {
829
- return {
830
- ...supabase,
831
- update: vi.fn().mockReturnValue({
832
- ...supabase,
833
- eq: vi.fn().mockReturnValue({
834
- then: (resolve: (val: unknown) => void) =>
835
- Promise.resolve({ data: null, error: null }).then(resolve),
836
- }),
837
- }),
838
- } as unknown as ReturnType<SupabaseClient['from']>;
839
- }
840
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
450
+ it('should activate body of work successfully', async () => {
451
+ const ctx = createMockContext();
452
+ mockApiClient.proxy.mockResolvedValue({
453
+ ok: true,
454
+ data: {
455
+ success: true,
456
+ body_of_work_id: VALID_UUID,
457
+ title: 'Sprint 1',
458
+ status: 'active',
459
+ },
841
460
  });
842
461
 
843
- const ctx = createMockContext(supabase);
844
-
845
462
  const result = await activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
846
463
 
847
464
  expect(result.result).toMatchObject({
@@ -861,18 +478,14 @@ describe('addTaskDependency', () => {
861
478
  beforeEach(() => vi.clearAllMocks());
862
479
 
863
480
  it('should throw error for missing body_of_work_id', async () => {
864
- const supabase = createMockSupabase();
865
- const ctx = createMockContext(supabase);
866
-
481
+ const ctx = createMockContext();
867
482
  await expect(
868
483
  addTaskDependency({ task_id: VALID_UUID, depends_on_task_id: VALID_UUID_2 }, ctx)
869
484
  ).rejects.toThrow(ValidationError);
870
485
  });
871
486
 
872
487
  it('should throw error when task depends on itself', async () => {
873
- const supabase = createMockSupabase();
874
- const ctx = createMockContext(supabase);
875
-
488
+ const ctx = createMockContext();
876
489
  await expect(
877
490
  addTaskDependency({
878
491
  body_of_work_id: VALID_UUID,
@@ -882,32 +495,28 @@ describe('addTaskDependency', () => {
882
495
  ).rejects.toThrow('A task cannot depend on itself');
883
496
  });
884
497
 
885
- it('should throw error when tasks do not belong to body of work', async () => {
886
- const supabase = createMockSupabase();
887
-
888
- vi.mocked(supabase.from).mockReturnValue({
889
- ...supabase,
890
- select: vi.fn().mockReturnValue({
891
- ...supabase,
892
- eq: vi.fn().mockReturnValue({
893
- ...supabase,
894
- in: vi.fn().mockReturnValue({
895
- then: (resolve: (val: unknown) => void) =>
896
- Promise.resolve({ data: [{ task_id: VALID_UUID_2 }], error: null }).then(resolve),
897
- }),
898
- }),
899
- }),
900
- } as unknown as ReturnType<SupabaseClient['from']>);
901
-
902
- const ctx = createMockContext(supabase);
903
-
904
- await expect(
905
- addTaskDependency({
906
- body_of_work_id: VALID_UUID,
498
+ it('should add dependency successfully', async () => {
499
+ const ctx = createMockContext();
500
+ mockApiClient.proxy.mockResolvedValue({
501
+ ok: true,
502
+ data: {
503
+ success: true,
907
504
  task_id: VALID_UUID_2,
908
505
  depends_on_task_id: VALID_UUID_3,
909
- }, ctx)
910
- ).rejects.toThrow('Both tasks must belong to the specified body of work');
506
+ },
507
+ });
508
+
509
+ const result = await addTaskDependency({
510
+ body_of_work_id: VALID_UUID,
511
+ task_id: VALID_UUID_2,
512
+ depends_on_task_id: VALID_UUID_3,
513
+ }, ctx);
514
+
515
+ expect(result.result).toMatchObject({
516
+ success: true,
517
+ task_id: VALID_UUID_2,
518
+ depends_on_task_id: VALID_UUID_3,
519
+ });
911
520
  });
912
521
  });
913
522
 
@@ -919,28 +528,29 @@ describe('removeTaskDependency', () => {
919
528
  beforeEach(() => vi.clearAllMocks());
920
529
 
921
530
  it('should throw error for missing task_id', async () => {
922
- const supabase = createMockSupabase();
923
- const ctx = createMockContext(supabase);
924
-
531
+ const ctx = createMockContext();
925
532
  await expect(
926
533
  removeTaskDependency({ depends_on_task_id: VALID_UUID }, ctx)
927
534
  ).rejects.toThrow(ValidationError);
928
535
  });
929
536
 
930
537
  it('should throw error for missing depends_on_task_id', async () => {
931
- const supabase = createMockSupabase();
932
- const ctx = createMockContext(supabase);
933
-
538
+ const ctx = createMockContext();
934
539
  await expect(
935
540
  removeTaskDependency({ task_id: VALID_UUID }, ctx)
936
541
  ).rejects.toThrow(ValidationError);
937
542
  });
938
543
 
939
544
  it('should remove dependency successfully', async () => {
940
- const supabase = createMockSupabase({
941
- deleteResult: { data: null, error: null },
545
+ const ctx = createMockContext();
546
+ mockApiClient.proxy.mockResolvedValue({
547
+ ok: true,
548
+ data: {
549
+ success: true,
550
+ task_id: VALID_UUID,
551
+ depends_on_task_id: VALID_UUID_2,
552
+ },
942
553
  });
943
- const ctx = createMockContext(supabase);
944
554
 
945
555
  const result = await removeTaskDependency(
946
556
  { task_id: VALID_UUID, depends_on_task_id: VALID_UUID_2 },
@@ -963,9 +573,7 @@ describe('getTaskDependencies', () => {
963
573
  beforeEach(() => vi.clearAllMocks());
964
574
 
965
575
  it('should throw error when neither body_of_work_id nor task_id provided', async () => {
966
- const supabase = createMockSupabase();
967
- const ctx = createMockContext(supabase);
968
-
576
+ const ctx = createMockContext();
969
577
  await expect(getTaskDependencies({}, ctx)).rejects.toThrow(
970
578
  'Either body_of_work_id or task_id is required'
971
579
  );
@@ -976,46 +584,33 @@ describe('getTaskDependencies', () => {
976
584
  { id: 'd1', task_id: 't1', depends_on_task_id: 't2', created_at: '2026-01-14' },
977
585
  ];
978
586
 
979
- const supabase = createMockSupabase();
980
-
981
- vi.mocked(supabase.from).mockReturnValue({
982
- ...supabase,
983
- select: vi.fn().mockReturnValue({
984
- ...supabase,
985
- eq: vi.fn().mockReturnValue({
986
- then: (resolve: (val: unknown) => void) =>
987
- Promise.resolve({ data: mockDeps, error: null }).then(resolve),
988
- }),
989
- }),
990
- } as unknown as ReturnType<SupabaseClient['from']>);
991
-
992
- const ctx = createMockContext(supabase);
587
+ const ctx = createMockContext();
588
+ mockApiClient.proxy.mockResolvedValue({
589
+ ok: true,
590
+ data: { dependencies: mockDeps },
591
+ });
993
592
 
994
593
  const result = await getTaskDependencies({ body_of_work_id: VALID_UUID }, ctx);
995
594
 
996
595
  expect(result.result).toMatchObject({ dependencies: mockDeps });
596
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('get_task_dependencies', expect.objectContaining({
597
+ body_of_work_id: VALID_UUID,
598
+ }));
997
599
  });
998
600
 
999
601
  it('should return dependencies filtered by task_id', async () => {
1000
- const supabase = createMockSupabase();
1001
-
1002
- vi.mocked(supabase.from).mockReturnValue({
1003
- ...supabase,
1004
- select: vi.fn().mockReturnValue({
1005
- ...supabase,
1006
- eq: vi.fn().mockReturnValue({
1007
- then: (resolve: (val: unknown) => void) =>
1008
- Promise.resolve({ data: [], error: null }).then(resolve),
1009
- }),
1010
- }),
1011
- } as unknown as ReturnType<SupabaseClient['from']>);
1012
-
1013
- const ctx = createMockContext(supabase);
602
+ const ctx = createMockContext();
603
+ mockApiClient.proxy.mockResolvedValue({
604
+ ok: true,
605
+ data: { dependencies: [] },
606
+ });
1014
607
 
1015
608
  const result = await getTaskDependencies({ task_id: VALID_UUID }, ctx);
1016
609
 
1017
610
  expect(result.result).toMatchObject({ dependencies: [] });
1018
- expect(supabase.from).toHaveBeenCalledWith('body_of_work_task_dependencies');
611
+ expect(mockApiClient.proxy).toHaveBeenCalledWith('get_task_dependencies', expect.objectContaining({
612
+ task_id: VALID_UUID,
613
+ }));
1019
614
  });
1020
615
  });
1021
616
 
@@ -1027,103 +622,55 @@ describe('getNextBodyOfWorkTask', () => {
1027
622
  beforeEach(() => vi.clearAllMocks());
1028
623
 
1029
624
  it('should throw error for missing body_of_work_id', async () => {
1030
- const supabase = createMockSupabase();
1031
- const ctx = createMockContext(supabase);
1032
-
625
+ const ctx = createMockContext();
1033
626
  await expect(getNextBodyOfWorkTask({}, ctx)).rejects.toThrow(ValidationError);
1034
627
  });
1035
628
 
1036
- it('should throw error when body of work not found', async () => {
1037
- const supabase = createMockSupabase({
1038
- selectResult: { data: null, error: { message: 'Not found' } },
629
+ it('should throw error when API returns error', async () => {
630
+ const ctx = createMockContext();
631
+ mockApiClient.proxy.mockResolvedValue({
632
+ ok: false,
633
+ error: 'Body of work not found',
1039
634
  });
1040
- const ctx = createMockContext(supabase);
1041
635
 
1042
636
  await expect(
1043
637
  getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx)
1044
- ).rejects.toThrow('Body of work not found');
638
+ ).rejects.toThrow('Failed to get next body of work task');
1045
639
  });
1046
640
 
1047
- it('should return null when body of work is not active', async () => {
1048
- const supabase = createMockSupabase({
1049
- selectResult: { data: { status: 'draft', title: 'Test' }, error: null },
641
+ it('should return null when no tasks available', async () => {
642
+ const ctx = createMockContext();
643
+ mockApiClient.proxy.mockResolvedValue({
644
+ ok: true,
645
+ data: {
646
+ next_task: null,
647
+ message: 'No available tasks',
648
+ },
1050
649
  });
1051
- const ctx = createMockContext(supabase);
1052
650
 
1053
651
  const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
1054
652
 
1055
653
  expect(result.result).toMatchObject({
1056
654
  next_task: null,
1057
- message: 'Body of work is draft, not active',
1058
- });
1059
- });
1060
-
1061
- it('should return next pending task from pre phase first', async () => {
1062
- const mockBow = { status: 'active', title: 'Sprint' };
1063
- const mockTaskLinks = [
1064
- {
1065
- task_id: 't1',
1066
- phase: 'pre',
1067
- order_index: 0,
1068
- tasks: { id: 't1', title: 'Setup', status: 'pending', priority: 1, claimed_by_session_id: null },
1069
- },
1070
- {
1071
- task_id: 't2',
1072
- phase: 'core',
1073
- order_index: 0,
1074
- tasks: { id: 't2', title: 'Feature', status: 'pending', priority: 2, claimed_by_session_id: null },
655
+ message: 'No available tasks',
656
+ });
657
+ });
658
+
659
+ it('should return next pending task', async () => {
660
+ const ctx = createMockContext();
661
+ mockApiClient.proxy.mockResolvedValue({
662
+ ok: true,
663
+ data: {
664
+ next_task: {
665
+ id: 't1',
666
+ title: 'Setup',
667
+ phase: 'pre',
668
+ status: 'pending',
669
+ },
670
+ message: 'Task available',
1075
671
  },
1076
- ];
1077
-
1078
- const supabase = createMockSupabase();
1079
- let callCount = 0;
1080
-
1081
- vi.mocked(supabase.from).mockImplementation((table: string) => {
1082
- callCount++;
1083
- if (table === 'bodies_of_work') {
1084
- return {
1085
- ...supabase,
1086
- select: vi.fn().mockReturnValue({
1087
- ...supabase,
1088
- eq: vi.fn().mockReturnValue({
1089
- ...supabase,
1090
- single: vi.fn().mockResolvedValue({ data: mockBow, error: null }),
1091
- }),
1092
- }),
1093
- } as unknown as ReturnType<SupabaseClient['from']>;
1094
- }
1095
- if (table === 'body_of_work_tasks') {
1096
- return {
1097
- ...supabase,
1098
- select: vi.fn().mockReturnValue({
1099
- ...supabase,
1100
- eq: vi.fn().mockReturnValue({
1101
- ...supabase,
1102
- order: vi.fn().mockReturnValue({
1103
- then: (resolve: (val: unknown) => void) =>
1104
- Promise.resolve({ data: mockTaskLinks, error: null }).then(resolve),
1105
- }),
1106
- }),
1107
- }),
1108
- } as unknown as ReturnType<SupabaseClient['from']>;
1109
- }
1110
- if (table === 'body_of_work_task_dependencies') {
1111
- return {
1112
- ...supabase,
1113
- select: vi.fn().mockReturnValue({
1114
- ...supabase,
1115
- eq: vi.fn().mockReturnValue({
1116
- then: (resolve: (val: unknown) => void) =>
1117
- Promise.resolve({ data: [], error: null }).then(resolve),
1118
- }),
1119
- }),
1120
- } as unknown as ReturnType<SupabaseClient['from']>;
1121
- }
1122
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
1123
672
  });
1124
673
 
1125
- const ctx = createMockContext(supabase);
1126
-
1127
674
  const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
1128
675
 
1129
676
  expect(result.result).toHaveProperty('next_task');
@@ -1131,146 +678,4 @@ describe('getNextBodyOfWorkTask', () => {
1131
678
  expect(nextTask.id).toBe('t1');
1132
679
  expect(nextTask.phase).toBe('pre');
1133
680
  });
1134
-
1135
- it('should skip tasks claimed by other sessions', async () => {
1136
- const mockBow = { status: 'active', title: 'Sprint' };
1137
- const mockTaskLinks = [
1138
- {
1139
- task_id: 't1',
1140
- phase: 'pre',
1141
- order_index: 0,
1142
- tasks: { id: 't1', title: 'Claimed', status: 'pending', priority: 1, claimed_by_session_id: 'other-session' },
1143
- },
1144
- {
1145
- task_id: 't2',
1146
- phase: 'pre',
1147
- order_index: 1,
1148
- tasks: { id: 't2', title: 'Available', status: 'pending', priority: 2, claimed_by_session_id: null },
1149
- },
1150
- ];
1151
-
1152
- const supabase = createMockSupabase();
1153
-
1154
- vi.mocked(supabase.from).mockImplementation((table: string) => {
1155
- if (table === 'bodies_of_work') {
1156
- return {
1157
- ...supabase,
1158
- select: vi.fn().mockReturnValue({
1159
- ...supabase,
1160
- eq: vi.fn().mockReturnValue({
1161
- ...supabase,
1162
- single: vi.fn().mockResolvedValue({ data: mockBow, error: null }),
1163
- }),
1164
- }),
1165
- } as unknown as ReturnType<SupabaseClient['from']>;
1166
- }
1167
- if (table === 'body_of_work_tasks') {
1168
- return {
1169
- ...supabase,
1170
- select: vi.fn().mockReturnValue({
1171
- ...supabase,
1172
- eq: vi.fn().mockReturnValue({
1173
- ...supabase,
1174
- order: vi.fn().mockReturnValue({
1175
- then: (resolve: (val: unknown) => void) =>
1176
- Promise.resolve({ data: mockTaskLinks, error: null }).then(resolve),
1177
- }),
1178
- }),
1179
- }),
1180
- } as unknown as ReturnType<SupabaseClient['from']>;
1181
- }
1182
- if (table === 'body_of_work_task_dependencies') {
1183
- return {
1184
- ...supabase,
1185
- select: vi.fn().mockReturnValue({
1186
- ...supabase,
1187
- eq: vi.fn().mockReturnValue({
1188
- then: (resolve: (val: unknown) => void) =>
1189
- Promise.resolve({ data: [], error: null }).then(resolve),
1190
- }),
1191
- }),
1192
- } as unknown as ReturnType<SupabaseClient['from']>;
1193
- }
1194
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
1195
- });
1196
-
1197
- const ctx = createMockContext(supabase);
1198
-
1199
- const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
1200
-
1201
- const nextTask = (result.result as { next_task: { id: string } }).next_task;
1202
- expect(nextTask.id).toBe('t2');
1203
- });
1204
-
1205
- it('should return null when all tasks are completed or in progress', async () => {
1206
- const mockBow = { status: 'active', title: 'Sprint' };
1207
- const mockTaskLinks = [
1208
- {
1209
- task_id: 't1',
1210
- phase: 'core',
1211
- order_index: 0,
1212
- tasks: { id: 't1', title: 'Done', status: 'completed', priority: 1, claimed_by_session_id: null },
1213
- },
1214
- {
1215
- task_id: 't2',
1216
- phase: 'core',
1217
- order_index: 1,
1218
- tasks: { id: 't2', title: 'Working', status: 'in_progress', priority: 2, claimed_by_session_id: null },
1219
- },
1220
- ];
1221
-
1222
- const supabase = createMockSupabase();
1223
-
1224
- vi.mocked(supabase.from).mockImplementation((table: string) => {
1225
- if (table === 'bodies_of_work') {
1226
- return {
1227
- ...supabase,
1228
- select: vi.fn().mockReturnValue({
1229
- ...supabase,
1230
- eq: vi.fn().mockReturnValue({
1231
- ...supabase,
1232
- single: vi.fn().mockResolvedValue({ data: mockBow, error: null }),
1233
- }),
1234
- }),
1235
- } as unknown as ReturnType<SupabaseClient['from']>;
1236
- }
1237
- if (table === 'body_of_work_tasks') {
1238
- return {
1239
- ...supabase,
1240
- select: vi.fn().mockReturnValue({
1241
- ...supabase,
1242
- eq: vi.fn().mockReturnValue({
1243
- ...supabase,
1244
- order: vi.fn().mockReturnValue({
1245
- then: (resolve: (val: unknown) => void) =>
1246
- Promise.resolve({ data: mockTaskLinks, error: null }).then(resolve),
1247
- }),
1248
- }),
1249
- }),
1250
- } as unknown as ReturnType<SupabaseClient['from']>;
1251
- }
1252
- if (table === 'body_of_work_task_dependencies') {
1253
- return {
1254
- ...supabase,
1255
- select: vi.fn().mockReturnValue({
1256
- ...supabase,
1257
- eq: vi.fn().mockReturnValue({
1258
- then: (resolve: (val: unknown) => void) =>
1259
- Promise.resolve({ data: [], error: null }).then(resolve),
1260
- }),
1261
- }),
1262
- } as unknown as ReturnType<SupabaseClient['from']>;
1263
- }
1264
- return supabase as unknown as ReturnType<SupabaseClient['from']>;
1265
- });
1266
-
1267
- const ctx = createMockContext(supabase);
1268
-
1269
- const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
1270
-
1271
- expect(result.result).toMatchObject({
1272
- next_task: null,
1273
- message: expect.stringContaining('No available tasks'),
1274
- });
1275
- });
1276
681
  });