@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,584 @@
|
|
|
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
|
+
addMilestone,
|
|
6
|
+
updateMilestone,
|
|
7
|
+
completeMilestone,
|
|
8
|
+
deleteMilestone,
|
|
9
|
+
getMilestones,
|
|
10
|
+
} from './milestones.js';
|
|
11
|
+
import { ValidationError } from '../validators.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Test Utilities
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
function createMockSupabase(overrides: {
|
|
18
|
+
selectResult?: { data: unknown; error: unknown };
|
|
19
|
+
insertResult?: { data: unknown; error: unknown };
|
|
20
|
+
updateResult?: { data: unknown; error: unknown };
|
|
21
|
+
deleteResult?: { data: unknown; error: unknown };
|
|
22
|
+
} = {}) {
|
|
23
|
+
const defaultResult = { data: null, error: null };
|
|
24
|
+
let currentOperation = 'select';
|
|
25
|
+
let insertThenSelect = false;
|
|
26
|
+
|
|
27
|
+
const mock = {
|
|
28
|
+
from: vi.fn().mockReturnThis(),
|
|
29
|
+
select: vi.fn(() => {
|
|
30
|
+
if (currentOperation === 'insert' || currentOperation === 'update') {
|
|
31
|
+
insertThenSelect = true;
|
|
32
|
+
} else {
|
|
33
|
+
currentOperation = 'select';
|
|
34
|
+
insertThenSelect = false;
|
|
35
|
+
}
|
|
36
|
+
return mock;
|
|
37
|
+
}),
|
|
38
|
+
insert: vi.fn(() => {
|
|
39
|
+
currentOperation = 'insert';
|
|
40
|
+
insertThenSelect = false;
|
|
41
|
+
return mock;
|
|
42
|
+
}),
|
|
43
|
+
update: vi.fn(() => {
|
|
44
|
+
currentOperation = 'update';
|
|
45
|
+
insertThenSelect = false;
|
|
46
|
+
return mock;
|
|
47
|
+
}),
|
|
48
|
+
delete: vi.fn(() => {
|
|
49
|
+
currentOperation = 'delete';
|
|
50
|
+
insertThenSelect = false;
|
|
51
|
+
return mock;
|
|
52
|
+
}),
|
|
53
|
+
eq: vi.fn().mockReturnThis(),
|
|
54
|
+
neq: vi.fn().mockReturnThis(),
|
|
55
|
+
in: vi.fn().mockReturnThis(),
|
|
56
|
+
is: vi.fn().mockReturnThis(),
|
|
57
|
+
not: vi.fn().mockReturnThis(),
|
|
58
|
+
or: vi.fn().mockReturnThis(),
|
|
59
|
+
gt: vi.fn().mockReturnThis(),
|
|
60
|
+
gte: vi.fn().mockReturnThis(),
|
|
61
|
+
lte: vi.fn().mockReturnThis(),
|
|
62
|
+
lt: vi.fn().mockReturnThis(),
|
|
63
|
+
order: vi.fn().mockReturnThis(),
|
|
64
|
+
limit: vi.fn().mockReturnThis(),
|
|
65
|
+
single: vi.fn(() => {
|
|
66
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
67
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult);
|
|
68
|
+
}
|
|
69
|
+
if (currentOperation === 'select') {
|
|
70
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
71
|
+
}
|
|
72
|
+
if (currentOperation === 'update') {
|
|
73
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
74
|
+
}
|
|
75
|
+
return Promise.resolve(defaultResult);
|
|
76
|
+
}),
|
|
77
|
+
maybeSingle: vi.fn(() => {
|
|
78
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
79
|
+
}),
|
|
80
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
81
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
82
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
83
|
+
}
|
|
84
|
+
if (currentOperation === 'select') {
|
|
85
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
86
|
+
}
|
|
87
|
+
if (currentOperation === 'update') {
|
|
88
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
89
|
+
}
|
|
90
|
+
if (currentOperation === 'delete') {
|
|
91
|
+
return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return mock as unknown as SupabaseClient;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createMockContext(
|
|
101
|
+
supabase: SupabaseClient,
|
|
102
|
+
options: { sessionId?: string | null } = {}
|
|
103
|
+
): HandlerContext {
|
|
104
|
+
const defaultTokenUsage: TokenUsage = {
|
|
105
|
+
callCount: 5,
|
|
106
|
+
totalTokens: 2500,
|
|
107
|
+
byTool: {},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
supabase,
|
|
114
|
+
auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
|
|
115
|
+
session: {
|
|
116
|
+
instanceId: 'instance-abc',
|
|
117
|
+
currentSessionId: sessionId,
|
|
118
|
+
currentPersona: 'Wave',
|
|
119
|
+
tokenUsage: defaultTokenUsage,
|
|
120
|
+
},
|
|
121
|
+
updateSession: vi.fn(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// addMilestone Tests
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
describe('addMilestone', () => {
|
|
130
|
+
beforeEach(() => vi.clearAllMocks());
|
|
131
|
+
|
|
132
|
+
it('should throw error for missing task_id', async () => {
|
|
133
|
+
const supabase = createMockSupabase();
|
|
134
|
+
const ctx = createMockContext(supabase);
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
addMilestone({ title: 'Milestone 1' }, ctx)
|
|
138
|
+
).rejects.toThrow(ValidationError);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw error for invalid task_id UUID', async () => {
|
|
142
|
+
const supabase = createMockSupabase();
|
|
143
|
+
const ctx = createMockContext(supabase);
|
|
144
|
+
|
|
145
|
+
await expect(
|
|
146
|
+
addMilestone({ task_id: 'invalid', title: 'Milestone 1' }, ctx)
|
|
147
|
+
).rejects.toThrow(ValidationError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw error for missing title', async () => {
|
|
151
|
+
const supabase = createMockSupabase();
|
|
152
|
+
const ctx = createMockContext(supabase);
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
addMilestone({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
156
|
+
).rejects.toThrow(ValidationError);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw error when task not found', async () => {
|
|
160
|
+
const supabase = createMockSupabase({
|
|
161
|
+
selectResult: { data: null, error: { message: 'Not found' } },
|
|
162
|
+
});
|
|
163
|
+
const ctx = createMockContext(supabase);
|
|
164
|
+
|
|
165
|
+
await expect(
|
|
166
|
+
addMilestone({
|
|
167
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
168
|
+
title: 'Milestone 1',
|
|
169
|
+
}, ctx)
|
|
170
|
+
).rejects.toThrow('Task not found');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should add milestone successfully', async () => {
|
|
174
|
+
const mockMilestone = {
|
|
175
|
+
id: 'milestone-1',
|
|
176
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
177
|
+
title: 'First Milestone',
|
|
178
|
+
order_index: 0,
|
|
179
|
+
status: 'pending',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const supabase = createMockSupabase({
|
|
183
|
+
selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
|
|
184
|
+
insertResult: { data: mockMilestone, error: null },
|
|
185
|
+
});
|
|
186
|
+
const ctx = createMockContext(supabase);
|
|
187
|
+
|
|
188
|
+
const result = await addMilestone(
|
|
189
|
+
{
|
|
190
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
191
|
+
title: 'First Milestone',
|
|
192
|
+
},
|
|
193
|
+
ctx
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(result.result).toMatchObject({
|
|
197
|
+
success: true,
|
|
198
|
+
milestone: mockMilestone,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should use provided order_index', async () => {
|
|
203
|
+
const supabase = createMockSupabase({
|
|
204
|
+
selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
|
|
205
|
+
insertResult: { data: { id: 'milestone-1', order_index: 5 }, error: null },
|
|
206
|
+
});
|
|
207
|
+
const ctx = createMockContext(supabase);
|
|
208
|
+
|
|
209
|
+
await addMilestone(
|
|
210
|
+
{
|
|
211
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
212
|
+
title: 'Test Milestone',
|
|
213
|
+
order_index: 5,
|
|
214
|
+
},
|
|
215
|
+
ctx
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
order_index: 5,
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should include session_id in insert', async () => {
|
|
226
|
+
const supabase = createMockSupabase({
|
|
227
|
+
selectResult: { data: { id: 'task-1', project_id: 'proj-1' }, error: null },
|
|
228
|
+
insertResult: { data: { id: 'milestone-1' }, error: null },
|
|
229
|
+
});
|
|
230
|
+
const ctx = createMockContext(supabase, { sessionId: 'my-session' });
|
|
231
|
+
|
|
232
|
+
await addMilestone(
|
|
233
|
+
{
|
|
234
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
235
|
+
title: 'Test',
|
|
236
|
+
},
|
|
237
|
+
ctx
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
created_by: 'agent',
|
|
243
|
+
created_by_session_id: 'my-session',
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// updateMilestone Tests
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
describe('updateMilestone', () => {
|
|
254
|
+
beforeEach(() => vi.clearAllMocks());
|
|
255
|
+
|
|
256
|
+
it('should throw error for missing milestone_id', async () => {
|
|
257
|
+
const supabase = createMockSupabase();
|
|
258
|
+
const ctx = createMockContext(supabase);
|
|
259
|
+
|
|
260
|
+
await expect(updateMilestone({}, ctx)).rejects.toThrow(ValidationError);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should throw error for invalid milestone_id UUID', async () => {
|
|
264
|
+
const supabase = createMockSupabase();
|
|
265
|
+
const ctx = createMockContext(supabase);
|
|
266
|
+
|
|
267
|
+
await expect(
|
|
268
|
+
updateMilestone({ milestone_id: 'invalid', title: 'New Title' }, ctx)
|
|
269
|
+
).rejects.toThrow(ValidationError);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should throw error when no fields to update', async () => {
|
|
273
|
+
const supabase = createMockSupabase();
|
|
274
|
+
const ctx = createMockContext(supabase);
|
|
275
|
+
|
|
276
|
+
await expect(
|
|
277
|
+
updateMilestone({ milestone_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
278
|
+
).rejects.toThrow('At least one field to update is required');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should throw error for invalid status', async () => {
|
|
282
|
+
const supabase = createMockSupabase();
|
|
283
|
+
const ctx = createMockContext(supabase);
|
|
284
|
+
|
|
285
|
+
await expect(
|
|
286
|
+
updateMilestone({
|
|
287
|
+
milestone_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
288
|
+
status: 'invalid_status',
|
|
289
|
+
}, ctx)
|
|
290
|
+
).rejects.toThrow('status must be pending, in_progress, or completed');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should update title successfully', async () => {
|
|
294
|
+
const supabase = createMockSupabase({
|
|
295
|
+
updateResult: { data: { id: 'milestone-1', title: 'Updated Title' }, error: null },
|
|
296
|
+
});
|
|
297
|
+
const ctx = createMockContext(supabase);
|
|
298
|
+
|
|
299
|
+
const result = await updateMilestone(
|
|
300
|
+
{
|
|
301
|
+
milestone_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
302
|
+
title: 'Updated Title',
|
|
303
|
+
},
|
|
304
|
+
ctx
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(result.result).toMatchObject({
|
|
308
|
+
success: true,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should set completed_at when status is completed', async () => {
|
|
313
|
+
const supabase = createMockSupabase({
|
|
314
|
+
updateResult: { data: { id: 'milestone-1', status: 'completed' }, error: null },
|
|
315
|
+
});
|
|
316
|
+
const ctx = createMockContext(supabase);
|
|
317
|
+
|
|
318
|
+
await updateMilestone(
|
|
319
|
+
{
|
|
320
|
+
milestone_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
321
|
+
status: 'completed',
|
|
322
|
+
},
|
|
323
|
+
ctx
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
327
|
+
expect.objectContaining({
|
|
328
|
+
status: 'completed',
|
|
329
|
+
completed_at: expect.any(String),
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should clear completed_at when status is not completed', async () => {
|
|
335
|
+
const supabase = createMockSupabase({
|
|
336
|
+
updateResult: { data: { id: 'milestone-1', status: 'in_progress' }, error: null },
|
|
337
|
+
});
|
|
338
|
+
const ctx = createMockContext(supabase);
|
|
339
|
+
|
|
340
|
+
await updateMilestone(
|
|
341
|
+
{
|
|
342
|
+
milestone_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
343
|
+
status: 'in_progress',
|
|
344
|
+
},
|
|
345
|
+
ctx
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
status: 'in_progress',
|
|
351
|
+
completed_at: null,
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// completeMilestone Tests
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
describe('completeMilestone', () => {
|
|
362
|
+
beforeEach(() => vi.clearAllMocks());
|
|
363
|
+
|
|
364
|
+
it('should throw error for missing milestone_id', async () => {
|
|
365
|
+
const supabase = createMockSupabase();
|
|
366
|
+
const ctx = createMockContext(supabase);
|
|
367
|
+
|
|
368
|
+
await expect(completeMilestone({}, ctx)).rejects.toThrow(ValidationError);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should throw error for invalid milestone_id UUID', async () => {
|
|
372
|
+
const supabase = createMockSupabase();
|
|
373
|
+
const ctx = createMockContext(supabase);
|
|
374
|
+
|
|
375
|
+
await expect(
|
|
376
|
+
completeMilestone({ milestone_id: 'invalid' }, ctx)
|
|
377
|
+
).rejects.toThrow(ValidationError);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should complete milestone successfully', async () => {
|
|
381
|
+
const supabase = createMockSupabase({
|
|
382
|
+
updateResult: {
|
|
383
|
+
data: { id: 'milestone-1', status: 'completed', completed_at: '2025-01-14T12:00:00Z' },
|
|
384
|
+
error: null,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
const ctx = createMockContext(supabase);
|
|
388
|
+
|
|
389
|
+
const result = await completeMilestone(
|
|
390
|
+
{ milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
391
|
+
ctx
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(result.result).toMatchObject({
|
|
395
|
+
success: true,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should set status to completed with timestamp', async () => {
|
|
400
|
+
const supabase = createMockSupabase({
|
|
401
|
+
updateResult: { data: { id: 'milestone-1' }, error: null },
|
|
402
|
+
});
|
|
403
|
+
const ctx = createMockContext(supabase);
|
|
404
|
+
|
|
405
|
+
await completeMilestone(
|
|
406
|
+
{ milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
407
|
+
ctx
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
411
|
+
expect.objectContaining({
|
|
412
|
+
status: 'completed',
|
|
413
|
+
completed_at: expect.any(String),
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// deleteMilestone Tests
|
|
421
|
+
// ============================================================================
|
|
422
|
+
|
|
423
|
+
describe('deleteMilestone', () => {
|
|
424
|
+
beforeEach(() => vi.clearAllMocks());
|
|
425
|
+
|
|
426
|
+
it('should throw error for missing milestone_id', async () => {
|
|
427
|
+
const supabase = createMockSupabase();
|
|
428
|
+
const ctx = createMockContext(supabase);
|
|
429
|
+
|
|
430
|
+
await expect(deleteMilestone({}, ctx)).rejects.toThrow(ValidationError);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should throw error for invalid milestone_id UUID', async () => {
|
|
434
|
+
const supabase = createMockSupabase();
|
|
435
|
+
const ctx = createMockContext(supabase);
|
|
436
|
+
|
|
437
|
+
await expect(
|
|
438
|
+
deleteMilestone({ milestone_id: 'invalid' }, ctx)
|
|
439
|
+
).rejects.toThrow(ValidationError);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should delete milestone successfully', async () => {
|
|
443
|
+
const supabase = createMockSupabase({
|
|
444
|
+
deleteResult: { data: null, error: null },
|
|
445
|
+
});
|
|
446
|
+
const ctx = createMockContext(supabase);
|
|
447
|
+
|
|
448
|
+
const result = await deleteMilestone(
|
|
449
|
+
{ milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
450
|
+
ctx
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(result.result).toMatchObject({
|
|
454
|
+
success: true,
|
|
455
|
+
message: 'Milestone deleted',
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should call delete on task_milestones table', async () => {
|
|
460
|
+
const supabase = createMockSupabase({
|
|
461
|
+
deleteResult: { data: null, error: null },
|
|
462
|
+
});
|
|
463
|
+
const ctx = createMockContext(supabase);
|
|
464
|
+
|
|
465
|
+
await deleteMilestone(
|
|
466
|
+
{ milestone_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
467
|
+
ctx
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(supabase.from).toHaveBeenCalledWith('task_milestones');
|
|
471
|
+
expect(supabase.delete).toHaveBeenCalled();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ============================================================================
|
|
476
|
+
// getMilestones Tests
|
|
477
|
+
// ============================================================================
|
|
478
|
+
|
|
479
|
+
describe('getMilestones', () => {
|
|
480
|
+
beforeEach(() => vi.clearAllMocks());
|
|
481
|
+
|
|
482
|
+
it('should throw error for missing task_id', async () => {
|
|
483
|
+
const supabase = createMockSupabase();
|
|
484
|
+
const ctx = createMockContext(supabase);
|
|
485
|
+
|
|
486
|
+
await expect(getMilestones({}, ctx)).rejects.toThrow(ValidationError);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should throw error for invalid task_id UUID', async () => {
|
|
490
|
+
const supabase = createMockSupabase();
|
|
491
|
+
const ctx = createMockContext(supabase);
|
|
492
|
+
|
|
493
|
+
await expect(
|
|
494
|
+
getMilestones({ task_id: 'invalid' }, ctx)
|
|
495
|
+
).rejects.toThrow(ValidationError);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should return empty list with zero stats', async () => {
|
|
499
|
+
const supabase = createMockSupabase({
|
|
500
|
+
selectResult: { data: [], error: null },
|
|
501
|
+
});
|
|
502
|
+
const ctx = createMockContext(supabase);
|
|
503
|
+
|
|
504
|
+
const result = await getMilestones(
|
|
505
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
506
|
+
ctx
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(result.result).toMatchObject({
|
|
510
|
+
milestones: [],
|
|
511
|
+
stats: {
|
|
512
|
+
total: 0,
|
|
513
|
+
completed: 0,
|
|
514
|
+
in_progress: 0,
|
|
515
|
+
pending: 0,
|
|
516
|
+
progress_percentage: 0,
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should return milestones with stats', async () => {
|
|
522
|
+
const mockMilestones = [
|
|
523
|
+
{ id: 'm1', title: 'Step 1', status: 'completed', order_index: 0 },
|
|
524
|
+
{ id: 'm2', title: 'Step 2', status: 'in_progress', order_index: 1 },
|
|
525
|
+
{ id: 'm3', title: 'Step 3', status: 'pending', order_index: 2 },
|
|
526
|
+
{ id: 'm4', title: 'Step 4', status: 'pending', order_index: 3 },
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
const supabase = createMockSupabase({
|
|
530
|
+
selectResult: { data: mockMilestones, error: null },
|
|
531
|
+
});
|
|
532
|
+
const ctx = createMockContext(supabase);
|
|
533
|
+
|
|
534
|
+
const result = await getMilestones(
|
|
535
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
536
|
+
ctx
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(result.result).toMatchObject({
|
|
540
|
+
milestones: mockMilestones,
|
|
541
|
+
stats: {
|
|
542
|
+
total: 4,
|
|
543
|
+
completed: 1,
|
|
544
|
+
in_progress: 1,
|
|
545
|
+
pending: 2,
|
|
546
|
+
progress_percentage: 25, // 1/4 = 25%
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should calculate 100% when all completed', async () => {
|
|
552
|
+
const mockMilestones = [
|
|
553
|
+
{ id: 'm1', status: 'completed' },
|
|
554
|
+
{ id: 'm2', status: 'completed' },
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
const supabase = createMockSupabase({
|
|
558
|
+
selectResult: { data: mockMilestones, error: null },
|
|
559
|
+
});
|
|
560
|
+
const ctx = createMockContext(supabase);
|
|
561
|
+
|
|
562
|
+
const result = await getMilestones(
|
|
563
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
564
|
+
ctx
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
expect((result.result as { stats: { progress_percentage: number } }).stats.progress_percentage).toBe(100);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should query task_milestones table ordered by order_index', async () => {
|
|
571
|
+
const supabase = createMockSupabase({
|
|
572
|
+
selectResult: { data: [], error: null },
|
|
573
|
+
});
|
|
574
|
+
const ctx = createMockContext(supabase);
|
|
575
|
+
|
|
576
|
+
await getMilestones(
|
|
577
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
578
|
+
ctx
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
expect(supabase.from).toHaveBeenCalledWith('task_milestones');
|
|
582
|
+
expect(supabase.order).toHaveBeenCalledWith('order_index', { ascending: true });
|
|
583
|
+
});
|
|
584
|
+
});
|