@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.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1114 -0
- package/dist/api-client.js +698 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +106 -476
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +194 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
175
|
-
|
|
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(
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
205
|
-
|
|
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(
|
|
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
|
|
234
|
-
const
|
|
235
|
-
|
|
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
|
|
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
|
|
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(
|
|
145
|
+
expect(mockApiClient.proxy).not.toHaveBeenCalled();
|
|
270
146
|
});
|
|
271
147
|
|
|
272
148
|
it('should update title', async () => {
|
|
273
|
-
const
|
|
274
|
-
|
|
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(
|
|
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
|
|
289
|
-
|
|
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(
|
|
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
|
|
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
|
|
327
|
-
|
|
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('
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
438
|
-
|
|
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(
|
|
443
|
-
|
|
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
|
|
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
|
|
463
|
-
|
|
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(
|
|
474
|
-
|
|
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
|
|
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
|
|
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
|
|
504
|
-
const
|
|
505
|
-
|
|
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('
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
|
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
|
|
668
|
-
const
|
|
669
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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
|
|
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
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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('
|
|
447
|
+
).rejects.toThrow('Failed to activate body of work');
|
|
796
448
|
});
|
|
797
449
|
|
|
798
|
-
it('should activate body of work
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
|
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
|
|
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
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
},
|
|
910
|
-
)
|
|
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
|
|
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
|
|
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
|
|
941
|
-
|
|
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
|
|
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
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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(
|
|
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
|
|
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
|
|
1037
|
-
const
|
|
1038
|
-
|
|
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('
|
|
638
|
+
).rejects.toThrow('Failed to get next body of work task');
|
|
1045
639
|
});
|
|
1046
640
|
|
|
1047
|
-
it('should return null when
|
|
1048
|
-
const
|
|
1049
|
-
|
|
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: '
|
|
1058
|
-
});
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
it('should return next pending task
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
});
|