@vibescope/mcp-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,1276 @@
|
|
|
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
|
+
import {
|
|
5
|
+
createBodyOfWork,
|
|
6
|
+
updateBodyOfWork,
|
|
7
|
+
getBodyOfWork,
|
|
8
|
+
getBodiesOfWork,
|
|
9
|
+
deleteBodyOfWork,
|
|
10
|
+
addTaskToBodyOfWork,
|
|
11
|
+
removeTaskFromBodyOfWork,
|
|
12
|
+
activateBodyOfWork,
|
|
13
|
+
addTaskDependency,
|
|
14
|
+
removeTaskDependency,
|
|
15
|
+
getTaskDependencies,
|
|
16
|
+
getNextBodyOfWorkTask,
|
|
17
|
+
} from './bodies-of-work.js';
|
|
18
|
+
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
|
+
}
|
|
136
|
+
|
|
137
|
+
const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
138
|
+
const VALID_UUID_2 = '223e4567-e89b-12d3-a456-426614174001';
|
|
139
|
+
const VALID_UUID_3 = '323e4567-e89b-12d3-a456-426614174002';
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// createBodyOfWork Tests
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
describe('createBodyOfWork', () => {
|
|
146
|
+
beforeEach(() => vi.clearAllMocks());
|
|
147
|
+
|
|
148
|
+
it('should throw error for missing project_id', async () => {
|
|
149
|
+
const supabase = createMockSupabase();
|
|
150
|
+
const ctx = createMockContext(supabase);
|
|
151
|
+
|
|
152
|
+
await expect(createBodyOfWork({ title: 'Test' }, ctx)).rejects.toThrow(ValidationError);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
156
|
+
const supabase = createMockSupabase();
|
|
157
|
+
const ctx = createMockContext(supabase);
|
|
158
|
+
|
|
159
|
+
await expect(
|
|
160
|
+
createBodyOfWork({ project_id: 'invalid', title: 'Test' }, ctx)
|
|
161
|
+
).rejects.toThrow(ValidationError);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should throw error for missing title', async () => {
|
|
165
|
+
const supabase = createMockSupabase();
|
|
166
|
+
const ctx = createMockContext(supabase);
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
createBodyOfWork({ project_id: VALID_UUID }, ctx)
|
|
170
|
+
).rejects.toThrow(ValidationError);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should create body of work with required fields', async () => {
|
|
174
|
+
const supabase = createMockSupabase({
|
|
175
|
+
insertResult: { data: { id: 'bow-1' }, error: null },
|
|
176
|
+
});
|
|
177
|
+
const ctx = createMockContext(supabase);
|
|
178
|
+
|
|
179
|
+
const result = await createBodyOfWork(
|
|
180
|
+
{ project_id: VALID_UUID, title: 'Sprint 1' },
|
|
181
|
+
ctx
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(result.result).toMatchObject({
|
|
185
|
+
success: true,
|
|
186
|
+
body_of_work_id: 'bow-1',
|
|
187
|
+
title: 'Sprint 1',
|
|
188
|
+
status: 'draft',
|
|
189
|
+
});
|
|
190
|
+
expect(supabase.from).toHaveBeenCalledWith('bodies_of_work');
|
|
191
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
project_id: VALID_UUID,
|
|
194
|
+
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
|
+
})
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should create body of work with all optional fields', async () => {
|
|
204
|
+
const supabase = createMockSupabase({
|
|
205
|
+
insertResult: { data: { id: 'bow-2' }, error: null },
|
|
206
|
+
});
|
|
207
|
+
const ctx = createMockContext(supabase);
|
|
208
|
+
|
|
209
|
+
await createBodyOfWork(
|
|
210
|
+
{
|
|
211
|
+
project_id: VALID_UUID,
|
|
212
|
+
title: 'Release 2.0',
|
|
213
|
+
description: 'Major release',
|
|
214
|
+
auto_deploy_on_completion: true,
|
|
215
|
+
deploy_environment: 'staging',
|
|
216
|
+
deploy_version_bump: 'major',
|
|
217
|
+
deploy_trigger: 'all_completed',
|
|
218
|
+
},
|
|
219
|
+
ctx
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
description: 'Major release',
|
|
225
|
+
auto_deploy_on_completion: true,
|
|
226
|
+
deploy_environment: 'staging',
|
|
227
|
+
deploy_version_bump: 'major',
|
|
228
|
+
deploy_trigger: 'all_completed',
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should throw error when database insert fails', async () => {
|
|
234
|
+
const supabase = createMockSupabase({
|
|
235
|
+
insertResult: { data: null, error: { message: 'Insert failed' } },
|
|
236
|
+
});
|
|
237
|
+
const ctx = createMockContext(supabase);
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
createBodyOfWork({ project_id: VALID_UUID, title: 'Test' }, ctx)
|
|
241
|
+
).rejects.toThrow('Failed to create body of work');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// updateBodyOfWork Tests
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
describe('updateBodyOfWork', () => {
|
|
250
|
+
beforeEach(() => vi.clearAllMocks());
|
|
251
|
+
|
|
252
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
253
|
+
const supabase = createMockSupabase();
|
|
254
|
+
const ctx = createMockContext(supabase);
|
|
255
|
+
|
|
256
|
+
await expect(updateBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should return success when no updates provided', async () => {
|
|
260
|
+
const supabase = createMockSupabase();
|
|
261
|
+
const ctx = createMockContext(supabase);
|
|
262
|
+
|
|
263
|
+
const result = await updateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
|
|
264
|
+
|
|
265
|
+
expect(result.result).toMatchObject({
|
|
266
|
+
success: true,
|
|
267
|
+
message: 'No updates provided',
|
|
268
|
+
});
|
|
269
|
+
expect(supabase.update).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should update title', async () => {
|
|
273
|
+
const supabase = createMockSupabase({
|
|
274
|
+
updateResult: { data: null, error: null },
|
|
275
|
+
});
|
|
276
|
+
const ctx = createMockContext(supabase);
|
|
277
|
+
|
|
278
|
+
const result = await updateBodyOfWork(
|
|
279
|
+
{ body_of_work_id: VALID_UUID, title: 'New Title' },
|
|
280
|
+
ctx
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(result.result).toMatchObject({ success: true, body_of_work_id: VALID_UUID });
|
|
284
|
+
expect(supabase.update).toHaveBeenCalledWith({ title: 'New Title' });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should update multiple fields', async () => {
|
|
288
|
+
const supabase = createMockSupabase({
|
|
289
|
+
updateResult: { data: null, error: null },
|
|
290
|
+
});
|
|
291
|
+
const ctx = createMockContext(supabase);
|
|
292
|
+
|
|
293
|
+
await updateBodyOfWork(
|
|
294
|
+
{
|
|
295
|
+
body_of_work_id: VALID_UUID,
|
|
296
|
+
title: 'Updated',
|
|
297
|
+
description: 'New desc',
|
|
298
|
+
auto_deploy_on_completion: true,
|
|
299
|
+
},
|
|
300
|
+
ctx
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(supabase.update).toHaveBeenCalledWith({
|
|
304
|
+
title: 'Updated',
|
|
305
|
+
description: 'New desc',
|
|
306
|
+
auto_deploy_on_completion: true,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// getBodyOfWork Tests
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
describe('getBodyOfWork', () => {
|
|
316
|
+
beforeEach(() => vi.clearAllMocks());
|
|
317
|
+
|
|
318
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
319
|
+
const supabase = createMockSupabase();
|
|
320
|
+
const ctx = createMockContext(supabase);
|
|
321
|
+
|
|
322
|
+
await expect(getBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should throw error when body of work not found', async () => {
|
|
326
|
+
const supabase = createMockSupabase({
|
|
327
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
328
|
+
});
|
|
329
|
+
const ctx = createMockContext(supabase);
|
|
330
|
+
|
|
331
|
+
await expect(
|
|
332
|
+
getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
|
|
333
|
+
).rejects.toThrow('Body of work not found');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
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']>;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const ctx = createMockContext(supabase);
|
|
388
|
+
|
|
389
|
+
const result = await getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
|
|
390
|
+
|
|
391
|
+
expect(result.result).toHaveProperty('pre_tasks');
|
|
392
|
+
expect(result.result).toHaveProperty('core_tasks');
|
|
393
|
+
expect(result.result).toHaveProperty('post_tasks');
|
|
394
|
+
expect(result.result).toHaveProperty('total_tasks');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// getBodiesOfWork Tests
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
describe('getBodiesOfWork', () => {
|
|
403
|
+
beforeEach(() => vi.clearAllMocks());
|
|
404
|
+
|
|
405
|
+
it('should throw error for missing project_id', async () => {
|
|
406
|
+
const supabase = createMockSupabase();
|
|
407
|
+
const ctx = createMockContext(supabase);
|
|
408
|
+
|
|
409
|
+
await expect(getBodiesOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
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);
|
|
430
|
+
|
|
431
|
+
const result = await getBodiesOfWork({ project_id: VALID_UUID }, ctx);
|
|
432
|
+
|
|
433
|
+
expect(result.result).toMatchObject({ bodies_of_work: [] });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should filter by status when provided', async () => {
|
|
437
|
+
const supabase = createMockSupabase();
|
|
438
|
+
const ctx = createMockContext(supabase);
|
|
439
|
+
|
|
440
|
+
await getBodiesOfWork({ project_id: VALID_UUID, status: 'active' }, ctx);
|
|
441
|
+
|
|
442
|
+
expect(supabase.eq).toHaveBeenCalledWith('project_id', VALID_UUID);
|
|
443
|
+
expect(supabase.eq).toHaveBeenCalledWith('status', 'active');
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// deleteBodyOfWork Tests
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
describe('deleteBodyOfWork', () => {
|
|
452
|
+
beforeEach(() => vi.clearAllMocks());
|
|
453
|
+
|
|
454
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
455
|
+
const supabase = createMockSupabase();
|
|
456
|
+
const ctx = createMockContext(supabase);
|
|
457
|
+
|
|
458
|
+
await expect(deleteBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should delete body of work successfully', async () => {
|
|
462
|
+
const supabase = createMockSupabase({
|
|
463
|
+
deleteResult: { data: null, error: null },
|
|
464
|
+
});
|
|
465
|
+
const ctx = createMockContext(supabase);
|
|
466
|
+
|
|
467
|
+
const result = await deleteBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
|
|
468
|
+
|
|
469
|
+
expect(result.result).toMatchObject({
|
|
470
|
+
success: true,
|
|
471
|
+
message: 'Body of work deleted. Tasks are preserved.',
|
|
472
|
+
});
|
|
473
|
+
expect(supabase.from).toHaveBeenCalledWith('bodies_of_work');
|
|
474
|
+
expect(supabase.delete).toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ============================================================================
|
|
479
|
+
// addTaskToBodyOfWork Tests
|
|
480
|
+
// ============================================================================
|
|
481
|
+
|
|
482
|
+
describe('addTaskToBodyOfWork', () => {
|
|
483
|
+
beforeEach(() => vi.clearAllMocks());
|
|
484
|
+
|
|
485
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
486
|
+
const supabase = createMockSupabase();
|
|
487
|
+
const ctx = createMockContext(supabase);
|
|
488
|
+
|
|
489
|
+
await expect(
|
|
490
|
+
addTaskToBodyOfWork({ task_id: VALID_UUID }, ctx)
|
|
491
|
+
).rejects.toThrow(ValidationError);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should throw error for missing task_id', async () => {
|
|
495
|
+
const supabase = createMockSupabase();
|
|
496
|
+
const ctx = createMockContext(supabase);
|
|
497
|
+
|
|
498
|
+
await expect(
|
|
499
|
+
addTaskToBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
|
|
500
|
+
).rejects.toThrow(ValidationError);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should throw error when body of work not found', async () => {
|
|
504
|
+
const supabase = createMockSupabase({
|
|
505
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
506
|
+
});
|
|
507
|
+
const ctx = createMockContext(supabase);
|
|
508
|
+
|
|
509
|
+
await expect(
|
|
510
|
+
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');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
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']>;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const ctx = createMockContext(supabase);
|
|
637
|
+
|
|
638
|
+
const result = await addTaskToBodyOfWork(
|
|
639
|
+
{ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 },
|
|
640
|
+
ctx
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
expect(result.result).toMatchObject({
|
|
644
|
+
success: true,
|
|
645
|
+
body_of_work_id: VALID_UUID,
|
|
646
|
+
task_id: VALID_UUID_2,
|
|
647
|
+
phase: 'core',
|
|
648
|
+
order_index: 0,
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// ============================================================================
|
|
654
|
+
// removeTaskFromBodyOfWork Tests
|
|
655
|
+
// ============================================================================
|
|
656
|
+
|
|
657
|
+
describe('removeTaskFromBodyOfWork', () => {
|
|
658
|
+
beforeEach(() => vi.clearAllMocks());
|
|
659
|
+
|
|
660
|
+
it('should throw error for missing task_id', async () => {
|
|
661
|
+
const supabase = createMockSupabase();
|
|
662
|
+
const ctx = createMockContext(supabase);
|
|
663
|
+
|
|
664
|
+
await expect(removeTaskFromBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
665
|
+
});
|
|
666
|
+
|
|
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 },
|
|
670
|
+
});
|
|
671
|
+
const ctx = createMockContext(supabase);
|
|
672
|
+
|
|
673
|
+
const result = await removeTaskFromBodyOfWork({ task_id: VALID_UUID }, ctx);
|
|
674
|
+
|
|
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');
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// ============================================================================
|
|
723
|
+
// activateBodyOfWork Tests
|
|
724
|
+
// ============================================================================
|
|
725
|
+
|
|
726
|
+
describe('activateBodyOfWork', () => {
|
|
727
|
+
beforeEach(() => vi.clearAllMocks());
|
|
728
|
+
|
|
729
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
730
|
+
const supabase = createMockSupabase();
|
|
731
|
+
const ctx = createMockContext(supabase);
|
|
732
|
+
|
|
733
|
+
await expect(activateBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
|
|
734
|
+
});
|
|
735
|
+
|
|
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']>;
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const ctx = createMockContext(supabase);
|
|
792
|
+
|
|
793
|
+
await expect(
|
|
794
|
+
activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
|
|
795
|
+
).rejects.toThrow('Cannot activate body of work with no tasks');
|
|
796
|
+
});
|
|
797
|
+
|
|
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']>;
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const ctx = createMockContext(supabase);
|
|
844
|
+
|
|
845
|
+
const result = await activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
|
|
846
|
+
|
|
847
|
+
expect(result.result).toMatchObject({
|
|
848
|
+
success: true,
|
|
849
|
+
body_of_work_id: VALID_UUID,
|
|
850
|
+
title: 'Sprint 1',
|
|
851
|
+
status: 'active',
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// ============================================================================
|
|
857
|
+
// addTaskDependency Tests
|
|
858
|
+
// ============================================================================
|
|
859
|
+
|
|
860
|
+
describe('addTaskDependency', () => {
|
|
861
|
+
beforeEach(() => vi.clearAllMocks());
|
|
862
|
+
|
|
863
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
864
|
+
const supabase = createMockSupabase();
|
|
865
|
+
const ctx = createMockContext(supabase);
|
|
866
|
+
|
|
867
|
+
await expect(
|
|
868
|
+
addTaskDependency({ task_id: VALID_UUID, depends_on_task_id: VALID_UUID_2 }, ctx)
|
|
869
|
+
).rejects.toThrow(ValidationError);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('should throw error when task depends on itself', async () => {
|
|
873
|
+
const supabase = createMockSupabase();
|
|
874
|
+
const ctx = createMockContext(supabase);
|
|
875
|
+
|
|
876
|
+
await expect(
|
|
877
|
+
addTaskDependency({
|
|
878
|
+
body_of_work_id: VALID_UUID,
|
|
879
|
+
task_id: VALID_UUID_2,
|
|
880
|
+
depends_on_task_id: VALID_UUID_2,
|
|
881
|
+
}, ctx)
|
|
882
|
+
).rejects.toThrow('A task cannot depend on itself');
|
|
883
|
+
});
|
|
884
|
+
|
|
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,
|
|
907
|
+
task_id: VALID_UUID_2,
|
|
908
|
+
depends_on_task_id: VALID_UUID_3,
|
|
909
|
+
}, ctx)
|
|
910
|
+
).rejects.toThrow('Both tasks must belong to the specified body of work');
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// ============================================================================
|
|
915
|
+
// removeTaskDependency Tests
|
|
916
|
+
// ============================================================================
|
|
917
|
+
|
|
918
|
+
describe('removeTaskDependency', () => {
|
|
919
|
+
beforeEach(() => vi.clearAllMocks());
|
|
920
|
+
|
|
921
|
+
it('should throw error for missing task_id', async () => {
|
|
922
|
+
const supabase = createMockSupabase();
|
|
923
|
+
const ctx = createMockContext(supabase);
|
|
924
|
+
|
|
925
|
+
await expect(
|
|
926
|
+
removeTaskDependency({ depends_on_task_id: VALID_UUID }, ctx)
|
|
927
|
+
).rejects.toThrow(ValidationError);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('should throw error for missing depends_on_task_id', async () => {
|
|
931
|
+
const supabase = createMockSupabase();
|
|
932
|
+
const ctx = createMockContext(supabase);
|
|
933
|
+
|
|
934
|
+
await expect(
|
|
935
|
+
removeTaskDependency({ task_id: VALID_UUID }, ctx)
|
|
936
|
+
).rejects.toThrow(ValidationError);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('should remove dependency successfully', async () => {
|
|
940
|
+
const supabase = createMockSupabase({
|
|
941
|
+
deleteResult: { data: null, error: null },
|
|
942
|
+
});
|
|
943
|
+
const ctx = createMockContext(supabase);
|
|
944
|
+
|
|
945
|
+
const result = await removeTaskDependency(
|
|
946
|
+
{ task_id: VALID_UUID, depends_on_task_id: VALID_UUID_2 },
|
|
947
|
+
ctx
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
expect(result.result).toMatchObject({
|
|
951
|
+
success: true,
|
|
952
|
+
task_id: VALID_UUID,
|
|
953
|
+
depends_on_task_id: VALID_UUID_2,
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// ============================================================================
|
|
959
|
+
// getTaskDependencies Tests
|
|
960
|
+
// ============================================================================
|
|
961
|
+
|
|
962
|
+
describe('getTaskDependencies', () => {
|
|
963
|
+
beforeEach(() => vi.clearAllMocks());
|
|
964
|
+
|
|
965
|
+
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
|
+
|
|
969
|
+
await expect(getTaskDependencies({}, ctx)).rejects.toThrow(
|
|
970
|
+
'Either body_of_work_id or task_id is required'
|
|
971
|
+
);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('should return dependencies filtered by body_of_work_id', async () => {
|
|
975
|
+
const mockDeps = [
|
|
976
|
+
{ id: 'd1', task_id: 't1', depends_on_task_id: 't2', created_at: '2026-01-14' },
|
|
977
|
+
];
|
|
978
|
+
|
|
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);
|
|
993
|
+
|
|
994
|
+
const result = await getTaskDependencies({ body_of_work_id: VALID_UUID }, ctx);
|
|
995
|
+
|
|
996
|
+
expect(result.result).toMatchObject({ dependencies: mockDeps });
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
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);
|
|
1014
|
+
|
|
1015
|
+
const result = await getTaskDependencies({ task_id: VALID_UUID }, ctx);
|
|
1016
|
+
|
|
1017
|
+
expect(result.result).toMatchObject({ dependencies: [] });
|
|
1018
|
+
expect(supabase.from).toHaveBeenCalledWith('body_of_work_task_dependencies');
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
// getNextBodyOfWorkTask Tests
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
|
|
1026
|
+
describe('getNextBodyOfWorkTask', () => {
|
|
1027
|
+
beforeEach(() => vi.clearAllMocks());
|
|
1028
|
+
|
|
1029
|
+
it('should throw error for missing body_of_work_id', async () => {
|
|
1030
|
+
const supabase = createMockSupabase();
|
|
1031
|
+
const ctx = createMockContext(supabase);
|
|
1032
|
+
|
|
1033
|
+
await expect(getNextBodyOfWorkTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('should throw error when body of work not found', async () => {
|
|
1037
|
+
const supabase = createMockSupabase({
|
|
1038
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
1039
|
+
});
|
|
1040
|
+
const ctx = createMockContext(supabase);
|
|
1041
|
+
|
|
1042
|
+
await expect(
|
|
1043
|
+
getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx)
|
|
1044
|
+
).rejects.toThrow('Body of work not found');
|
|
1045
|
+
});
|
|
1046
|
+
|
|
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 },
|
|
1050
|
+
});
|
|
1051
|
+
const ctx = createMockContext(supabase);
|
|
1052
|
+
|
|
1053
|
+
const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
|
|
1054
|
+
|
|
1055
|
+
expect(result.result).toMatchObject({
|
|
1056
|
+
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 },
|
|
1075
|
+
},
|
|
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
|
+
});
|
|
1124
|
+
|
|
1125
|
+
const ctx = createMockContext(supabase);
|
|
1126
|
+
|
|
1127
|
+
const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
|
|
1128
|
+
|
|
1129
|
+
expect(result.result).toHaveProperty('next_task');
|
|
1130
|
+
const nextTask = (result.result as { next_task: { id: string; phase: string } }).next_task;
|
|
1131
|
+
expect(nextTask.id).toBe('t1');
|
|
1132
|
+
expect(nextTask.phase).toBe('pre');
|
|
1133
|
+
});
|
|
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
|
+
});
|