@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,880 @@
|
|
|
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
|
+
getTasksAwaitingValidation,
|
|
6
|
+
claimValidation,
|
|
7
|
+
validateTask,
|
|
8
|
+
} from './validation.js';
|
|
9
|
+
import { ValidationError } from '../validators.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Test Utilities
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
function createMockSupabase(overrides: {
|
|
16
|
+
selectResult?: { data: unknown; error: unknown };
|
|
17
|
+
insertResult?: { data: unknown; error: unknown };
|
|
18
|
+
updateResult?: { data: unknown; error: unknown };
|
|
19
|
+
} = {}) {
|
|
20
|
+
const defaultResult = { data: null, error: null };
|
|
21
|
+
let currentOperation = 'select';
|
|
22
|
+
let insertThenSelect = false;
|
|
23
|
+
|
|
24
|
+
const mock = {
|
|
25
|
+
from: vi.fn().mockReturnThis(),
|
|
26
|
+
select: vi.fn(() => {
|
|
27
|
+
if (currentOperation === 'insert') {
|
|
28
|
+
insertThenSelect = true;
|
|
29
|
+
} else {
|
|
30
|
+
currentOperation = 'select';
|
|
31
|
+
insertThenSelect = false;
|
|
32
|
+
}
|
|
33
|
+
return mock;
|
|
34
|
+
}),
|
|
35
|
+
insert: vi.fn(() => {
|
|
36
|
+
currentOperation = 'insert';
|
|
37
|
+
insertThenSelect = false;
|
|
38
|
+
return mock;
|
|
39
|
+
}),
|
|
40
|
+
update: vi.fn(() => {
|
|
41
|
+
currentOperation = 'update';
|
|
42
|
+
insertThenSelect = false;
|
|
43
|
+
return mock;
|
|
44
|
+
}),
|
|
45
|
+
delete: vi.fn(() => {
|
|
46
|
+
currentOperation = 'delete';
|
|
47
|
+
insertThenSelect = false;
|
|
48
|
+
return mock;
|
|
49
|
+
}),
|
|
50
|
+
eq: vi.fn().mockReturnThis(),
|
|
51
|
+
neq: vi.fn().mockReturnThis(),
|
|
52
|
+
in: vi.fn().mockReturnThis(),
|
|
53
|
+
is: vi.fn().mockReturnThis(),
|
|
54
|
+
not: vi.fn().mockReturnThis(),
|
|
55
|
+
or: vi.fn().mockReturnThis(),
|
|
56
|
+
gte: vi.fn().mockReturnThis(),
|
|
57
|
+
lte: vi.fn().mockReturnThis(),
|
|
58
|
+
lt: vi.fn().mockReturnThis(),
|
|
59
|
+
order: vi.fn().mockReturnThis(),
|
|
60
|
+
limit: vi.fn().mockReturnThis(),
|
|
61
|
+
single: vi.fn(() => {
|
|
62
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
63
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult);
|
|
64
|
+
}
|
|
65
|
+
if (currentOperation === 'select') {
|
|
66
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
67
|
+
}
|
|
68
|
+
if (currentOperation === 'update') {
|
|
69
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve(defaultResult);
|
|
72
|
+
}),
|
|
73
|
+
maybeSingle: vi.fn(() => {
|
|
74
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
75
|
+
}),
|
|
76
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
77
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
78
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
79
|
+
}
|
|
80
|
+
if (currentOperation === 'select') {
|
|
81
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
82
|
+
}
|
|
83
|
+
if (currentOperation === 'update') {
|
|
84
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return mock as unknown as SupabaseClient;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createMockContext(
|
|
94
|
+
supabase: SupabaseClient,
|
|
95
|
+
options: { sessionId?: string | null } = {}
|
|
96
|
+
): HandlerContext {
|
|
97
|
+
const defaultTokenUsage: TokenUsage = {
|
|
98
|
+
callCount: 5,
|
|
99
|
+
totalTokens: 2500,
|
|
100
|
+
byTool: {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
supabase,
|
|
107
|
+
auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
|
|
108
|
+
session: {
|
|
109
|
+
instanceId: 'instance-abc',
|
|
110
|
+
currentSessionId: sessionId,
|
|
111
|
+
currentPersona: 'Wave',
|
|
112
|
+
tokenUsage: defaultTokenUsage,
|
|
113
|
+
},
|
|
114
|
+
updateSession: vi.fn(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// getTasksAwaitingValidation Tests
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
describe('getTasksAwaitingValidation', () => {
|
|
123
|
+
beforeEach(() => vi.clearAllMocks());
|
|
124
|
+
|
|
125
|
+
it('should throw error for missing project_id', async () => {
|
|
126
|
+
const supabase = createMockSupabase();
|
|
127
|
+
const ctx = createMockContext(supabase);
|
|
128
|
+
|
|
129
|
+
await expect(getTasksAwaitingValidation({}, ctx)).rejects.toThrow(ValidationError);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
133
|
+
const supabase = createMockSupabase();
|
|
134
|
+
const ctx = createMockContext(supabase);
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
getTasksAwaitingValidation({ project_id: 'invalid' }, ctx)
|
|
138
|
+
).rejects.toThrow(ValidationError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return empty list when no tasks awaiting validation', async () => {
|
|
142
|
+
const supabase = createMockSupabase({
|
|
143
|
+
selectResult: { data: [], error: null },
|
|
144
|
+
});
|
|
145
|
+
const ctx = createMockContext(supabase);
|
|
146
|
+
|
|
147
|
+
// Override to return array result
|
|
148
|
+
vi.mocked(supabase.from('').select).mockReturnValue({
|
|
149
|
+
...supabase,
|
|
150
|
+
then: (resolve: (val: unknown) => void) =>
|
|
151
|
+
Promise.resolve({ data: [], error: null }).then(resolve),
|
|
152
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
153
|
+
|
|
154
|
+
const result = await getTasksAwaitingValidation(
|
|
155
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
156
|
+
ctx
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(result.result).toHaveProperty('tasks');
|
|
160
|
+
expect((result.result as { tasks: unknown[] }).tasks).toEqual([]);
|
|
161
|
+
expect((result.result as { hint?: string }).hint).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return tasks with review status info', async () => {
|
|
165
|
+
const mockTasks = [
|
|
166
|
+
{
|
|
167
|
+
id: 'task-1',
|
|
168
|
+
title: 'Implement feature A',
|
|
169
|
+
completed_at: '2025-01-14T10:00:00Z',
|
|
170
|
+
completed_by_session_id: 'other-session',
|
|
171
|
+
reviewing_by_session_id: null,
|
|
172
|
+
reviewing_started_at: null,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'task-2',
|
|
176
|
+
title: 'Implement feature B',
|
|
177
|
+
completed_at: '2025-01-14T11:00:00Z',
|
|
178
|
+
completed_by_session_id: 'other-session',
|
|
179
|
+
reviewing_by_session_id: 'reviewer-session',
|
|
180
|
+
reviewing_started_at: new Date(Date.now() - 5 * 60000).toISOString(), // 5 minutes ago
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const supabase = createMockSupabase({
|
|
185
|
+
selectResult: { data: mockTasks, error: null },
|
|
186
|
+
});
|
|
187
|
+
const ctx = createMockContext(supabase);
|
|
188
|
+
|
|
189
|
+
// Override to return array result
|
|
190
|
+
vi.mocked(supabase.from('').select).mockReturnValue({
|
|
191
|
+
...supabase,
|
|
192
|
+
then: (resolve: (val: unknown) => void) =>
|
|
193
|
+
Promise.resolve({ data: mockTasks, error: null }).then(resolve),
|
|
194
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
195
|
+
|
|
196
|
+
const result = await getTasksAwaitingValidation(
|
|
197
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
198
|
+
ctx
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const tasks = (result.result as { tasks: Array<{ being_reviewed: boolean; review_minutes: number | null }> }).tasks;
|
|
202
|
+
expect(tasks).toHaveLength(2);
|
|
203
|
+
|
|
204
|
+
// First task not being reviewed
|
|
205
|
+
expect(tasks[0].being_reviewed).toBe(false);
|
|
206
|
+
expect(tasks[0].review_minutes).toBeNull();
|
|
207
|
+
|
|
208
|
+
// Second task being reviewed for ~5 minutes
|
|
209
|
+
expect(tasks[1].being_reviewed).toBe(true);
|
|
210
|
+
expect(tasks[1].review_minutes).toBeGreaterThanOrEqual(4);
|
|
211
|
+
expect(tasks[1].review_minutes).toBeLessThanOrEqual(6);
|
|
212
|
+
|
|
213
|
+
// Should have hint when tasks are present
|
|
214
|
+
expect((result.result as { hint?: string }).hint).toContain('claim_validation');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should query correct table with filters', async () => {
|
|
218
|
+
const supabase = createMockSupabase({
|
|
219
|
+
selectResult: { data: [], error: null },
|
|
220
|
+
});
|
|
221
|
+
const ctx = createMockContext(supabase);
|
|
222
|
+
|
|
223
|
+
await getTasksAwaitingValidation(
|
|
224
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
225
|
+
ctx
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(supabase.from).toHaveBeenCalledWith('tasks');
|
|
229
|
+
expect(supabase.eq).toHaveBeenCalledWith('project_id', '123e4567-e89b-12d3-a456-426614174000');
|
|
230
|
+
expect(supabase.eq).toHaveBeenCalledWith('status', 'completed');
|
|
231
|
+
expect(supabase.is).toHaveBeenCalledWith('validated_at', null);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw error when database query fails', async () => {
|
|
235
|
+
const supabase = createMockSupabase({
|
|
236
|
+
selectResult: { data: null, error: { message: 'Database error' } },
|
|
237
|
+
});
|
|
238
|
+
const ctx = createMockContext(supabase);
|
|
239
|
+
|
|
240
|
+
// Override to return error
|
|
241
|
+
vi.mocked(supabase.from('').select).mockReturnValue({
|
|
242
|
+
...supabase,
|
|
243
|
+
then: (resolve: (val: unknown) => void) =>
|
|
244
|
+
Promise.resolve({ data: null, error: { message: 'Database error' } }).then(resolve),
|
|
245
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
246
|
+
|
|
247
|
+
await expect(
|
|
248
|
+
getTasksAwaitingValidation({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
249
|
+
).rejects.toThrow('Failed to fetch tasks');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// claimValidation Tests
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
describe('claimValidation', () => {
|
|
258
|
+
beforeEach(() => vi.clearAllMocks());
|
|
259
|
+
|
|
260
|
+
it('should throw error for missing task_id', async () => {
|
|
261
|
+
const supabase = createMockSupabase();
|
|
262
|
+
const ctx = createMockContext(supabase);
|
|
263
|
+
|
|
264
|
+
await expect(claimValidation({}, ctx)).rejects.toThrow(ValidationError);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should throw error for invalid task_id UUID', async () => {
|
|
268
|
+
const supabase = createMockSupabase();
|
|
269
|
+
const ctx = createMockContext(supabase);
|
|
270
|
+
|
|
271
|
+
await expect(
|
|
272
|
+
claimValidation({ task_id: 'not-a-uuid' }, ctx)
|
|
273
|
+
).rejects.toThrow(ValidationError);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should throw error when task not found', async () => {
|
|
277
|
+
const supabase = createMockSupabase({
|
|
278
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
279
|
+
});
|
|
280
|
+
const ctx = createMockContext(supabase);
|
|
281
|
+
|
|
282
|
+
await expect(
|
|
283
|
+
claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
284
|
+
).rejects.toThrow('Task not found');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should throw error when task is not completed', async () => {
|
|
288
|
+
const supabase = createMockSupabase({
|
|
289
|
+
selectResult: {
|
|
290
|
+
data: {
|
|
291
|
+
id: 'task-1',
|
|
292
|
+
title: 'Test Task',
|
|
293
|
+
status: 'in_progress',
|
|
294
|
+
validated_at: null,
|
|
295
|
+
completed_by_session_id: 'other-session',
|
|
296
|
+
reviewing_by_session_id: null,
|
|
297
|
+
},
|
|
298
|
+
error: null,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
const ctx = createMockContext(supabase);
|
|
302
|
+
|
|
303
|
+
await expect(
|
|
304
|
+
claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
305
|
+
).rejects.toThrow('Can only claim completed tasks for review');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should throw error when task is already validated', async () => {
|
|
309
|
+
const supabase = createMockSupabase({
|
|
310
|
+
selectResult: {
|
|
311
|
+
data: {
|
|
312
|
+
id: 'task-1',
|
|
313
|
+
title: 'Test Task',
|
|
314
|
+
status: 'completed',
|
|
315
|
+
validated_at: '2025-01-14T12:00:00Z',
|
|
316
|
+
completed_by_session_id: 'other-session',
|
|
317
|
+
reviewing_by_session_id: null,
|
|
318
|
+
},
|
|
319
|
+
error: null,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
const ctx = createMockContext(supabase);
|
|
323
|
+
|
|
324
|
+
await expect(
|
|
325
|
+
claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
326
|
+
).rejects.toThrow('Task has already been validated');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should throw error when task is being reviewed by another agent', async () => {
|
|
330
|
+
const supabase = createMockSupabase({
|
|
331
|
+
selectResult: {
|
|
332
|
+
data: {
|
|
333
|
+
id: 'task-1',
|
|
334
|
+
title: 'Test Task',
|
|
335
|
+
status: 'completed',
|
|
336
|
+
validated_at: null,
|
|
337
|
+
completed_by_session_id: 'other-session',
|
|
338
|
+
reviewing_by_session_id: 'different-reviewer',
|
|
339
|
+
},
|
|
340
|
+
error: null,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
344
|
+
|
|
345
|
+
await expect(
|
|
346
|
+
claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
347
|
+
).rejects.toThrow('Task is already being reviewed by another agent');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should allow same agent to re-claim their own review', async () => {
|
|
351
|
+
const supabase = createMockSupabase({
|
|
352
|
+
selectResult: {
|
|
353
|
+
data: {
|
|
354
|
+
id: 'task-1',
|
|
355
|
+
title: 'Test Task',
|
|
356
|
+
status: 'completed',
|
|
357
|
+
validated_at: null,
|
|
358
|
+
completed_by_session_id: 'other-session',
|
|
359
|
+
reviewing_by_session_id: 'session-123', // Same as current session
|
|
360
|
+
},
|
|
361
|
+
error: null,
|
|
362
|
+
},
|
|
363
|
+
updateResult: { data: null, error: null },
|
|
364
|
+
});
|
|
365
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
366
|
+
|
|
367
|
+
const result = await claimValidation(
|
|
368
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
369
|
+
ctx
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(result.result).toMatchObject({
|
|
373
|
+
success: true,
|
|
374
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
375
|
+
title: 'Test Task',
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should claim task successfully', async () => {
|
|
380
|
+
const supabase = createMockSupabase({
|
|
381
|
+
selectResult: {
|
|
382
|
+
data: {
|
|
383
|
+
id: 'task-1',
|
|
384
|
+
title: 'Test Task',
|
|
385
|
+
status: 'completed',
|
|
386
|
+
validated_at: null,
|
|
387
|
+
completed_by_session_id: 'other-session',
|
|
388
|
+
reviewing_by_session_id: null,
|
|
389
|
+
},
|
|
390
|
+
error: null,
|
|
391
|
+
},
|
|
392
|
+
updateResult: { data: null, error: null },
|
|
393
|
+
});
|
|
394
|
+
const ctx = createMockContext(supabase);
|
|
395
|
+
|
|
396
|
+
const result = await claimValidation(
|
|
397
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
398
|
+
ctx
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(result.result).toMatchObject({
|
|
402
|
+
success: true,
|
|
403
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
404
|
+
title: 'Test Task',
|
|
405
|
+
message: expect.stringContaining('Dashboard'),
|
|
406
|
+
});
|
|
407
|
+
expect((result.result as { next_step: string }).next_step).toContain('validate_task');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should update task with reviewing session info', async () => {
|
|
411
|
+
const supabase = createMockSupabase({
|
|
412
|
+
selectResult: {
|
|
413
|
+
data: {
|
|
414
|
+
id: 'task-1',
|
|
415
|
+
title: 'Test Task',
|
|
416
|
+
status: 'completed',
|
|
417
|
+
validated_at: null,
|
|
418
|
+
completed_by_session_id: 'other-session',
|
|
419
|
+
reviewing_by_session_id: null,
|
|
420
|
+
},
|
|
421
|
+
error: null,
|
|
422
|
+
},
|
|
423
|
+
updateResult: { data: null, error: null },
|
|
424
|
+
});
|
|
425
|
+
const ctx = createMockContext(supabase, { sessionId: 'reviewer-session' });
|
|
426
|
+
|
|
427
|
+
await claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
|
|
428
|
+
|
|
429
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
430
|
+
expect.objectContaining({
|
|
431
|
+
reviewing_by_session_id: 'reviewer-session',
|
|
432
|
+
reviewing_started_at: expect.any(String),
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should throw error when update fails', async () => {
|
|
438
|
+
const supabase = createMockSupabase({
|
|
439
|
+
selectResult: {
|
|
440
|
+
data: {
|
|
441
|
+
id: 'task-1',
|
|
442
|
+
title: 'Test Task',
|
|
443
|
+
status: 'completed',
|
|
444
|
+
validated_at: null,
|
|
445
|
+
completed_by_session_id: 'other-session',
|
|
446
|
+
reviewing_by_session_id: null,
|
|
447
|
+
},
|
|
448
|
+
error: null,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
const ctx = createMockContext(supabase);
|
|
452
|
+
|
|
453
|
+
// Override update to return error
|
|
454
|
+
vi.mocked(supabase.from('').update).mockReturnValue({
|
|
455
|
+
...supabase,
|
|
456
|
+
eq: vi.fn().mockReturnValue({
|
|
457
|
+
then: (resolve: (val: unknown) => void) =>
|
|
458
|
+
Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
|
|
459
|
+
}),
|
|
460
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
461
|
+
|
|
462
|
+
await expect(
|
|
463
|
+
claimValidation({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
464
|
+
).rejects.toThrow('Failed to claim task');
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// validateTask Tests
|
|
470
|
+
// ============================================================================
|
|
471
|
+
|
|
472
|
+
describe('validateTask', () => {
|
|
473
|
+
beforeEach(() => vi.clearAllMocks());
|
|
474
|
+
|
|
475
|
+
it('should throw error for missing task_id', async () => {
|
|
476
|
+
const supabase = createMockSupabase();
|
|
477
|
+
const ctx = createMockContext(supabase);
|
|
478
|
+
|
|
479
|
+
await expect(
|
|
480
|
+
validateTask({ approved: true }, ctx)
|
|
481
|
+
).rejects.toThrow(ValidationError);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should throw error for invalid task_id UUID', async () => {
|
|
485
|
+
const supabase = createMockSupabase();
|
|
486
|
+
const ctx = createMockContext(supabase);
|
|
487
|
+
|
|
488
|
+
await expect(
|
|
489
|
+
validateTask({ task_id: 'invalid', approved: true }, ctx)
|
|
490
|
+
).rejects.toThrow(ValidationError);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should throw error when task not found', async () => {
|
|
494
|
+
const supabase = createMockSupabase({
|
|
495
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
496
|
+
});
|
|
497
|
+
const ctx = createMockContext(supabase);
|
|
498
|
+
|
|
499
|
+
await expect(
|
|
500
|
+
validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
|
|
501
|
+
).rejects.toThrow('Task not found');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should throw error when task is not completed', async () => {
|
|
505
|
+
const supabase = createMockSupabase({
|
|
506
|
+
selectResult: {
|
|
507
|
+
data: {
|
|
508
|
+
id: 'task-1',
|
|
509
|
+
title: 'Test Task',
|
|
510
|
+
status: 'in_progress',
|
|
511
|
+
validated_at: null,
|
|
512
|
+
completed_by_session_id: 'other-session',
|
|
513
|
+
project_id: 'proj-1',
|
|
514
|
+
git_branch: null,
|
|
515
|
+
},
|
|
516
|
+
error: null,
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
const ctx = createMockContext(supabase);
|
|
520
|
+
|
|
521
|
+
await expect(
|
|
522
|
+
validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
|
|
523
|
+
).rejects.toThrow('Can only validate completed tasks');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should throw error when task already validated', async () => {
|
|
527
|
+
const supabase = createMockSupabase({
|
|
528
|
+
selectResult: {
|
|
529
|
+
data: {
|
|
530
|
+
id: 'task-1',
|
|
531
|
+
title: 'Test Task',
|
|
532
|
+
status: 'completed',
|
|
533
|
+
validated_at: '2025-01-14T12:00:00Z',
|
|
534
|
+
completed_by_session_id: 'other-session',
|
|
535
|
+
project_id: 'proj-1',
|
|
536
|
+
git_branch: null,
|
|
537
|
+
},
|
|
538
|
+
error: null,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
const ctx = createMockContext(supabase);
|
|
542
|
+
|
|
543
|
+
await expect(
|
|
544
|
+
validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
|
|
545
|
+
).rejects.toThrow('Task has already been validated');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe('when approved', () => {
|
|
549
|
+
it('should mark task as validated', async () => {
|
|
550
|
+
const supabase = createMockSupabase({
|
|
551
|
+
selectResult: {
|
|
552
|
+
data: {
|
|
553
|
+
id: 'task-1',
|
|
554
|
+
title: 'Test Task',
|
|
555
|
+
status: 'completed',
|
|
556
|
+
validated_at: null,
|
|
557
|
+
completed_by_session_id: 'other-session',
|
|
558
|
+
project_id: 'proj-1',
|
|
559
|
+
git_branch: null,
|
|
560
|
+
},
|
|
561
|
+
error: null,
|
|
562
|
+
},
|
|
563
|
+
updateResult: { data: null, error: null },
|
|
564
|
+
insertResult: { data: null, error: null },
|
|
565
|
+
});
|
|
566
|
+
const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
|
|
567
|
+
|
|
568
|
+
const result = await validateTask(
|
|
569
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
|
|
570
|
+
ctx
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
expect(result.result).toMatchObject({
|
|
574
|
+
success: true,
|
|
575
|
+
validated_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
576
|
+
self_validated: false,
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should detect self-validation', async () => {
|
|
581
|
+
const supabase = createMockSupabase({
|
|
582
|
+
selectResult: {
|
|
583
|
+
data: {
|
|
584
|
+
id: 'task-1',
|
|
585
|
+
title: 'Test Task',
|
|
586
|
+
status: 'completed',
|
|
587
|
+
validated_at: null,
|
|
588
|
+
completed_by_session_id: 'session-123', // Same as validator
|
|
589
|
+
project_id: 'proj-1',
|
|
590
|
+
git_branch: null,
|
|
591
|
+
},
|
|
592
|
+
error: null,
|
|
593
|
+
},
|
|
594
|
+
updateResult: { data: null, error: null },
|
|
595
|
+
insertResult: { data: null, error: null },
|
|
596
|
+
});
|
|
597
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
598
|
+
|
|
599
|
+
const result = await validateTask(
|
|
600
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
|
|
601
|
+
ctx
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
expect(result.result).toMatchObject({
|
|
605
|
+
success: true,
|
|
606
|
+
self_validated: true,
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should clear reviewing status on approval', async () => {
|
|
611
|
+
const supabase = createMockSupabase({
|
|
612
|
+
selectResult: {
|
|
613
|
+
data: {
|
|
614
|
+
id: 'task-1',
|
|
615
|
+
title: 'Test Task',
|
|
616
|
+
status: 'completed',
|
|
617
|
+
validated_at: null,
|
|
618
|
+
completed_by_session_id: 'other-session',
|
|
619
|
+
project_id: 'proj-1',
|
|
620
|
+
git_branch: null,
|
|
621
|
+
},
|
|
622
|
+
error: null,
|
|
623
|
+
},
|
|
624
|
+
updateResult: { data: null, error: null },
|
|
625
|
+
insertResult: { data: null, error: null },
|
|
626
|
+
});
|
|
627
|
+
const ctx = createMockContext(supabase);
|
|
628
|
+
|
|
629
|
+
await validateTask(
|
|
630
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true },
|
|
631
|
+
ctx
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
635
|
+
expect.objectContaining({
|
|
636
|
+
validated_at: expect.any(String),
|
|
637
|
+
reviewing_by_session_id: null,
|
|
638
|
+
reviewing_started_at: null,
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('should log validation in progress_logs', async () => {
|
|
644
|
+
const supabase = createMockSupabase({
|
|
645
|
+
selectResult: {
|
|
646
|
+
data: {
|
|
647
|
+
id: 'task-1',
|
|
648
|
+
title: 'Test Feature',
|
|
649
|
+
status: 'completed',
|
|
650
|
+
validated_at: null,
|
|
651
|
+
completed_by_session_id: 'other-session',
|
|
652
|
+
project_id: 'proj-123',
|
|
653
|
+
git_branch: null,
|
|
654
|
+
},
|
|
655
|
+
error: null,
|
|
656
|
+
},
|
|
657
|
+
updateResult: { data: null, error: null },
|
|
658
|
+
insertResult: { data: null, error: null },
|
|
659
|
+
});
|
|
660
|
+
const ctx = createMockContext(supabase, { sessionId: 'validator-session' });
|
|
661
|
+
|
|
662
|
+
await validateTask(
|
|
663
|
+
{
|
|
664
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
665
|
+
approved: true,
|
|
666
|
+
validation_notes: 'Looks good!',
|
|
667
|
+
},
|
|
668
|
+
ctx
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
expect(supabase.from).toHaveBeenCalledWith('progress_logs');
|
|
672
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
673
|
+
expect.objectContaining({
|
|
674
|
+
project_id: 'proj-123',
|
|
675
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
676
|
+
summary: expect.stringContaining('Validated'),
|
|
677
|
+
details: 'Looks good!',
|
|
678
|
+
created_by: 'agent',
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe('when rejected', () => {
|
|
685
|
+
it('should reopen task', async () => {
|
|
686
|
+
const supabase = createMockSupabase({
|
|
687
|
+
selectResult: {
|
|
688
|
+
data: {
|
|
689
|
+
id: 'task-1',
|
|
690
|
+
title: 'Test Task',
|
|
691
|
+
status: 'completed',
|
|
692
|
+
validated_at: null,
|
|
693
|
+
completed_by_session_id: 'other-session',
|
|
694
|
+
project_id: 'proj-1',
|
|
695
|
+
git_branch: null,
|
|
696
|
+
},
|
|
697
|
+
error: null,
|
|
698
|
+
},
|
|
699
|
+
updateResult: { data: null, error: null },
|
|
700
|
+
insertResult: { data: null, error: null },
|
|
701
|
+
});
|
|
702
|
+
const ctx = createMockContext(supabase);
|
|
703
|
+
|
|
704
|
+
const result = await validateTask(
|
|
705
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
|
|
706
|
+
ctx
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
expect(result.result).toMatchObject({
|
|
710
|
+
success: true,
|
|
711
|
+
reopened_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should set task to in_progress with 80% progress', async () => {
|
|
716
|
+
const supabase = createMockSupabase({
|
|
717
|
+
selectResult: {
|
|
718
|
+
data: {
|
|
719
|
+
id: 'task-1',
|
|
720
|
+
title: 'Test Task',
|
|
721
|
+
status: 'completed',
|
|
722
|
+
validated_at: null,
|
|
723
|
+
completed_by_session_id: 'other-session',
|
|
724
|
+
project_id: 'proj-1',
|
|
725
|
+
git_branch: null,
|
|
726
|
+
},
|
|
727
|
+
error: null,
|
|
728
|
+
},
|
|
729
|
+
updateResult: { data: null, error: null },
|
|
730
|
+
insertResult: { data: null, error: null },
|
|
731
|
+
});
|
|
732
|
+
const ctx = createMockContext(supabase);
|
|
733
|
+
|
|
734
|
+
await validateTask(
|
|
735
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
|
|
736
|
+
ctx
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
740
|
+
expect.objectContaining({
|
|
741
|
+
status: 'in_progress',
|
|
742
|
+
completed_at: null,
|
|
743
|
+
progress_percentage: 80,
|
|
744
|
+
reviewing_by_session_id: null,
|
|
745
|
+
reviewing_started_at: null,
|
|
746
|
+
})
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should log rejection in progress_logs', async () => {
|
|
751
|
+
const supabase = createMockSupabase({
|
|
752
|
+
selectResult: {
|
|
753
|
+
data: {
|
|
754
|
+
id: 'task-1',
|
|
755
|
+
title: 'Broken Feature',
|
|
756
|
+
status: 'completed',
|
|
757
|
+
validated_at: null,
|
|
758
|
+
completed_by_session_id: 'other-session',
|
|
759
|
+
project_id: 'proj-123',
|
|
760
|
+
git_branch: null,
|
|
761
|
+
},
|
|
762
|
+
error: null,
|
|
763
|
+
},
|
|
764
|
+
updateResult: { data: null, error: null },
|
|
765
|
+
insertResult: { data: null, error: null },
|
|
766
|
+
});
|
|
767
|
+
const ctx = createMockContext(supabase);
|
|
768
|
+
|
|
769
|
+
await validateTask(
|
|
770
|
+
{
|
|
771
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
772
|
+
approved: false,
|
|
773
|
+
validation_notes: 'Tests failing',
|
|
774
|
+
},
|
|
775
|
+
ctx
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
expect(supabase.from).toHaveBeenCalledWith('progress_logs');
|
|
779
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
780
|
+
expect.objectContaining({
|
|
781
|
+
summary: expect.stringContaining('Validation failed'),
|
|
782
|
+
details: 'Tests failing',
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('should use default message when no validation_notes provided', async () => {
|
|
788
|
+
const supabase = createMockSupabase({
|
|
789
|
+
selectResult: {
|
|
790
|
+
data: {
|
|
791
|
+
id: 'task-1',
|
|
792
|
+
title: 'Test Task',
|
|
793
|
+
status: 'completed',
|
|
794
|
+
validated_at: null,
|
|
795
|
+
completed_by_session_id: 'other-session',
|
|
796
|
+
project_id: 'proj-123',
|
|
797
|
+
git_branch: null,
|
|
798
|
+
},
|
|
799
|
+
error: null,
|
|
800
|
+
},
|
|
801
|
+
updateResult: { data: null, error: null },
|
|
802
|
+
insertResult: { data: null, error: null },
|
|
803
|
+
});
|
|
804
|
+
const ctx = createMockContext(supabase);
|
|
805
|
+
|
|
806
|
+
await validateTask(
|
|
807
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false },
|
|
808
|
+
ctx
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
812
|
+
expect.objectContaining({
|
|
813
|
+
details: 'Needs more work',
|
|
814
|
+
})
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should throw error when update fails on approval', async () => {
|
|
820
|
+
const supabase = createMockSupabase({
|
|
821
|
+
selectResult: {
|
|
822
|
+
data: {
|
|
823
|
+
id: 'task-1',
|
|
824
|
+
title: 'Test Task',
|
|
825
|
+
status: 'completed',
|
|
826
|
+
validated_at: null,
|
|
827
|
+
completed_by_session_id: 'other-session',
|
|
828
|
+
project_id: 'proj-1',
|
|
829
|
+
git_branch: null,
|
|
830
|
+
},
|
|
831
|
+
error: null,
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
const ctx = createMockContext(supabase);
|
|
835
|
+
|
|
836
|
+
// Override update to return error
|
|
837
|
+
vi.mocked(supabase.from('').update).mockReturnValue({
|
|
838
|
+
...supabase,
|
|
839
|
+
eq: vi.fn().mockReturnValue({
|
|
840
|
+
then: (resolve: (val: unknown) => void) =>
|
|
841
|
+
Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
|
|
842
|
+
}),
|
|
843
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
844
|
+
|
|
845
|
+
await expect(
|
|
846
|
+
validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: true }, ctx)
|
|
847
|
+
).rejects.toThrow('Failed to validate task');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should throw error when update fails on rejection', async () => {
|
|
851
|
+
const supabase = createMockSupabase({
|
|
852
|
+
selectResult: {
|
|
853
|
+
data: {
|
|
854
|
+
id: 'task-1',
|
|
855
|
+
title: 'Test Task',
|
|
856
|
+
status: 'completed',
|
|
857
|
+
validated_at: null,
|
|
858
|
+
completed_by_session_id: 'other-session',
|
|
859
|
+
project_id: 'proj-1',
|
|
860
|
+
git_branch: null,
|
|
861
|
+
},
|
|
862
|
+
error: null,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
const ctx = createMockContext(supabase);
|
|
866
|
+
|
|
867
|
+
// Override update to return error
|
|
868
|
+
vi.mocked(supabase.from('').update).mockReturnValue({
|
|
869
|
+
...supabase,
|
|
870
|
+
eq: vi.fn().mockReturnValue({
|
|
871
|
+
then: (resolve: (val: unknown) => void) =>
|
|
872
|
+
Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
|
|
873
|
+
}),
|
|
874
|
+
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
875
|
+
|
|
876
|
+
await expect(
|
|
877
|
+
validateTask({ task_id: '123e4567-e89b-12d3-a456-426614174000', approved: false }, ctx)
|
|
878
|
+
).rejects.toThrow('Failed to reopen task');
|
|
879
|
+
});
|
|
880
|
+
});
|