@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,459 @@
|
|
|
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
|
+
heartbeat,
|
|
6
|
+
endWorkSession,
|
|
7
|
+
getHelp,
|
|
8
|
+
getTokenUsage,
|
|
9
|
+
} from './session.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Test Utilities
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a mock Supabase client with chainable methods
|
|
17
|
+
*/
|
|
18
|
+
function createMockSupabase(overrides: {
|
|
19
|
+
selectResult?: { data: unknown; error: unknown };
|
|
20
|
+
insertResult?: { data: unknown; error: unknown };
|
|
21
|
+
updateResult?: { data: unknown; error: unknown };
|
|
22
|
+
} = {}) {
|
|
23
|
+
const defaultResult = { data: null, error: null };
|
|
24
|
+
|
|
25
|
+
let currentOperation = 'select';
|
|
26
|
+
let insertThenSelect = false;
|
|
27
|
+
|
|
28
|
+
const mock = {
|
|
29
|
+
from: vi.fn().mockReturnThis(),
|
|
30
|
+
select: vi.fn(() => {
|
|
31
|
+
if (currentOperation === 'insert') {
|
|
32
|
+
insertThenSelect = true;
|
|
33
|
+
} else {
|
|
34
|
+
currentOperation = 'select';
|
|
35
|
+
insertThenSelect = false;
|
|
36
|
+
}
|
|
37
|
+
return mock;
|
|
38
|
+
}),
|
|
39
|
+
insert: vi.fn(() => {
|
|
40
|
+
currentOperation = 'insert';
|
|
41
|
+
insertThenSelect = false;
|
|
42
|
+
return mock;
|
|
43
|
+
}),
|
|
44
|
+
update: vi.fn(() => {
|
|
45
|
+
currentOperation = 'update';
|
|
46
|
+
insertThenSelect = false;
|
|
47
|
+
return mock;
|
|
48
|
+
}),
|
|
49
|
+
delete: vi.fn(() => {
|
|
50
|
+
currentOperation = 'delete';
|
|
51
|
+
insertThenSelect = false;
|
|
52
|
+
return mock;
|
|
53
|
+
}),
|
|
54
|
+
eq: vi.fn().mockReturnThis(),
|
|
55
|
+
neq: vi.fn().mockReturnThis(),
|
|
56
|
+
in: vi.fn().mockReturnThis(),
|
|
57
|
+
is: vi.fn().mockReturnThis(),
|
|
58
|
+
not: vi.fn().mockReturnThis(),
|
|
59
|
+
or: vi.fn().mockReturnThis(),
|
|
60
|
+
gte: vi.fn().mockReturnThis(),
|
|
61
|
+
lt: vi.fn().mockReturnThis(),
|
|
62
|
+
order: vi.fn().mockReturnThis(),
|
|
63
|
+
limit: vi.fn().mockReturnThis(),
|
|
64
|
+
single: vi.fn(() => {
|
|
65
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
66
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult);
|
|
67
|
+
}
|
|
68
|
+
if (currentOperation === 'select') {
|
|
69
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
70
|
+
}
|
|
71
|
+
if (currentOperation === 'update') {
|
|
72
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
73
|
+
}
|
|
74
|
+
return Promise.resolve(defaultResult);
|
|
75
|
+
}),
|
|
76
|
+
maybeSingle: vi.fn(() => {
|
|
77
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
78
|
+
}),
|
|
79
|
+
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
80
|
+
if (currentOperation === 'insert' || insertThenSelect) {
|
|
81
|
+
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
82
|
+
}
|
|
83
|
+
if (currentOperation === 'select') {
|
|
84
|
+
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
85
|
+
}
|
|
86
|
+
if (currentOperation === 'update') {
|
|
87
|
+
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
88
|
+
}
|
|
89
|
+
return Promise.resolve(defaultResult).then(resolve);
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return mock as unknown as SupabaseClient;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a mock handler context with full session state
|
|
98
|
+
*/
|
|
99
|
+
function createMockContext(
|
|
100
|
+
supabase: SupabaseClient,
|
|
101
|
+
options: {
|
|
102
|
+
sessionId?: string | null;
|
|
103
|
+
tokenUsage?: TokenUsage;
|
|
104
|
+
} = {}
|
|
105
|
+
): HandlerContext {
|
|
106
|
+
const defaultTokenUsage: TokenUsage = {
|
|
107
|
+
callCount: 10,
|
|
108
|
+
totalTokens: 5000,
|
|
109
|
+
byTool: {
|
|
110
|
+
get_tasks: { calls: 3, tokens: 1500 },
|
|
111
|
+
update_task: { calls: 4, tokens: 2000 },
|
|
112
|
+
complete_task: { calls: 3, tokens: 1500 },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Use 'in' check to allow explicit null values for sessionId
|
|
117
|
+
const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
supabase,
|
|
121
|
+
auth: {
|
|
122
|
+
userId: 'user-123',
|
|
123
|
+
apiKeyId: 'api-key-123',
|
|
124
|
+
},
|
|
125
|
+
session: {
|
|
126
|
+
instanceId: 'instance-abc123',
|
|
127
|
+
currentSessionId: sessionId,
|
|
128
|
+
currentPersona: 'Wave',
|
|
129
|
+
tokenUsage: options.tokenUsage ?? defaultTokenUsage,
|
|
130
|
+
},
|
|
131
|
+
updateSession: vi.fn(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// heartbeat Tests
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
describe('heartbeat', () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.clearAllMocks();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should record heartbeat successfully', async () => {
|
|
145
|
+
const supabase = createMockSupabase({
|
|
146
|
+
insertResult: { data: null, error: null },
|
|
147
|
+
updateResult: { data: null, error: null },
|
|
148
|
+
});
|
|
149
|
+
const ctx = createMockContext(supabase);
|
|
150
|
+
|
|
151
|
+
const result = await heartbeat({}, ctx);
|
|
152
|
+
|
|
153
|
+
expect(result.result).toMatchObject({
|
|
154
|
+
success: true,
|
|
155
|
+
session_id: 'session-123',
|
|
156
|
+
});
|
|
157
|
+
expect(result.result).toHaveProperty('timestamp');
|
|
158
|
+
expect(supabase.from).toHaveBeenCalledWith('agent_heartbeats');
|
|
159
|
+
expect(supabase.insert).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should use provided session_id over current session', async () => {
|
|
163
|
+
const supabase = createMockSupabase({
|
|
164
|
+
insertResult: { data: null, error: null },
|
|
165
|
+
updateResult: { data: null, error: null },
|
|
166
|
+
});
|
|
167
|
+
const ctx = createMockContext(supabase);
|
|
168
|
+
|
|
169
|
+
const result = await heartbeat({ session_id: 'other-session-456' }, ctx);
|
|
170
|
+
|
|
171
|
+
expect(result.result).toMatchObject({
|
|
172
|
+
success: true,
|
|
173
|
+
session_id: 'other-session-456',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return error when no active session', async () => {
|
|
178
|
+
const supabase = createMockSupabase();
|
|
179
|
+
const ctx = createMockContext(supabase, { sessionId: null });
|
|
180
|
+
|
|
181
|
+
const result = await heartbeat({}, ctx);
|
|
182
|
+
|
|
183
|
+
expect(result.result).toMatchObject({
|
|
184
|
+
error: 'No active session. Call start_work_session first.',
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should update session with token usage', async () => {
|
|
189
|
+
const supabase = createMockSupabase({
|
|
190
|
+
insertResult: { data: null, error: null },
|
|
191
|
+
updateResult: { data: null, error: null },
|
|
192
|
+
});
|
|
193
|
+
const ctx = createMockContext(supabase);
|
|
194
|
+
|
|
195
|
+
await heartbeat({}, ctx);
|
|
196
|
+
|
|
197
|
+
expect(supabase.from).toHaveBeenCalledWith('agent_sessions');
|
|
198
|
+
expect(supabase.update).toHaveBeenCalledWith(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
status: 'active',
|
|
201
|
+
total_tokens: 5000,
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// getHelp Tests
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
describe('getHelp', () => {
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
vi.clearAllMocks();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should return help content for valid topic', async () => {
|
|
217
|
+
const supabase = createMockSupabase();
|
|
218
|
+
const ctx = createMockContext(supabase);
|
|
219
|
+
|
|
220
|
+
const result = await getHelp({ topic: 'tasks' }, ctx);
|
|
221
|
+
|
|
222
|
+
expect(result.result).toHaveProperty('topic', 'tasks');
|
|
223
|
+
expect(result.result).toHaveProperty('content');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should return getting_started help', async () => {
|
|
227
|
+
const supabase = createMockSupabase();
|
|
228
|
+
const ctx = createMockContext(supabase);
|
|
229
|
+
|
|
230
|
+
const result = await getHelp({ topic: 'getting_started' }, ctx);
|
|
231
|
+
|
|
232
|
+
expect(result.result).toHaveProperty('topic', 'getting_started');
|
|
233
|
+
expect(result.result).toHaveProperty('content');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should return error for unknown topic', async () => {
|
|
237
|
+
const supabase = createMockSupabase();
|
|
238
|
+
const ctx = createMockContext(supabase);
|
|
239
|
+
|
|
240
|
+
const result = await getHelp({ topic: 'unknown_topic' }, ctx);
|
|
241
|
+
|
|
242
|
+
expect(result.result).toMatchObject({
|
|
243
|
+
error: 'Unknown topic: unknown_topic',
|
|
244
|
+
});
|
|
245
|
+
expect(result.result).toHaveProperty('available');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should list available topics for unknown topic', async () => {
|
|
249
|
+
const supabase = createMockSupabase();
|
|
250
|
+
const ctx = createMockContext(supabase);
|
|
251
|
+
|
|
252
|
+
const result = await getHelp({ topic: 'nonexistent' }, ctx);
|
|
253
|
+
|
|
254
|
+
const available = (result.result as { available: string[] }).available;
|
|
255
|
+
expect(Array.isArray(available)).toBe(true);
|
|
256
|
+
expect(available.length).toBeGreaterThan(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// getTokenUsage Tests
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
describe('getTokenUsage', () => {
|
|
265
|
+
beforeEach(() => {
|
|
266
|
+
vi.clearAllMocks();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should return token usage stats', async () => {
|
|
270
|
+
const supabase = createMockSupabase();
|
|
271
|
+
const ctx = createMockContext(supabase);
|
|
272
|
+
|
|
273
|
+
const result = await getTokenUsage({}, ctx);
|
|
274
|
+
|
|
275
|
+
expect(result.result).toHaveProperty('session');
|
|
276
|
+
expect(result.result).toHaveProperty('top_tools');
|
|
277
|
+
expect(result.result).toHaveProperty('note');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should return correct session stats', async () => {
|
|
281
|
+
const supabase = createMockSupabase();
|
|
282
|
+
const ctx = createMockContext(supabase, {
|
|
283
|
+
tokenUsage: {
|
|
284
|
+
callCount: 10,
|
|
285
|
+
totalTokens: 5000,
|
|
286
|
+
byTool: {},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const result = await getTokenUsage({}, ctx);
|
|
291
|
+
const session = (result.result as { session: { calls: number; tokens: number; avg_per_call: number } }).session;
|
|
292
|
+
|
|
293
|
+
expect(session.calls).toBe(10);
|
|
294
|
+
expect(session.tokens).toBe(5000);
|
|
295
|
+
expect(session.avg_per_call).toBe(500);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should return top tools sorted by tokens', async () => {
|
|
299
|
+
const supabase = createMockSupabase();
|
|
300
|
+
const ctx = createMockContext(supabase, {
|
|
301
|
+
tokenUsage: {
|
|
302
|
+
callCount: 10,
|
|
303
|
+
totalTokens: 5000,
|
|
304
|
+
byTool: {
|
|
305
|
+
tool_a: { calls: 2, tokens: 1000 },
|
|
306
|
+
tool_b: { calls: 5, tokens: 3000 },
|
|
307
|
+
tool_c: { calls: 3, tokens: 1000 },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const result = await getTokenUsage({}, ctx);
|
|
313
|
+
const topTools = (result.result as { top_tools: Array<{ tool: string; tokens: number }> }).top_tools;
|
|
314
|
+
|
|
315
|
+
// Should be sorted by tokens descending
|
|
316
|
+
expect(topTools[0].tool).toBe('tool_b');
|
|
317
|
+
expect(topTools[0].tokens).toBe(3000);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should handle zero calls gracefully', async () => {
|
|
321
|
+
const supabase = createMockSupabase();
|
|
322
|
+
const ctx = createMockContext(supabase, {
|
|
323
|
+
tokenUsage: {
|
|
324
|
+
callCount: 0,
|
|
325
|
+
totalTokens: 0,
|
|
326
|
+
byTool: {},
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = await getTokenUsage({}, ctx);
|
|
331
|
+
const session = (result.result as { session: { calls: number; tokens: number; avg_per_call: number } }).session;
|
|
332
|
+
|
|
333
|
+
expect(session.calls).toBe(0);
|
|
334
|
+
expect(session.tokens).toBe(0);
|
|
335
|
+
expect(session.avg_per_call).toBe(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should limit top_tools to 5', async () => {
|
|
339
|
+
const supabase = createMockSupabase();
|
|
340
|
+
const ctx = createMockContext(supabase, {
|
|
341
|
+
tokenUsage: {
|
|
342
|
+
callCount: 20,
|
|
343
|
+
totalTokens: 10000,
|
|
344
|
+
byTool: {
|
|
345
|
+
tool_1: { calls: 1, tokens: 100 },
|
|
346
|
+
tool_2: { calls: 2, tokens: 200 },
|
|
347
|
+
tool_3: { calls: 3, tokens: 300 },
|
|
348
|
+
tool_4: { calls: 4, tokens: 400 },
|
|
349
|
+
tool_5: { calls: 5, tokens: 500 },
|
|
350
|
+
tool_6: { calls: 6, tokens: 600 },
|
|
351
|
+
tool_7: { calls: 7, tokens: 700 },
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = await getTokenUsage({}, ctx);
|
|
357
|
+
const topTools = (result.result as { top_tools: Array<{ tool: string }> }).top_tools;
|
|
358
|
+
|
|
359
|
+
expect(topTools.length).toBe(5);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// endWorkSession Tests
|
|
365
|
+
// ============================================================================
|
|
366
|
+
|
|
367
|
+
describe('endWorkSession', () => {
|
|
368
|
+
beforeEach(() => {
|
|
369
|
+
vi.clearAllMocks();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should handle no active session gracefully', async () => {
|
|
373
|
+
const supabase = createMockSupabase();
|
|
374
|
+
const ctx = createMockContext(supabase, { sessionId: null });
|
|
375
|
+
|
|
376
|
+
const result = await endWorkSession({}, ctx);
|
|
377
|
+
|
|
378
|
+
expect(result.result).toMatchObject({
|
|
379
|
+
success: true,
|
|
380
|
+
message: 'No active session to end',
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should use provided session_id over current session', async () => {
|
|
385
|
+
const supabase = createMockSupabase({
|
|
386
|
+
selectResult: {
|
|
387
|
+
data: {
|
|
388
|
+
project_id: 'project-123',
|
|
389
|
+
agent_name: 'Wave',
|
|
390
|
+
started_at: new Date().toISOString()
|
|
391
|
+
},
|
|
392
|
+
error: null
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
const ctx = createMockContext(supabase);
|
|
396
|
+
|
|
397
|
+
const result = await endWorkSession({ session_id: 'other-session-456' }, ctx);
|
|
398
|
+
|
|
399
|
+
expect(result.result).toHaveProperty('ended_session_id', 'other-session-456');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should call updateSession to clear current session', async () => {
|
|
403
|
+
const supabase = createMockSupabase({
|
|
404
|
+
selectResult: {
|
|
405
|
+
data: {
|
|
406
|
+
project_id: 'project-123',
|
|
407
|
+
agent_name: 'Wave',
|
|
408
|
+
started_at: new Date().toISOString()
|
|
409
|
+
},
|
|
410
|
+
error: null
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
const ctx = createMockContext(supabase);
|
|
414
|
+
|
|
415
|
+
await endWorkSession({}, ctx);
|
|
416
|
+
|
|
417
|
+
expect(ctx.updateSession).toHaveBeenCalledWith({ currentSessionId: null });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should return session summary', async () => {
|
|
421
|
+
const supabase = createMockSupabase({
|
|
422
|
+
selectResult: {
|
|
423
|
+
data: {
|
|
424
|
+
project_id: 'project-123',
|
|
425
|
+
agent_name: 'Wave',
|
|
426
|
+
started_at: new Date().toISOString()
|
|
427
|
+
},
|
|
428
|
+
error: null
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
const ctx = createMockContext(supabase);
|
|
432
|
+
|
|
433
|
+
const result = await endWorkSession({}, ctx);
|
|
434
|
+
|
|
435
|
+
expect(result.result).toHaveProperty('session_summary');
|
|
436
|
+
const summary = (result.result as { session_summary: { agent_name: string; token_usage: unknown } }).session_summary;
|
|
437
|
+
expect(summary.agent_name).toBe('Wave');
|
|
438
|
+
expect(summary).toHaveProperty('token_usage');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should not call updateSession when ending a different session', async () => {
|
|
442
|
+
const supabase = createMockSupabase({
|
|
443
|
+
selectResult: {
|
|
444
|
+
data: {
|
|
445
|
+
project_id: 'project-123',
|
|
446
|
+
agent_name: 'Wave',
|
|
447
|
+
started_at: new Date().toISOString()
|
|
448
|
+
},
|
|
449
|
+
error: null
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
const ctx = createMockContext(supabase, { sessionId: 'session-123' });
|
|
453
|
+
|
|
454
|
+
await endWorkSession({ session_id: 'different-session' }, ctx);
|
|
455
|
+
|
|
456
|
+
// Should NOT clear the current session since we're ending a different one
|
|
457
|
+
expect(ctx.updateSession).not.toHaveBeenCalledWith({ currentSessionId: null });
|
|
458
|
+
});
|
|
459
|
+
});
|