@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,369 @@
|
|
|
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 { logProgress, getActivityFeed } from './progress.js';
|
|
5
|
+
import { ValidationError } from '../validators.js';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Test Utilities
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
function createMockSupabase(overrides: {
|
|
12
|
+
selectResult?: { data: unknown; error: unknown };
|
|
13
|
+
insertResult?: { data: unknown; error: unknown };
|
|
14
|
+
} = {}) {
|
|
15
|
+
const defaultResult = { data: null, error: null };
|
|
16
|
+
let currentOperation = 'select';
|
|
17
|
+
|
|
18
|
+
const mock = {
|
|
19
|
+
from: vi.fn().mockReturnThis(),
|
|
20
|
+
select: vi.fn(() => {
|
|
21
|
+
currentOperation = 'select';
|
|
22
|
+
return mock;
|
|
23
|
+
}),
|
|
24
|
+
insert: vi.fn(() => {
|
|
25
|
+
currentOperation = 'insert';
|
|
26
|
+
return mock;
|
|
27
|
+
}),
|
|
28
|
+
eq: vi.fn().mockReturnThis(),
|
|
29
|
+
gt: vi.fn().mockReturnThis(),
|
|
30
|
+
order: vi.fn().mockReturnThis(),
|
|
31
|
+
limit: vi.fn().mockReturnThis(),
|
|
32
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
33
|
+
if (currentOperation === 'insert') {
|
|
34
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
35
|
+
}
|
|
36
|
+
if (currentOperation === 'select') {
|
|
37
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return mock as unknown as SupabaseClient;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createMockContext(
|
|
47
|
+
supabase: SupabaseClient,
|
|
48
|
+
options: { sessionId?: string | null } = {}
|
|
49
|
+
): HandlerContext {
|
|
50
|
+
const defaultTokenUsage: TokenUsage = {
|
|
51
|
+
callCount: 5,
|
|
52
|
+
totalTokens: 2500,
|
|
53
|
+
byTool: {},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
supabase,
|
|
60
|
+
auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
|
|
61
|
+
session: {
|
|
62
|
+
instanceId: 'instance-abc',
|
|
63
|
+
currentSessionId: sessionId,
|
|
64
|
+
currentPersona: 'Wave',
|
|
65
|
+
tokenUsage: defaultTokenUsage,
|
|
66
|
+
},
|
|
67
|
+
updateSession: vi.fn(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// logProgress Tests
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe('logProgress', () => {
|
|
76
|
+
beforeEach(() => vi.clearAllMocks());
|
|
77
|
+
|
|
78
|
+
it('should log progress successfully', async () => {
|
|
79
|
+
const supabase = createMockSupabase({
|
|
80
|
+
insertResult: { data: null, error: null },
|
|
81
|
+
});
|
|
82
|
+
const ctx = createMockContext(supabase);
|
|
83
|
+
|
|
84
|
+
const result = await logProgress(
|
|
85
|
+
{
|
|
86
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
87
|
+
summary: 'Completed initial setup',
|
|
88
|
+
},
|
|
89
|
+
ctx
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(result.result).toMatchObject({
|
|
93
|
+
success: true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should log progress with task_id and details', async () => {
|
|
98
|
+
const supabase = createMockSupabase({
|
|
99
|
+
insertResult: { data: null, error: null },
|
|
100
|
+
});
|
|
101
|
+
const ctx = createMockContext(supabase);
|
|
102
|
+
|
|
103
|
+
await logProgress(
|
|
104
|
+
{
|
|
105
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
106
|
+
task_id: '456e4567-e89b-12d3-a456-426614174000',
|
|
107
|
+
summary: 'Working on feature',
|
|
108
|
+
details: 'Added the main component and started styling',
|
|
109
|
+
},
|
|
110
|
+
ctx
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
116
|
+
task_id: '456e4567-e89b-12d3-a456-426614174000',
|
|
117
|
+
summary: 'Working on feature',
|
|
118
|
+
details: 'Added the main component and started styling',
|
|
119
|
+
created_by: 'agent',
|
|
120
|
+
created_by_session_id: 'session-123',
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should include session_id in insert', async () => {
|
|
126
|
+
const supabase = createMockSupabase({
|
|
127
|
+
insertResult: { data: null, error: null },
|
|
128
|
+
});
|
|
129
|
+
const ctx = createMockContext(supabase, { sessionId: 'my-session' });
|
|
130
|
+
|
|
131
|
+
await logProgress(
|
|
132
|
+
{
|
|
133
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
134
|
+
summary: 'Progress update',
|
|
135
|
+
},
|
|
136
|
+
ctx
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(supabase.insert).toHaveBeenCalledWith(
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
created_by: 'agent',
|
|
142
|
+
created_by_session_id: 'my-session',
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should query progress_logs table', async () => {
|
|
148
|
+
const supabase = createMockSupabase({
|
|
149
|
+
insertResult: { data: null, error: null },
|
|
150
|
+
});
|
|
151
|
+
const ctx = createMockContext(supabase);
|
|
152
|
+
|
|
153
|
+
await logProgress(
|
|
154
|
+
{
|
|
155
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
156
|
+
summary: 'Progress',
|
|
157
|
+
},
|
|
158
|
+
ctx
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(supabase.from).toHaveBeenCalledWith('progress_logs');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should throw error when insert fails', async () => {
|
|
165
|
+
const supabase = createMockSupabase({
|
|
166
|
+
insertResult: { data: null, error: { message: 'Insert failed' } },
|
|
167
|
+
});
|
|
168
|
+
const ctx = createMockContext(supabase);
|
|
169
|
+
|
|
170
|
+
await expect(
|
|
171
|
+
logProgress({
|
|
172
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
173
|
+
summary: 'Progress',
|
|
174
|
+
}, ctx)
|
|
175
|
+
).rejects.toThrow('Failed to log progress: Insert failed');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// getActivityFeed Tests
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
describe('getActivityFeed', () => {
|
|
184
|
+
beforeEach(() => vi.clearAllMocks());
|
|
185
|
+
|
|
186
|
+
it('should throw error for missing project_id', async () => {
|
|
187
|
+
const supabase = createMockSupabase();
|
|
188
|
+
const ctx = createMockContext(supabase);
|
|
189
|
+
|
|
190
|
+
await expect(getActivityFeed({}, ctx)).rejects.toThrow(ValidationError);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
194
|
+
const supabase = createMockSupabase();
|
|
195
|
+
const ctx = createMockContext(supabase);
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
getActivityFeed({ project_id: 'invalid' }, ctx)
|
|
199
|
+
).rejects.toThrow(ValidationError);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return empty activities when no data', async () => {
|
|
203
|
+
const supabase = createMockSupabase({
|
|
204
|
+
selectResult: { data: [], error: null },
|
|
205
|
+
});
|
|
206
|
+
const ctx = createMockContext(supabase);
|
|
207
|
+
|
|
208
|
+
const result = await getActivityFeed(
|
|
209
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
210
|
+
ctx
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(result.result).toMatchObject({
|
|
214
|
+
activities: [],
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should query all activity types by default', async () => {
|
|
219
|
+
const supabase = createMockSupabase({
|
|
220
|
+
selectResult: { data: [], error: null },
|
|
221
|
+
});
|
|
222
|
+
const ctx = createMockContext(supabase);
|
|
223
|
+
|
|
224
|
+
await getActivityFeed(
|
|
225
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
226
|
+
ctx
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Should query tasks, progress_logs, blockers, decisions
|
|
230
|
+
expect(supabase.from).toHaveBeenCalledWith('tasks');
|
|
231
|
+
expect(supabase.from).toHaveBeenCalledWith('progress_logs');
|
|
232
|
+
expect(supabase.from).toHaveBeenCalledWith('blockers');
|
|
233
|
+
expect(supabase.from).toHaveBeenCalledWith('decisions');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should filter by specific types', async () => {
|
|
237
|
+
const supabase = createMockSupabase({
|
|
238
|
+
selectResult: { data: [], error: null },
|
|
239
|
+
});
|
|
240
|
+
const ctx = createMockContext(supabase);
|
|
241
|
+
|
|
242
|
+
await getActivityFeed(
|
|
243
|
+
{
|
|
244
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
245
|
+
types: ['task', 'blocker'],
|
|
246
|
+
},
|
|
247
|
+
ctx
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Should only query tasks and blockers
|
|
251
|
+
expect(supabase.from).toHaveBeenCalledWith('tasks');
|
|
252
|
+
expect(supabase.from).toHaveBeenCalledWith('blockers');
|
|
253
|
+
expect(supabase.from).not.toHaveBeenCalledWith('progress_logs');
|
|
254
|
+
expect(supabase.from).not.toHaveBeenCalledWith('decisions');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should respect limit parameter', async () => {
|
|
258
|
+
const supabase = createMockSupabase({
|
|
259
|
+
selectResult: { data: [], error: null },
|
|
260
|
+
});
|
|
261
|
+
const ctx = createMockContext(supabase);
|
|
262
|
+
|
|
263
|
+
await getActivityFeed(
|
|
264
|
+
{
|
|
265
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
266
|
+
limit: 10,
|
|
267
|
+
},
|
|
268
|
+
ctx
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(supabase.limit).toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should cap limit at 200', async () => {
|
|
275
|
+
const supabase = createMockSupabase({
|
|
276
|
+
selectResult: { data: [], error: null },
|
|
277
|
+
});
|
|
278
|
+
const ctx = createMockContext(supabase);
|
|
279
|
+
|
|
280
|
+
const result = await getActivityFeed(
|
|
281
|
+
{
|
|
282
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
283
|
+
limit: 500,
|
|
284
|
+
},
|
|
285
|
+
ctx
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Should return at most 200 items
|
|
289
|
+
expect((result.result as { activities: unknown[] }).activities.length).toBeLessThanOrEqual(200);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should sort activities by created_at descending', async () => {
|
|
293
|
+
const mockTasks = [
|
|
294
|
+
{ id: 't1', title: 'Task 1', status: 'pending', created_by: 'agent', created_at: '2025-01-14T10:00:00Z' },
|
|
295
|
+
];
|
|
296
|
+
const mockProgress = [
|
|
297
|
+
{ id: 'p1', summary: 'Progress 1', created_by: 'agent', created_at: '2025-01-14T11:00:00Z' },
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
let callCount = 0;
|
|
301
|
+
const supabase = {
|
|
302
|
+
from: vi.fn().mockReturnThis(),
|
|
303
|
+
select: vi.fn().mockReturnThis(),
|
|
304
|
+
eq: vi.fn().mockReturnThis(),
|
|
305
|
+
gt: vi.fn().mockReturnThis(),
|
|
306
|
+
order: vi.fn().mockReturnThis(),
|
|
307
|
+
limit: vi.fn().mockReturnThis(),
|
|
308
|
+
then: vi.fn((resolve: (val: unknown) => void) => {
|
|
309
|
+
callCount++;
|
|
310
|
+
if (callCount === 1) {
|
|
311
|
+
return Promise.resolve({ data: mockTasks, error: null }).then(resolve);
|
|
312
|
+
}
|
|
313
|
+
if (callCount === 2) {
|
|
314
|
+
return Promise.resolve({ data: mockProgress, error: null }).then(resolve);
|
|
315
|
+
}
|
|
316
|
+
return Promise.resolve({ data: [], error: null }).then(resolve);
|
|
317
|
+
}),
|
|
318
|
+
} as unknown as SupabaseClient;
|
|
319
|
+
|
|
320
|
+
const ctx = createMockContext(supabase);
|
|
321
|
+
|
|
322
|
+
const result = await getActivityFeed(
|
|
323
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
324
|
+
ctx
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const activities = (result.result as { activities: { created_at: string }[] }).activities;
|
|
328
|
+
// Should be sorted by date descending (most recent first)
|
|
329
|
+
for (let i = 1; i < activities.length; i++) {
|
|
330
|
+
const prevDate = new Date(activities[i - 1].created_at).getTime();
|
|
331
|
+
const currDate = new Date(activities[i].created_at).getTime();
|
|
332
|
+
expect(prevDate).toBeGreaterThanOrEqual(currDate);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should filter by created_by', async () => {
|
|
337
|
+
const supabase = createMockSupabase({
|
|
338
|
+
selectResult: { data: [], error: null },
|
|
339
|
+
});
|
|
340
|
+
const ctx = createMockContext(supabase);
|
|
341
|
+
|
|
342
|
+
await getActivityFeed(
|
|
343
|
+
{
|
|
344
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
345
|
+
created_by: 'agent',
|
|
346
|
+
},
|
|
347
|
+
ctx
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(supabase.eq).toHaveBeenCalledWith('created_by', 'agent');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should filter by since parameter', async () => {
|
|
354
|
+
const supabase = createMockSupabase({
|
|
355
|
+
selectResult: { data: [], error: null },
|
|
356
|
+
});
|
|
357
|
+
const ctx = createMockContext(supabase);
|
|
358
|
+
|
|
359
|
+
await getActivityFeed(
|
|
360
|
+
{
|
|
361
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
362
|
+
since: '2025-01-14T00:00:00Z',
|
|
363
|
+
},
|
|
364
|
+
ctx
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(supabase.gt).toHaveBeenCalledWith('created_at', '2025-01-14T00:00:00Z');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles progress logging and activity feed:
|
|
5
|
+
* - log_progress
|
|
6
|
+
* - get_activity_feed
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
10
|
+
import { validateRequired, validateUUID } from '../validators.js';
|
|
11
|
+
|
|
12
|
+
export const logProgress: Handler = async (args, ctx) => {
|
|
13
|
+
const { project_id, task_id, summary, details } = args as {
|
|
14
|
+
project_id: string;
|
|
15
|
+
task_id?: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
details?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { supabase, session } = ctx;
|
|
21
|
+
|
|
22
|
+
const { error } = await supabase
|
|
23
|
+
.from('progress_logs')
|
|
24
|
+
.insert({
|
|
25
|
+
project_id,
|
|
26
|
+
task_id: task_id || null,
|
|
27
|
+
summary,
|
|
28
|
+
details: details || null,
|
|
29
|
+
created_by: 'agent',
|
|
30
|
+
created_by_session_id: session.currentSessionId,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (error) throw new Error(`Failed to log progress: ${error.message}`);
|
|
34
|
+
|
|
35
|
+
return { result: { success: true } };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getActivityFeed: Handler = async (args, ctx) => {
|
|
39
|
+
const { project_id, types, created_by, since, limit = 50 } = args as {
|
|
40
|
+
project_id: string;
|
|
41
|
+
types?: ('task' | 'progress' | 'blocker' | 'decision')[];
|
|
42
|
+
created_by?: 'agent' | 'user';
|
|
43
|
+
since?: string;
|
|
44
|
+
limit?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
validateRequired(project_id, 'project_id');
|
|
48
|
+
validateUUID(project_id, 'project_id');
|
|
49
|
+
|
|
50
|
+
const { supabase } = ctx;
|
|
51
|
+
const effectiveLimit = Math.min(limit, 200);
|
|
52
|
+
const includeTypes = types || ['task', 'progress', 'blocker', 'decision'];
|
|
53
|
+
// Fetch slightly more per type to account for interleaving, but not full limit
|
|
54
|
+
const perTypeLimit = Math.min(Math.ceil(effectiveLimit / includeTypes.length) * 2, effectiveLimit);
|
|
55
|
+
|
|
56
|
+
interface ActivityItem {
|
|
57
|
+
type: string;
|
|
58
|
+
id: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
summary?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
status?: string;
|
|
63
|
+
created_by: string;
|
|
64
|
+
created_at: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const activities: ActivityItem[] = [];
|
|
68
|
+
|
|
69
|
+
// Fetch tasks if included
|
|
70
|
+
if (includeTypes.includes('task')) {
|
|
71
|
+
let query = supabase
|
|
72
|
+
.from('tasks')
|
|
73
|
+
.select('id, title, status, created_by, created_at, completed_at')
|
|
74
|
+
.eq('project_id', project_id)
|
|
75
|
+
.order('created_at', { ascending: false })
|
|
76
|
+
.limit(perTypeLimit);
|
|
77
|
+
|
|
78
|
+
if (created_by) query = query.eq('created_by', created_by);
|
|
79
|
+
if (since) query = query.gt('created_at', since);
|
|
80
|
+
|
|
81
|
+
const { data: tasks } = await query;
|
|
82
|
+
if (tasks) {
|
|
83
|
+
for (const task of tasks) {
|
|
84
|
+
activities.push({
|
|
85
|
+
type: 'task',
|
|
86
|
+
id: task.id,
|
|
87
|
+
title: task.title,
|
|
88
|
+
status: task.status,
|
|
89
|
+
created_by: task.created_by,
|
|
90
|
+
created_at: task.created_at,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fetch progress logs if included
|
|
97
|
+
if (includeTypes.includes('progress')) {
|
|
98
|
+
let query = supabase
|
|
99
|
+
.from('progress_logs')
|
|
100
|
+
.select('id, summary, created_by, created_at')
|
|
101
|
+
.eq('project_id', project_id)
|
|
102
|
+
.order('created_at', { ascending: false })
|
|
103
|
+
.limit(perTypeLimit);
|
|
104
|
+
|
|
105
|
+
if (created_by) query = query.eq('created_by', created_by);
|
|
106
|
+
if (since) query = query.gt('created_at', since);
|
|
107
|
+
|
|
108
|
+
const { data: logs } = await query;
|
|
109
|
+
if (logs) {
|
|
110
|
+
for (const log of logs) {
|
|
111
|
+
activities.push({
|
|
112
|
+
type: 'progress',
|
|
113
|
+
id: log.id,
|
|
114
|
+
summary: log.summary,
|
|
115
|
+
created_by: log.created_by,
|
|
116
|
+
created_at: log.created_at,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fetch blockers if included
|
|
123
|
+
if (includeTypes.includes('blocker')) {
|
|
124
|
+
let query = supabase
|
|
125
|
+
.from('blockers')
|
|
126
|
+
.select('id, description, status, created_by, created_at')
|
|
127
|
+
.eq('project_id', project_id)
|
|
128
|
+
.order('created_at', { ascending: false })
|
|
129
|
+
.limit(perTypeLimit);
|
|
130
|
+
|
|
131
|
+
if (created_by) query = query.eq('created_by', created_by);
|
|
132
|
+
if (since) query = query.gt('created_at', since);
|
|
133
|
+
|
|
134
|
+
const { data: blockers } = await query;
|
|
135
|
+
if (blockers) {
|
|
136
|
+
for (const blocker of blockers) {
|
|
137
|
+
activities.push({
|
|
138
|
+
type: 'blocker',
|
|
139
|
+
id: blocker.id,
|
|
140
|
+
description: blocker.description,
|
|
141
|
+
status: blocker.status,
|
|
142
|
+
created_by: blocker.created_by,
|
|
143
|
+
created_at: blocker.created_at,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fetch decisions if included
|
|
150
|
+
if (includeTypes.includes('decision')) {
|
|
151
|
+
let query = supabase
|
|
152
|
+
.from('decisions')
|
|
153
|
+
.select('id, title, created_by, created_at')
|
|
154
|
+
.eq('project_id', project_id)
|
|
155
|
+
.order('created_at', { ascending: false })
|
|
156
|
+
.limit(perTypeLimit);
|
|
157
|
+
|
|
158
|
+
if (created_by) query = query.eq('created_by', created_by);
|
|
159
|
+
if (since) query = query.gt('created_at', since);
|
|
160
|
+
|
|
161
|
+
const { data: decisions } = await query;
|
|
162
|
+
if (decisions) {
|
|
163
|
+
for (const decision of decisions) {
|
|
164
|
+
activities.push({
|
|
165
|
+
type: 'decision',
|
|
166
|
+
id: decision.id,
|
|
167
|
+
title: decision.title,
|
|
168
|
+
created_by: decision.created_by,
|
|
169
|
+
created_at: decision.created_at,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sort by created_at descending and limit
|
|
176
|
+
activities.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
177
|
+
const limitedActivities = activities.slice(0, effectiveLimit);
|
|
178
|
+
|
|
179
|
+
return { result: { activities: limitedActivities } };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Progress handlers registry
|
|
184
|
+
*/
|
|
185
|
+
export const progressHandlers: HandlerRegistry = {
|
|
186
|
+
log_progress: logProgress,
|
|
187
|
+
get_activity_feed: getActivityFeed,
|
|
188
|
+
};
|