@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,531 @@
|
|
|
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 { getPendingRequests, acknowledgeRequest, answerQuestion } from './requests.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
|
+
updateResult?: { data: unknown; error: unknown };
|
|
14
|
+
sessionsResult?: { data: unknown; error: unknown };
|
|
15
|
+
} = {}) {
|
|
16
|
+
const defaultResult = { data: null, error: null };
|
|
17
|
+
// Use an object to track state so it persists across all mock function calls
|
|
18
|
+
const state = {
|
|
19
|
+
currentOperation: 'select' as string,
|
|
20
|
+
currentTable: '' as string,
|
|
21
|
+
updateCalled: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const mock = {
|
|
25
|
+
from: vi.fn((table: string) => {
|
|
26
|
+
state.currentTable = table;
|
|
27
|
+
// Reset state for new query chain
|
|
28
|
+
state.currentOperation = 'select';
|
|
29
|
+
state.updateCalled = false;
|
|
30
|
+
return mock;
|
|
31
|
+
}),
|
|
32
|
+
select: vi.fn(() => {
|
|
33
|
+
// Don't reset updateCalled - we need to know if update was in this chain
|
|
34
|
+
if (!state.updateCalled) {
|
|
35
|
+
state.currentOperation = 'select';
|
|
36
|
+
}
|
|
37
|
+
return mock;
|
|
38
|
+
}),
|
|
39
|
+
update: vi.fn(() => {
|
|
40
|
+
state.currentOperation = 'update';
|
|
41
|
+
state.updateCalled = true;
|
|
42
|
+
return mock;
|
|
43
|
+
}),
|
|
44
|
+
eq: vi.fn().mockReturnThis(),
|
|
45
|
+
or: vi.fn().mockReturnThis(),
|
|
46
|
+
order: vi.fn().mockReturnThis(),
|
|
47
|
+
single: vi.fn(() => {
|
|
48
|
+
// If update was called in this chain, return updateResult
|
|
49
|
+
if (state.updateCalled) {
|
|
50
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
51
|
+
}
|
|
52
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
53
|
+
}),
|
|
54
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
55
|
+
if (state.currentTable === 'agent_sessions') {
|
|
56
|
+
return Promise.resolve(overrides.sessionsResult ?? { data: [], error: null }).then(resolve);
|
|
57
|
+
}
|
|
58
|
+
if (state.updateCalled) {
|
|
59
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
60
|
+
}
|
|
61
|
+
if (state.currentOperation === 'select') {
|
|
62
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
63
|
+
}
|
|
64
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return mock as unknown as SupabaseClient;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createMockContext(
|
|
72
|
+
supabase: SupabaseClient,
|
|
73
|
+
options: { sessionId?: string | null } = {}
|
|
74
|
+
): HandlerContext {
|
|
75
|
+
const defaultTokenUsage: TokenUsage = {
|
|
76
|
+
callCount: 5,
|
|
77
|
+
totalTokens: 2500,
|
|
78
|
+
byTool: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
supabase,
|
|
85
|
+
auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
|
|
86
|
+
session: {
|
|
87
|
+
instanceId: 'instance-abc',
|
|
88
|
+
currentSessionId: sessionId,
|
|
89
|
+
currentPersona: 'Wave',
|
|
90
|
+
tokenUsage: defaultTokenUsage,
|
|
91
|
+
},
|
|
92
|
+
updateSession: vi.fn(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// getPendingRequests Tests
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
describe('getPendingRequests', () => {
|
|
101
|
+
beforeEach(() => vi.clearAllMocks());
|
|
102
|
+
|
|
103
|
+
it('should throw error for missing project_id', async () => {
|
|
104
|
+
const supabase = createMockSupabase();
|
|
105
|
+
const ctx = createMockContext(supabase);
|
|
106
|
+
|
|
107
|
+
await expect(getPendingRequests({}, ctx)).rejects.toThrow(ValidationError);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
111
|
+
const supabase = createMockSupabase();
|
|
112
|
+
const ctx = createMockContext(supabase);
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
getPendingRequests({ project_id: 'invalid' }, ctx)
|
|
116
|
+
).rejects.toThrow(ValidationError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return empty list when no requests', async () => {
|
|
120
|
+
const supabase = createMockSupabase({
|
|
121
|
+
selectResult: { data: [], error: null },
|
|
122
|
+
sessionsResult: { data: [], error: null },
|
|
123
|
+
});
|
|
124
|
+
const ctx = createMockContext(supabase);
|
|
125
|
+
|
|
126
|
+
const result = await getPendingRequests(
|
|
127
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
128
|
+
ctx
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.result).toMatchObject({
|
|
132
|
+
requests: [],
|
|
133
|
+
count: 0,
|
|
134
|
+
questions_count: 0,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return pending requests', async () => {
|
|
139
|
+
const mockRequests = [
|
|
140
|
+
{
|
|
141
|
+
id: 'r1',
|
|
142
|
+
request_type: 'task',
|
|
143
|
+
content: 'Please do this',
|
|
144
|
+
session_id: null, // broadcast
|
|
145
|
+
acknowledged_at: null,
|
|
146
|
+
created_at: '2025-01-14T10:00:00Z',
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const supabase = createMockSupabase({
|
|
151
|
+
selectResult: { data: mockRequests, error: null },
|
|
152
|
+
sessionsResult: { data: [], error: null },
|
|
153
|
+
});
|
|
154
|
+
const ctx = createMockContext(supabase);
|
|
155
|
+
|
|
156
|
+
const result = await getPendingRequests(
|
|
157
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
158
|
+
ctx
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect((result.result as { count: number }).count).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should filter broadcast requests (session_id is null)', async () => {
|
|
165
|
+
const mockRequests = [
|
|
166
|
+
{
|
|
167
|
+
id: 'r1',
|
|
168
|
+
request_type: 'task',
|
|
169
|
+
content: 'Broadcast request',
|
|
170
|
+
session_id: null,
|
|
171
|
+
acknowledged_at: null,
|
|
172
|
+
created_at: '2025-01-14T10:00:00Z',
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const supabase = createMockSupabase({
|
|
177
|
+
selectResult: { data: mockRequests, error: null },
|
|
178
|
+
sessionsResult: { data: [], error: null },
|
|
179
|
+
});
|
|
180
|
+
const ctx = createMockContext(supabase);
|
|
181
|
+
|
|
182
|
+
const result = await getPendingRequests(
|
|
183
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
184
|
+
ctx
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Broadcast request should be included
|
|
188
|
+
expect((result.result as { count: number }).count).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should include requests targeted to current session', async () => {
|
|
192
|
+
const mockRequests = [
|
|
193
|
+
{
|
|
194
|
+
id: 'r1',
|
|
195
|
+
request_type: 'task',
|
|
196
|
+
content: 'Targeted request',
|
|
197
|
+
session_id: 'session-123',
|
|
198
|
+
acknowledged_at: null,
|
|
199
|
+
created_at: '2025-01-14T10:00:00Z',
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const supabase = createMockSupabase({
|
|
204
|
+
selectResult: { data: mockRequests, error: null },
|
|
205
|
+
sessionsResult: { data: [], error: null },
|
|
206
|
+
});
|
|
207
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
208
|
+
|
|
209
|
+
const result = await getPendingRequests(
|
|
210
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
211
|
+
ctx
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect((result.result as { count: number }).count).toBe(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should exclude requests targeted to other sessions', async () => {
|
|
218
|
+
const mockRequests = [
|
|
219
|
+
{
|
|
220
|
+
id: 'r1',
|
|
221
|
+
request_type: 'task',
|
|
222
|
+
content: 'Other session request',
|
|
223
|
+
session_id: 'other-session',
|
|
224
|
+
acknowledged_at: null,
|
|
225
|
+
created_at: '2025-01-14T10:00:00Z',
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const supabase = createMockSupabase({
|
|
230
|
+
selectResult: { data: mockRequests, error: null },
|
|
231
|
+
sessionsResult: { data: [{ id: 'other-session' }], error: null },
|
|
232
|
+
});
|
|
233
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
234
|
+
|
|
235
|
+
const result = await getPendingRequests(
|
|
236
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
237
|
+
ctx
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Should be excluded because targeted to an active other session
|
|
241
|
+
expect((result.result as { count: number }).count).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should count unanswered questions', async () => {
|
|
245
|
+
const mockRequests = [
|
|
246
|
+
{
|
|
247
|
+
id: 'r1',
|
|
248
|
+
request_type: 'question',
|
|
249
|
+
content: 'What is this?',
|
|
250
|
+
session_id: null,
|
|
251
|
+
acknowledged_at: null,
|
|
252
|
+
answered_at: null,
|
|
253
|
+
created_at: '2025-01-14T10:00:00Z',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'r2',
|
|
257
|
+
request_type: 'task',
|
|
258
|
+
content: 'Do this',
|
|
259
|
+
session_id: null,
|
|
260
|
+
acknowledged_at: null,
|
|
261
|
+
created_at: '2025-01-14T11:00:00Z',
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const supabase = createMockSupabase({
|
|
266
|
+
selectResult: { data: mockRequests, error: null },
|
|
267
|
+
sessionsResult: { data: [], error: null },
|
|
268
|
+
});
|
|
269
|
+
const ctx = createMockContext(supabase);
|
|
270
|
+
|
|
271
|
+
const result = await getPendingRequests(
|
|
272
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
273
|
+
ctx
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect((result.result as { questions_count: number }).questions_count).toBe(1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should add wait_minutes to requests', async () => {
|
|
280
|
+
const mockRequests = [
|
|
281
|
+
{
|
|
282
|
+
id: 'r1',
|
|
283
|
+
request_type: 'task',
|
|
284
|
+
content: 'Request',
|
|
285
|
+
session_id: null,
|
|
286
|
+
acknowledged_at: null,
|
|
287
|
+
created_at: new Date(Date.now() - 5 * 60000).toISOString(), // 5 minutes ago
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const supabase = createMockSupabase({
|
|
292
|
+
selectResult: { data: mockRequests, error: null },
|
|
293
|
+
sessionsResult: { data: [], error: null },
|
|
294
|
+
});
|
|
295
|
+
const ctx = createMockContext(supabase);
|
|
296
|
+
|
|
297
|
+
const result = await getPendingRequests(
|
|
298
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
299
|
+
ctx
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const requests = (result.result as { requests: { wait_minutes: number }[] }).requests;
|
|
303
|
+
expect(requests[0].wait_minutes).toBeGreaterThanOrEqual(4); // Allow some margin
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should query agent_requests table', async () => {
|
|
307
|
+
const supabase = createMockSupabase({
|
|
308
|
+
selectResult: { data: [], error: null },
|
|
309
|
+
sessionsResult: { data: [], error: null },
|
|
310
|
+
});
|
|
311
|
+
const ctx = createMockContext(supabase);
|
|
312
|
+
|
|
313
|
+
await getPendingRequests(
|
|
314
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
315
|
+
ctx
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(supabase.from).toHaveBeenCalledWith('agent_requests');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ============================================================================
|
|
323
|
+
// acknowledgeRequest Tests
|
|
324
|
+
// ============================================================================
|
|
325
|
+
|
|
326
|
+
describe('acknowledgeRequest', () => {
|
|
327
|
+
beforeEach(() => vi.clearAllMocks());
|
|
328
|
+
|
|
329
|
+
it('should throw error for missing request_id', async () => {
|
|
330
|
+
const supabase = createMockSupabase();
|
|
331
|
+
const ctx = createMockContext(supabase);
|
|
332
|
+
|
|
333
|
+
await expect(acknowledgeRequest({}, ctx)).rejects.toThrow(ValidationError);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should throw error for invalid request_id UUID', async () => {
|
|
337
|
+
const supabase = createMockSupabase();
|
|
338
|
+
const ctx = createMockContext(supabase);
|
|
339
|
+
|
|
340
|
+
await expect(
|
|
341
|
+
acknowledgeRequest({ request_id: 'invalid' }, ctx)
|
|
342
|
+
).rejects.toThrow(ValidationError);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should acknowledge request successfully', async () => {
|
|
346
|
+
const mockRequest = {
|
|
347
|
+
id: 'r1',
|
|
348
|
+
request_type: 'task',
|
|
349
|
+
content: 'Do this',
|
|
350
|
+
acknowledged_at: '2025-01-14T12:00:00Z',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const supabase = createMockSupabase({
|
|
354
|
+
updateResult: { data: mockRequest, error: null },
|
|
355
|
+
});
|
|
356
|
+
const ctx = createMockContext(supabase);
|
|
357
|
+
|
|
358
|
+
const result = await acknowledgeRequest(
|
|
359
|
+
{ request_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
360
|
+
ctx
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(result.result).toMatchObject({
|
|
364
|
+
success: true,
|
|
365
|
+
request: mockRequest,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should set acknowledged_at and acknowledged_by_session_id', async () => {
|
|
370
|
+
const supabase = createMockSupabase({
|
|
371
|
+
updateResult: { data: { id: 'r1' }, error: null },
|
|
372
|
+
});
|
|
373
|
+
const ctx = createMockContext(supabase, { sessionId: 'my-session' });
|
|
374
|
+
|
|
375
|
+
await acknowledgeRequest(
|
|
376
|
+
{ request_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
377
|
+
ctx
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
381
|
+
expect.objectContaining({
|
|
382
|
+
acknowledged_at: expect.any(String),
|
|
383
|
+
acknowledged_by_session_id: 'my-session',
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should query agent_requests table', async () => {
|
|
389
|
+
const supabase = createMockSupabase({
|
|
390
|
+
updateResult: { data: { id: 'r1' }, error: null },
|
|
391
|
+
});
|
|
392
|
+
const ctx = createMockContext(supabase);
|
|
393
|
+
|
|
394
|
+
await acknowledgeRequest(
|
|
395
|
+
{ request_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
396
|
+
ctx
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(supabase.from).toHaveBeenCalledWith('agent_requests');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should throw error when update fails', async () => {
|
|
403
|
+
const supabase = createMockSupabase({
|
|
404
|
+
updateResult: { data: null, error: { message: 'Update failed' } },
|
|
405
|
+
});
|
|
406
|
+
const ctx = createMockContext(supabase);
|
|
407
|
+
|
|
408
|
+
await expect(
|
|
409
|
+
acknowledgeRequest({ request_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
410
|
+
).rejects.toThrow();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ============================================================================
|
|
415
|
+
// answerQuestion Tests
|
|
416
|
+
// ============================================================================
|
|
417
|
+
|
|
418
|
+
describe('answerQuestion', () => {
|
|
419
|
+
beforeEach(() => vi.clearAllMocks());
|
|
420
|
+
|
|
421
|
+
it('should throw error for missing request_id', async () => {
|
|
422
|
+
const supabase = createMockSupabase();
|
|
423
|
+
const ctx = createMockContext(supabase);
|
|
424
|
+
|
|
425
|
+
await expect(
|
|
426
|
+
answerQuestion({ answer: 'The answer' }, ctx)
|
|
427
|
+
).rejects.toThrow(ValidationError);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should throw error for missing answer', async () => {
|
|
431
|
+
const supabase = createMockSupabase();
|
|
432
|
+
const ctx = createMockContext(supabase);
|
|
433
|
+
|
|
434
|
+
await expect(
|
|
435
|
+
answerQuestion({ request_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
436
|
+
).rejects.toThrow(ValidationError);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should throw error for invalid request_id UUID', async () => {
|
|
440
|
+
const supabase = createMockSupabase();
|
|
441
|
+
const ctx = createMockContext(supabase);
|
|
442
|
+
|
|
443
|
+
await expect(
|
|
444
|
+
answerQuestion({ request_id: 'invalid', answer: 'The answer' }, ctx)
|
|
445
|
+
).rejects.toThrow(ValidationError);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should answer question successfully', async () => {
|
|
449
|
+
const mockRequest = {
|
|
450
|
+
id: 'r1',
|
|
451
|
+
request_type: 'question',
|
|
452
|
+
content: 'What is this?',
|
|
453
|
+
answer: 'This is the answer',
|
|
454
|
+
answered_at: '2025-01-14T12:00:00Z',
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const supabase = createMockSupabase({
|
|
458
|
+
updateResult: { data: mockRequest, error: null },
|
|
459
|
+
});
|
|
460
|
+
const ctx = createMockContext(supabase);
|
|
461
|
+
|
|
462
|
+
const result = await answerQuestion(
|
|
463
|
+
{
|
|
464
|
+
request_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
465
|
+
answer: 'This is the answer',
|
|
466
|
+
},
|
|
467
|
+
ctx
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(result.result).toMatchObject({
|
|
471
|
+
success: true,
|
|
472
|
+
message: 'Question answered successfully',
|
|
473
|
+
request: mockRequest,
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should set answer, answered_at, and acknowledgment fields', async () => {
|
|
478
|
+
const supabase = createMockSupabase({
|
|
479
|
+
updateResult: { data: { id: 'r1' }, error: null },
|
|
480
|
+
});
|
|
481
|
+
const ctx = createMockContext(supabase, { sessionId: 'my-session' });
|
|
482
|
+
|
|
483
|
+
await answerQuestion(
|
|
484
|
+
{
|
|
485
|
+
request_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
486
|
+
answer: 'The answer to the question',
|
|
487
|
+
},
|
|
488
|
+
ctx
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
492
|
+
expect.objectContaining({
|
|
493
|
+
answer: 'The answer to the question',
|
|
494
|
+
answered_at: expect.any(String),
|
|
495
|
+
acknowledged_at: expect.any(String),
|
|
496
|
+
acknowledged_by_session_id: 'my-session',
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should query agent_requests table', async () => {
|
|
502
|
+
const supabase = createMockSupabase({
|
|
503
|
+
updateResult: { data: { id: 'r1' }, error: null },
|
|
504
|
+
});
|
|
505
|
+
const ctx = createMockContext(supabase);
|
|
506
|
+
|
|
507
|
+
await answerQuestion(
|
|
508
|
+
{
|
|
509
|
+
request_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
510
|
+
answer: 'Answer',
|
|
511
|
+
},
|
|
512
|
+
ctx
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
expect(supabase.from).toHaveBeenCalledWith('agent_requests');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should throw error when update fails', async () => {
|
|
519
|
+
const supabase = createMockSupabase({
|
|
520
|
+
updateResult: { data: null, error: { message: 'Update failed' } },
|
|
521
|
+
});
|
|
522
|
+
const ctx = createMockContext(supabase);
|
|
523
|
+
|
|
524
|
+
await expect(
|
|
525
|
+
answerQuestion({
|
|
526
|
+
request_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
527
|
+
answer: 'Answer',
|
|
528
|
+
}, ctx)
|
|
529
|
+
).rejects.toThrow();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requests Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles user request handling:
|
|
5
|
+
* - get_pending_requests
|
|
6
|
+
* - acknowledge_request
|
|
7
|
+
* - answer_question
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
11
|
+
import { validateRequired, validateUUID } from '../validators.js';
|
|
12
|
+
|
|
13
|
+
export const getPendingRequests: Handler = async (args, ctx) => {
|
|
14
|
+
const { project_id } = args as { project_id: string };
|
|
15
|
+
|
|
16
|
+
validateRequired(project_id, 'project_id');
|
|
17
|
+
validateUUID(project_id, 'project_id');
|
|
18
|
+
|
|
19
|
+
const { supabase, session } = ctx;
|
|
20
|
+
const currentSessionId = session.currentSessionId;
|
|
21
|
+
|
|
22
|
+
// Get active session IDs to identify orphaned questions
|
|
23
|
+
const { data: activeSessions } = await supabase
|
|
24
|
+
.from('agent_sessions')
|
|
25
|
+
.select('id')
|
|
26
|
+
.eq('project_id', project_id)
|
|
27
|
+
.eq('status', 'active');
|
|
28
|
+
const activeSessionIds = new Set((activeSessions || []).map((s) => s.id));
|
|
29
|
+
|
|
30
|
+
// Get pending requests for this project:
|
|
31
|
+
// - Unacknowledged requests OR unanswered questions (questions need answers, not just acknowledgment)
|
|
32
|
+
const { data: requests, error } = await supabase
|
|
33
|
+
.from('agent_requests')
|
|
34
|
+
.select('*')
|
|
35
|
+
.eq('project_id', project_id)
|
|
36
|
+
.or('acknowledged_at.is.null,and(request_type.eq.question,answered_at.is.null)')
|
|
37
|
+
.order('created_at', { ascending: false });
|
|
38
|
+
|
|
39
|
+
if (error) throw error;
|
|
40
|
+
|
|
41
|
+
// Filter to requests this agent can handle
|
|
42
|
+
const filteredRequests = (requests || []).filter((r) => {
|
|
43
|
+
// Broadcast requests (session_id is null) - anyone can handle
|
|
44
|
+
if (!r.session_id) return true;
|
|
45
|
+
// Targeted to this session
|
|
46
|
+
if (r.session_id === currentSessionId) return true;
|
|
47
|
+
// Orphaned questions (targeted session is disconnected) - any agent can answer
|
|
48
|
+
if (r.request_type === 'question' && !activeSessionIds.has(r.session_id)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Sort questions first (highest priority) and add wait times
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const sortedRequests = filteredRequests
|
|
55
|
+
.map((r) => ({
|
|
56
|
+
...r,
|
|
57
|
+
wait_minutes: Math.floor((now.getTime() - new Date(r.created_at).getTime()) / 60000),
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => {
|
|
60
|
+
// Questions first, then by created_at (oldest first for urgency)
|
|
61
|
+
const aIsQuestion = a.request_type === 'question' && !a.answered_at;
|
|
62
|
+
const bIsQuestion = b.request_type === 'question' && !b.answered_at;
|
|
63
|
+
if (aIsQuestion && !bIsQuestion) return -1;
|
|
64
|
+
if (!aIsQuestion && bIsQuestion) return 1;
|
|
65
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Count unanswered questions separately
|
|
69
|
+
const questionsCount = sortedRequests.filter(
|
|
70
|
+
(r) => r.request_type === 'question' && !r.answered_at
|
|
71
|
+
).length;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
result: {
|
|
75
|
+
requests: sortedRequests,
|
|
76
|
+
count: sortedRequests.length,
|
|
77
|
+
questions_count: questionsCount,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const acknowledgeRequest: Handler = async (args, ctx) => {
|
|
83
|
+
const { request_id } = args as { request_id: string };
|
|
84
|
+
|
|
85
|
+
validateRequired(request_id, 'request_id');
|
|
86
|
+
validateUUID(request_id, 'request_id');
|
|
87
|
+
|
|
88
|
+
const { supabase, session } = ctx;
|
|
89
|
+
|
|
90
|
+
const { data: request, error } = await supabase
|
|
91
|
+
.from('agent_requests')
|
|
92
|
+
.update({
|
|
93
|
+
acknowledged_at: new Date().toISOString(),
|
|
94
|
+
acknowledged_by_session_id: session.currentSessionId,
|
|
95
|
+
})
|
|
96
|
+
.eq('id', request_id)
|
|
97
|
+
.select()
|
|
98
|
+
.single();
|
|
99
|
+
|
|
100
|
+
if (error) throw error;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
result: {
|
|
104
|
+
success: true,
|
|
105
|
+
request,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const answerQuestion: Handler = async (args, ctx) => {
|
|
111
|
+
const { request_id, answer } = args as { request_id: string; answer: string };
|
|
112
|
+
|
|
113
|
+
validateRequired(request_id, 'request_id');
|
|
114
|
+
validateRequired(answer, 'answer');
|
|
115
|
+
validateUUID(request_id, 'request_id');
|
|
116
|
+
|
|
117
|
+
const { supabase, session } = ctx;
|
|
118
|
+
|
|
119
|
+
// Update the request with the answer
|
|
120
|
+
const { data: request, error } = await supabase
|
|
121
|
+
.from('agent_requests')
|
|
122
|
+
.update({
|
|
123
|
+
answer,
|
|
124
|
+
answered_at: new Date().toISOString(),
|
|
125
|
+
acknowledged_at: new Date().toISOString(),
|
|
126
|
+
acknowledged_by_session_id: session.currentSessionId,
|
|
127
|
+
})
|
|
128
|
+
.eq('id', request_id)
|
|
129
|
+
.select()
|
|
130
|
+
.single();
|
|
131
|
+
|
|
132
|
+
if (error) throw error;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
result: {
|
|
136
|
+
success: true,
|
|
137
|
+
message: 'Question answered successfully',
|
|
138
|
+
request,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Requests handlers registry
|
|
145
|
+
*/
|
|
146
|
+
export const requestHandlers: HandlerRegistry = {
|
|
147
|
+
get_pending_requests: getPendingRequests,
|
|
148
|
+
acknowledge_request: acknowledgeRequest,
|
|
149
|
+
answer_question: answerQuestion,
|
|
150
|
+
};
|