@vibescope/mcp-server 0.2.3 → 0.2.5
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/CHANGELOG.md +84 -0
- package/README.md +194 -138
- package/dist/api-client.d.ts +276 -8
- package/dist/api-client.js +123 -8
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +28 -10
- package/dist/handlers/blockers.d.ts +11 -0
- package/dist/handlers/blockers.js +37 -2
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +30 -1
- package/dist/handlers/connectors.js +2 -2
- package/dist/handlers/decisions.d.ts +11 -0
- package/dist/handlers/decisions.js +37 -2
- package/dist/handlers/deployment.d.ts +6 -0
- package/dist/handlers/deployment.js +33 -5
- package/dist/handlers/discovery.js +27 -11
- package/dist/handlers/fallback.js +12 -6
- package/dist/handlers/file-checkouts.d.ts +1 -0
- package/dist/handlers/file-checkouts.js +17 -2
- package/dist/handlers/findings.d.ts +5 -0
- package/dist/handlers/findings.js +19 -2
- package/dist/handlers/git-issues.js +4 -2
- package/dist/handlers/ideas.d.ts +5 -0
- package/dist/handlers/ideas.js +19 -2
- package/dist/handlers/progress.js +2 -2
- package/dist/handlers/project.d.ts +1 -0
- package/dist/handlers/project.js +35 -2
- package/dist/handlers/requests.js +6 -3
- package/dist/handlers/roles.js +13 -2
- package/dist/handlers/session.d.ts +12 -0
- package/dist/handlers/session.js +288 -25
- package/dist/handlers/sprints.d.ts +2 -0
- package/dist/handlers/sprints.js +30 -1
- package/dist/handlers/tasks.d.ts +25 -2
- package/dist/handlers/tasks.js +228 -35
- package/dist/handlers/tool-docs.js +834 -767
- package/dist/index.js +73 -73
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +218 -0
- package/dist/setup.d.ts +22 -0
- package/dist/setup.js +313 -0
- package/dist/templates/agent-guidelines.d.ts +18 -0
- package/dist/templates/agent-guidelines.js +207 -0
- package/dist/tools.js +527 -174
- package/dist/utils.d.ts +5 -2
- package/dist/utils.js +101 -62
- package/docs/TOOLS.md +2053 -2053
- package/package.json +51 -46
- package/scripts/generate-docs.ts +212 -212
- package/scripts/version-bump.ts +203 -0
- package/src/api-client.test.ts +723 -723
- package/src/api-client.ts +2499 -2140
- package/src/cli.ts +27 -10
- package/src/handlers/__test-setup__.ts +236 -231
- package/src/handlers/__test-utils__.ts +87 -87
- package/src/handlers/blockers.test.ts +468 -392
- package/src/handlers/blockers.ts +163 -109
- package/src/handlers/bodies-of-work.test.ts +704 -704
- package/src/handlers/bodies-of-work.ts +526 -468
- package/src/handlers/connectors.test.ts +834 -834
- package/src/handlers/connectors.ts +229 -229
- package/src/handlers/cost.test.ts +462 -462
- package/src/handlers/cost.ts +285 -285
- package/src/handlers/decisions.test.ts +382 -313
- package/src/handlers/decisions.ts +153 -99
- package/src/handlers/deployment.test.ts +551 -470
- package/src/handlers/deployment.ts +541 -508
- package/src/handlers/discovery.test.ts +206 -206
- package/src/handlers/discovery.ts +390 -374
- package/src/handlers/fallback.test.ts +537 -536
- package/src/handlers/fallback.ts +194 -188
- package/src/handlers/file-checkouts.test.ts +750 -670
- package/src/handlers/file-checkouts.ts +185 -165
- package/src/handlers/findings.test.ts +633 -633
- package/src/handlers/findings.ts +239 -203
- package/src/handlers/git-issues.test.ts +631 -631
- package/src/handlers/git-issues.ts +136 -134
- package/src/handlers/ideas.test.ts +644 -644
- package/src/handlers/ideas.ts +207 -175
- package/src/handlers/index.ts +84 -84
- package/src/handlers/milestones.test.ts +475 -475
- package/src/handlers/milestones.ts +180 -180
- package/src/handlers/organizations.test.ts +826 -826
- package/src/handlers/organizations.ts +315 -315
- package/src/handlers/progress.test.ts +269 -269
- package/src/handlers/progress.ts +77 -77
- package/src/handlers/project.test.ts +546 -546
- package/src/handlers/project.ts +239 -194
- package/src/handlers/requests.test.ts +303 -272
- package/src/handlers/requests.ts +99 -96
- package/src/handlers/roles.test.ts +303 -303
- package/src/handlers/roles.ts +226 -208
- package/src/handlers/session.test.ts +875 -576
- package/src/handlers/session.ts +738 -425
- package/src/handlers/sprints.test.ts +732 -732
- package/src/handlers/sprints.ts +537 -477
- package/src/handlers/tasks.test.ts +907 -980
- package/src/handlers/tasks.ts +945 -716
- package/src/handlers/tool-categories.test.ts +66 -66
- package/src/handlers/tool-docs.ts +1096 -1024
- package/src/handlers/types.test.ts +259 -0
- package/src/handlers/types.ts +175 -175
- package/src/handlers/validation.test.ts +582 -582
- package/src/handlers/validation.ts +97 -97
- package/src/index.ts +792 -792
- package/src/setup.test.ts +231 -0
- package/src/setup.ts +370 -0
- package/src/templates/agent-guidelines.ts +210 -0
- package/src/token-tracking.test.ts +453 -453
- package/src/token-tracking.ts +164 -164
- package/src/tools.ts +3562 -3208
- package/src/utils.test.ts +683 -681
- package/src/utils.ts +436 -392
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +249 -249
- package/tsconfig.json +16 -16
- package/vitest.config.ts +14 -14
|
@@ -1,453 +1,453 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
estimateTokens,
|
|
4
|
-
createTokenUsage,
|
|
5
|
-
trackTokenUsage,
|
|
6
|
-
setCurrentModel,
|
|
7
|
-
resetTokenUsage,
|
|
8
|
-
getTokenUsageSummary,
|
|
9
|
-
type TokenUsage,
|
|
10
|
-
} from './token-tracking.js';
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// estimateTokens Tests
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
describe('estimateTokens', () => {
|
|
17
|
-
it('should return 1 for empty object', () => {
|
|
18
|
-
// "{}" is 2 chars, ceil(2/4) = 1
|
|
19
|
-
expect(estimateTokens({})).toBe(1);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should return 1 for empty array', () => {
|
|
23
|
-
// "[]" is 2 chars, ceil(2/4) = 1
|
|
24
|
-
expect(estimateTokens([])).toBe(1);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should return 1 for empty string', () => {
|
|
28
|
-
// '""' is 2 chars, ceil(2/4) = 1
|
|
29
|
-
expect(estimateTokens('')).toBe(1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should return 1 for null', () => {
|
|
33
|
-
// "null" is 4 chars, ceil(4/4) = 1
|
|
34
|
-
expect(estimateTokens(null)).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should return 1 for boolean', () => {
|
|
38
|
-
// "true" is 4 chars, ceil(4/4) = 1
|
|
39
|
-
expect(estimateTokens(true)).toBe(1);
|
|
40
|
-
// "false" is 5 chars, ceil(5/4) = 2
|
|
41
|
-
expect(estimateTokens(false)).toBe(2);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should estimate tokens for simple object', () => {
|
|
45
|
-
const obj = { name: 'test' };
|
|
46
|
-
// {"name":"test"} is 15 chars, ceil(15/4) = 4
|
|
47
|
-
expect(estimateTokens(obj)).toBe(4);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should estimate tokens for array of strings', () => {
|
|
51
|
-
const arr = ['one', 'two', 'three'];
|
|
52
|
-
// ["one","two","three"] is 21 chars, ceil(21/4) = 6
|
|
53
|
-
expect(estimateTokens(arr)).toBe(6);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should estimate tokens for nested object', () => {
|
|
57
|
-
const obj = {
|
|
58
|
-
user: {
|
|
59
|
-
name: 'John',
|
|
60
|
-
age: 30,
|
|
61
|
-
},
|
|
62
|
-
active: true,
|
|
63
|
-
};
|
|
64
|
-
// Complex object - just verify it returns a reasonable positive number
|
|
65
|
-
const tokens = estimateTokens(obj);
|
|
66
|
-
expect(tokens).toBeGreaterThan(0);
|
|
67
|
-
expect(tokens).toBeLessThan(100); // Sanity check
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should estimate tokens for large object', () => {
|
|
71
|
-
const obj = {
|
|
72
|
-
tasks: Array(100)
|
|
73
|
-
.fill(null)
|
|
74
|
-
.map((_, i) => ({
|
|
75
|
-
id: `task-${i}`,
|
|
76
|
-
title: `Task number ${i}`,
|
|
77
|
-
status: 'pending',
|
|
78
|
-
})),
|
|
79
|
-
};
|
|
80
|
-
const tokens = estimateTokens(obj);
|
|
81
|
-
// Should be a large number for 100 tasks
|
|
82
|
-
expect(tokens).toBeGreaterThan(500);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should handle numbers', () => {
|
|
86
|
-
// "12345" is 5 chars, ceil(5/4) = 2
|
|
87
|
-
expect(estimateTokens(12345)).toBe(2);
|
|
88
|
-
// "3.14159" is 7 chars, ceil(7/4) = 2
|
|
89
|
-
expect(estimateTokens(3.14159)).toBe(2);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should handle undefined by treating as null', () => {
|
|
93
|
-
// JSON.stringify(undefined) returns undefined, not a string
|
|
94
|
-
// Our function handles this gracefully
|
|
95
|
-
const tokens = estimateTokens(undefined);
|
|
96
|
-
expect(tokens).toBeGreaterThanOrEqual(1);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should handle circular reference gracefully', () => {
|
|
100
|
-
const obj: Record<string, unknown> = { name: 'test' };
|
|
101
|
-
obj.self = obj; // Create circular reference
|
|
102
|
-
|
|
103
|
-
// Should not throw, should return minimal estimate
|
|
104
|
-
const tokens = estimateTokens(obj);
|
|
105
|
-
expect(tokens).toBe(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should handle objects with toJSON method', () => {
|
|
109
|
-
const obj = {
|
|
110
|
-
data: 'test',
|
|
111
|
-
toJSON() {
|
|
112
|
-
return { serialized: true };
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
// toJSON returns {"serialized":true} which is 18 chars, ceil(18/4) = 5
|
|
116
|
-
expect(estimateTokens(obj)).toBe(5);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ============================================================================
|
|
121
|
-
// createTokenUsage Tests
|
|
122
|
-
// ============================================================================
|
|
123
|
-
|
|
124
|
-
describe('createTokenUsage', () => {
|
|
125
|
-
it('should create fresh token usage object', () => {
|
|
126
|
-
const usage = createTokenUsage();
|
|
127
|
-
|
|
128
|
-
expect(usage.callCount).toBe(0);
|
|
129
|
-
expect(usage.totalTokens).toBe(0);
|
|
130
|
-
expect(usage.byTool).toEqual({});
|
|
131
|
-
expect(usage.byModel).toEqual({});
|
|
132
|
-
expect(usage.currentModel).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should create independent instances', () => {
|
|
136
|
-
const usage1 = createTokenUsage();
|
|
137
|
-
const usage2 = createTokenUsage();
|
|
138
|
-
|
|
139
|
-
usage1.callCount = 5;
|
|
140
|
-
usage1.byTool['test'] = { calls: 1, tokens: 10 };
|
|
141
|
-
|
|
142
|
-
expect(usage2.callCount).toBe(0);
|
|
143
|
-
expect(usage2.byTool).toEqual({});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// ============================================================================
|
|
148
|
-
// trackTokenUsage Tests
|
|
149
|
-
// ============================================================================
|
|
150
|
-
|
|
151
|
-
describe('trackTokenUsage', () => {
|
|
152
|
-
let usage: TokenUsage;
|
|
153
|
-
|
|
154
|
-
beforeEach(() => {
|
|
155
|
-
usage = createTokenUsage();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should increment call count', () => {
|
|
159
|
-
trackTokenUsage(usage, 'test_tool', {}, {});
|
|
160
|
-
expect(usage.callCount).toBe(1);
|
|
161
|
-
|
|
162
|
-
trackTokenUsage(usage, 'test_tool', {}, {});
|
|
163
|
-
expect(usage.callCount).toBe(2);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should accumulate total tokens', () => {
|
|
167
|
-
trackTokenUsage(usage, 'tool1', { key: 'value' }, { result: 'ok' });
|
|
168
|
-
const firstTotal = usage.totalTokens;
|
|
169
|
-
expect(firstTotal).toBeGreaterThan(0);
|
|
170
|
-
|
|
171
|
-
trackTokenUsage(usage, 'tool2', { key: 'value' }, { result: 'ok' });
|
|
172
|
-
expect(usage.totalTokens).toBeGreaterThan(firstTotal);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should track by tool name', () => {
|
|
176
|
-
trackTokenUsage(usage, 'add_task', { title: 'Test' }, { success: true });
|
|
177
|
-
trackTokenUsage(usage, 'add_task', { title: 'Test 2' }, { success: true });
|
|
178
|
-
trackTokenUsage(usage, 'complete_task', { id: '123' }, { success: true });
|
|
179
|
-
|
|
180
|
-
expect(usage.byTool['add_task'].calls).toBe(2);
|
|
181
|
-
expect(usage.byTool['complete_task'].calls).toBe(1);
|
|
182
|
-
expect(usage.byTool['add_task'].tokens).toBeGreaterThan(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should track by model when set', () => {
|
|
186
|
-
setCurrentModel(usage, 'opus');
|
|
187
|
-
trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
|
|
188
|
-
|
|
189
|
-
expect(usage.byModel['opus']).toBeDefined();
|
|
190
|
-
expect(usage.byModel['opus'].input).toBeGreaterThan(0);
|
|
191
|
-
expect(usage.byModel['opus'].output).toBeGreaterThan(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should not track by model when not set', () => {
|
|
195
|
-
trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
|
|
196
|
-
|
|
197
|
-
expect(Object.keys(usage.byModel)).toHaveLength(0);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should track multiple models separately', () => {
|
|
201
|
-
setCurrentModel(usage, 'opus');
|
|
202
|
-
trackTokenUsage(usage, 'tool1', { x: 1 }, { y: 2 });
|
|
203
|
-
|
|
204
|
-
setCurrentModel(usage, 'sonnet');
|
|
205
|
-
trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
|
|
206
|
-
trackTokenUsage(usage, 'tool3', { x: 1 }, { y: 2 });
|
|
207
|
-
|
|
208
|
-
expect(Object.keys(usage.byModel)).toHaveLength(2);
|
|
209
|
-
expect(usage.byModel['opus']).toBeDefined();
|
|
210
|
-
expect(usage.byModel['sonnet']).toBeDefined();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('should handle empty args and response', () => {
|
|
214
|
-
trackTokenUsage(usage, 'empty_tool', {}, {});
|
|
215
|
-
|
|
216
|
-
expect(usage.callCount).toBe(1);
|
|
217
|
-
expect(usage.totalTokens).toBeGreaterThanOrEqual(2); // At least 2 for {} + {}
|
|
218
|
-
expect(usage.byTool['empty_tool'].calls).toBe(1);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should handle large args and response', () => {
|
|
222
|
-
const largeArgs = { items: Array(1000).fill('item') };
|
|
223
|
-
const largeResponse = { results: Array(1000).fill({ ok: true }) };
|
|
224
|
-
|
|
225
|
-
trackTokenUsage(usage, 'large_tool', largeArgs, largeResponse);
|
|
226
|
-
|
|
227
|
-
expect(usage.totalTokens).toBeGreaterThan(1000);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ============================================================================
|
|
232
|
-
// setCurrentModel Tests
|
|
233
|
-
// ============================================================================
|
|
234
|
-
|
|
235
|
-
describe('setCurrentModel', () => {
|
|
236
|
-
it('should set the current model', () => {
|
|
237
|
-
const usage = createTokenUsage();
|
|
238
|
-
|
|
239
|
-
setCurrentModel(usage, 'opus');
|
|
240
|
-
expect(usage.currentModel).toBe('opus');
|
|
241
|
-
|
|
242
|
-
setCurrentModel(usage, 'sonnet');
|
|
243
|
-
expect(usage.currentModel).toBe('sonnet');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should allow clearing the model with null', () => {
|
|
247
|
-
const usage = createTokenUsage();
|
|
248
|
-
|
|
249
|
-
setCurrentModel(usage, 'opus');
|
|
250
|
-
expect(usage.currentModel).toBe('opus');
|
|
251
|
-
|
|
252
|
-
setCurrentModel(usage, null);
|
|
253
|
-
expect(usage.currentModel).toBeNull();
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ============================================================================
|
|
258
|
-
// resetTokenUsage Tests
|
|
259
|
-
// ============================================================================
|
|
260
|
-
|
|
261
|
-
describe('resetTokenUsage', () => {
|
|
262
|
-
it('should reset all tracking data', () => {
|
|
263
|
-
const usage = createTokenUsage();
|
|
264
|
-
|
|
265
|
-
// Add some data
|
|
266
|
-
setCurrentModel(usage, 'opus');
|
|
267
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
268
|
-
trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
|
|
269
|
-
|
|
270
|
-
expect(usage.callCount).toBe(2);
|
|
271
|
-
expect(usage.totalTokens).toBeGreaterThan(0);
|
|
272
|
-
expect(Object.keys(usage.byTool)).toHaveLength(2);
|
|
273
|
-
|
|
274
|
-
// Reset
|
|
275
|
-
resetTokenUsage(usage);
|
|
276
|
-
|
|
277
|
-
expect(usage.callCount).toBe(0);
|
|
278
|
-
expect(usage.totalTokens).toBe(0);
|
|
279
|
-
expect(usage.byTool).toEqual({});
|
|
280
|
-
expect(usage.byModel).toEqual({});
|
|
281
|
-
expect(usage.currentModel).toBeNull();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should allow tracking after reset', () => {
|
|
285
|
-
const usage = createTokenUsage();
|
|
286
|
-
|
|
287
|
-
trackTokenUsage(usage, 'tool1', {}, {});
|
|
288
|
-
resetTokenUsage(usage);
|
|
289
|
-
|
|
290
|
-
trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
|
|
291
|
-
|
|
292
|
-
expect(usage.callCount).toBe(1);
|
|
293
|
-
expect(usage.byTool['tool1']).toBeUndefined();
|
|
294
|
-
expect(usage.byTool['tool2']).toBeDefined();
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// ============================================================================
|
|
299
|
-
// getTokenUsageSummary Tests
|
|
300
|
-
// ============================================================================
|
|
301
|
-
|
|
302
|
-
describe('getTokenUsageSummary', () => {
|
|
303
|
-
it('should return empty summary for new usage', () => {
|
|
304
|
-
const usage = createTokenUsage();
|
|
305
|
-
const summary = getTokenUsageSummary(usage);
|
|
306
|
-
|
|
307
|
-
expect(summary.total_calls).toBe(0);
|
|
308
|
-
expect(summary.total_tokens).toBe(0);
|
|
309
|
-
expect(summary.average_tokens_per_call).toBe(0);
|
|
310
|
-
expect(summary.by_tool).toEqual({});
|
|
311
|
-
expect(summary.by_model).toEqual({});
|
|
312
|
-
expect(summary.current_model).toBeNull();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('should calculate average tokens per call', () => {
|
|
316
|
-
const usage = createTokenUsage();
|
|
317
|
-
|
|
318
|
-
// Track 3 calls with roughly similar sizes
|
|
319
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
320
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
321
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
322
|
-
|
|
323
|
-
const summary = getTokenUsageSummary(usage);
|
|
324
|
-
|
|
325
|
-
expect(summary.total_calls).toBe(3);
|
|
326
|
-
expect(summary.average_tokens_per_call).toBeGreaterThan(0);
|
|
327
|
-
expect(summary.average_tokens_per_call).toBe(
|
|
328
|
-
Math.round(summary.total_tokens / summary.total_calls)
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('should include per-tool averages', () => {
|
|
333
|
-
const usage = createTokenUsage();
|
|
334
|
-
|
|
335
|
-
trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
|
|
336
|
-
trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
|
|
337
|
-
|
|
338
|
-
const summary = getTokenUsageSummary(usage);
|
|
339
|
-
|
|
340
|
-
expect(summary.by_tool['small_tool'].calls).toBe(2);
|
|
341
|
-
expect(summary.by_tool['small_tool'].avg).toBeGreaterThan(0);
|
|
342
|
-
expect(summary.by_tool['small_tool'].avg).toBe(
|
|
343
|
-
Math.round(summary.by_tool['small_tool'].tokens / summary.by_tool['small_tool'].calls)
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('should include model breakdown', () => {
|
|
348
|
-
const usage = createTokenUsage();
|
|
349
|
-
|
|
350
|
-
setCurrentModel(usage, 'opus');
|
|
351
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
352
|
-
|
|
353
|
-
setCurrentModel(usage, 'haiku');
|
|
354
|
-
trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
|
|
355
|
-
|
|
356
|
-
const summary = getTokenUsageSummary(usage);
|
|
357
|
-
|
|
358
|
-
expect(summary.by_model['opus']).toBeDefined();
|
|
359
|
-
expect(summary.by_model['haiku']).toBeDefined();
|
|
360
|
-
expect(summary.current_model).toBe('haiku');
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('should return a copy of byModel to prevent mutation', () => {
|
|
364
|
-
const usage = createTokenUsage();
|
|
365
|
-
|
|
366
|
-
setCurrentModel(usage, 'opus');
|
|
367
|
-
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
368
|
-
|
|
369
|
-
const summary = getTokenUsageSummary(usage);
|
|
370
|
-
|
|
371
|
-
// Modify the summary
|
|
372
|
-
summary.by_model['opus'].input = 999;
|
|
373
|
-
|
|
374
|
-
// Original should be unchanged
|
|
375
|
-
expect(usage.byModel['opus'].input).not.toBe(999);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// ============================================================================
|
|
380
|
-
// Integration Tests
|
|
381
|
-
// ============================================================================
|
|
382
|
-
|
|
383
|
-
describe('Token Tracking Integration', () => {
|
|
384
|
-
it('should track a realistic session workflow', () => {
|
|
385
|
-
const usage = createTokenUsage();
|
|
386
|
-
|
|
387
|
-
// Agent starts with opus model
|
|
388
|
-
setCurrentModel(usage, 'opus');
|
|
389
|
-
|
|
390
|
-
// Start session
|
|
391
|
-
trackTokenUsage(
|
|
392
|
-
usage,
|
|
393
|
-
'start_work_session',
|
|
394
|
-
{ git_url: 'https://github.com/org/repo', model: 'opus' },
|
|
395
|
-
{
|
|
396
|
-
session_id: '123',
|
|
397
|
-
persona: 'Atlas',
|
|
398
|
-
next_task: { id: 'task-1', title: 'Fix bug' },
|
|
399
|
-
}
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
// Update task
|
|
403
|
-
trackTokenUsage(
|
|
404
|
-
usage,
|
|
405
|
-
'update_task',
|
|
406
|
-
{ task_id: 'task-1', status: 'in_progress', progress_percentage: 25 },
|
|
407
|
-
{ success: true }
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// Complete task
|
|
411
|
-
trackTokenUsage(
|
|
412
|
-
usage,
|
|
413
|
-
'complete_task',
|
|
414
|
-
{ task_id: 'task-1', summary: 'Fixed the bug' },
|
|
415
|
-
{ success: true, next_task: null }
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
const summary = getTokenUsageSummary(usage);
|
|
419
|
-
|
|
420
|
-
expect(summary.total_calls).toBe(3);
|
|
421
|
-
expect(summary.by_tool['start_work_session'].calls).toBe(1);
|
|
422
|
-
expect(summary.by_tool['update_task'].calls).toBe(1);
|
|
423
|
-
expect(summary.by_tool['complete_task'].calls).toBe(1);
|
|
424
|
-
expect(summary.by_model['opus']).toBeDefined();
|
|
425
|
-
expect(summary.by_model['opus'].input).toBeGreaterThan(0);
|
|
426
|
-
expect(summary.by_model['opus'].output).toBeGreaterThan(0);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should handle session reset', () => {
|
|
430
|
-
const usage = createTokenUsage();
|
|
431
|
-
|
|
432
|
-
// First session
|
|
433
|
-
setCurrentModel(usage, 'sonnet');
|
|
434
|
-
trackTokenUsage(usage, 'tool1', {}, {});
|
|
435
|
-
trackTokenUsage(usage, 'tool2', {}, {});
|
|
436
|
-
|
|
437
|
-
const firstSummary = getTokenUsageSummary(usage);
|
|
438
|
-
expect(firstSummary.total_calls).toBe(2);
|
|
439
|
-
|
|
440
|
-
// Reset for new session
|
|
441
|
-
resetTokenUsage(usage);
|
|
442
|
-
|
|
443
|
-
// Second session
|
|
444
|
-
setCurrentModel(usage, 'haiku');
|
|
445
|
-
trackTokenUsage(usage, 'tool3', {}, {});
|
|
446
|
-
|
|
447
|
-
const secondSummary = getTokenUsageSummary(usage);
|
|
448
|
-
expect(secondSummary.total_calls).toBe(1);
|
|
449
|
-
expect(secondSummary.by_tool['tool1']).toBeUndefined();
|
|
450
|
-
expect(secondSummary.by_tool['tool3']).toBeDefined();
|
|
451
|
-
expect(secondSummary.current_model).toBe('haiku');
|
|
452
|
-
});
|
|
453
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
estimateTokens,
|
|
4
|
+
createTokenUsage,
|
|
5
|
+
trackTokenUsage,
|
|
6
|
+
setCurrentModel,
|
|
7
|
+
resetTokenUsage,
|
|
8
|
+
getTokenUsageSummary,
|
|
9
|
+
type TokenUsage,
|
|
10
|
+
} from './token-tracking.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// estimateTokens Tests
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
describe('estimateTokens', () => {
|
|
17
|
+
it('should return 1 for empty object', () => {
|
|
18
|
+
// "{}" is 2 chars, ceil(2/4) = 1
|
|
19
|
+
expect(estimateTokens({})).toBe(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return 1 for empty array', () => {
|
|
23
|
+
// "[]" is 2 chars, ceil(2/4) = 1
|
|
24
|
+
expect(estimateTokens([])).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return 1 for empty string', () => {
|
|
28
|
+
// '""' is 2 chars, ceil(2/4) = 1
|
|
29
|
+
expect(estimateTokens('')).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return 1 for null', () => {
|
|
33
|
+
// "null" is 4 chars, ceil(4/4) = 1
|
|
34
|
+
expect(estimateTokens(null)).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return 1 for boolean', () => {
|
|
38
|
+
// "true" is 4 chars, ceil(4/4) = 1
|
|
39
|
+
expect(estimateTokens(true)).toBe(1);
|
|
40
|
+
// "false" is 5 chars, ceil(5/4) = 2
|
|
41
|
+
expect(estimateTokens(false)).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should estimate tokens for simple object', () => {
|
|
45
|
+
const obj = { name: 'test' };
|
|
46
|
+
// {"name":"test"} is 15 chars, ceil(15/4) = 4
|
|
47
|
+
expect(estimateTokens(obj)).toBe(4);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should estimate tokens for array of strings', () => {
|
|
51
|
+
const arr = ['one', 'two', 'three'];
|
|
52
|
+
// ["one","two","three"] is 21 chars, ceil(21/4) = 6
|
|
53
|
+
expect(estimateTokens(arr)).toBe(6);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should estimate tokens for nested object', () => {
|
|
57
|
+
const obj = {
|
|
58
|
+
user: {
|
|
59
|
+
name: 'John',
|
|
60
|
+
age: 30,
|
|
61
|
+
},
|
|
62
|
+
active: true,
|
|
63
|
+
};
|
|
64
|
+
// Complex object - just verify it returns a reasonable positive number
|
|
65
|
+
const tokens = estimateTokens(obj);
|
|
66
|
+
expect(tokens).toBeGreaterThan(0);
|
|
67
|
+
expect(tokens).toBeLessThan(100); // Sanity check
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should estimate tokens for large object', () => {
|
|
71
|
+
const obj = {
|
|
72
|
+
tasks: Array(100)
|
|
73
|
+
.fill(null)
|
|
74
|
+
.map((_, i) => ({
|
|
75
|
+
id: `task-${i}`,
|
|
76
|
+
title: `Task number ${i}`,
|
|
77
|
+
status: 'pending',
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
const tokens = estimateTokens(obj);
|
|
81
|
+
// Should be a large number for 100 tasks
|
|
82
|
+
expect(tokens).toBeGreaterThan(500);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle numbers', () => {
|
|
86
|
+
// "12345" is 5 chars, ceil(5/4) = 2
|
|
87
|
+
expect(estimateTokens(12345)).toBe(2);
|
|
88
|
+
// "3.14159" is 7 chars, ceil(7/4) = 2
|
|
89
|
+
expect(estimateTokens(3.14159)).toBe(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle undefined by treating as null', () => {
|
|
93
|
+
// JSON.stringify(undefined) returns undefined, not a string
|
|
94
|
+
// Our function handles this gracefully
|
|
95
|
+
const tokens = estimateTokens(undefined);
|
|
96
|
+
expect(tokens).toBeGreaterThanOrEqual(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle circular reference gracefully', () => {
|
|
100
|
+
const obj: Record<string, unknown> = { name: 'test' };
|
|
101
|
+
obj.self = obj; // Create circular reference
|
|
102
|
+
|
|
103
|
+
// Should not throw, should return minimal estimate
|
|
104
|
+
const tokens = estimateTokens(obj);
|
|
105
|
+
expect(tokens).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle objects with toJSON method', () => {
|
|
109
|
+
const obj = {
|
|
110
|
+
data: 'test',
|
|
111
|
+
toJSON() {
|
|
112
|
+
return { serialized: true };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
// toJSON returns {"serialized":true} which is 18 chars, ceil(18/4) = 5
|
|
116
|
+
expect(estimateTokens(obj)).toBe(5);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// createTokenUsage Tests
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
describe('createTokenUsage', () => {
|
|
125
|
+
it('should create fresh token usage object', () => {
|
|
126
|
+
const usage = createTokenUsage();
|
|
127
|
+
|
|
128
|
+
expect(usage.callCount).toBe(0);
|
|
129
|
+
expect(usage.totalTokens).toBe(0);
|
|
130
|
+
expect(usage.byTool).toEqual({});
|
|
131
|
+
expect(usage.byModel).toEqual({});
|
|
132
|
+
expect(usage.currentModel).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should create independent instances', () => {
|
|
136
|
+
const usage1 = createTokenUsage();
|
|
137
|
+
const usage2 = createTokenUsage();
|
|
138
|
+
|
|
139
|
+
usage1.callCount = 5;
|
|
140
|
+
usage1.byTool['test'] = { calls: 1, tokens: 10 };
|
|
141
|
+
|
|
142
|
+
expect(usage2.callCount).toBe(0);
|
|
143
|
+
expect(usage2.byTool).toEqual({});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// trackTokenUsage Tests
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
describe('trackTokenUsage', () => {
|
|
152
|
+
let usage: TokenUsage;
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
usage = createTokenUsage();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should increment call count', () => {
|
|
159
|
+
trackTokenUsage(usage, 'test_tool', {}, {});
|
|
160
|
+
expect(usage.callCount).toBe(1);
|
|
161
|
+
|
|
162
|
+
trackTokenUsage(usage, 'test_tool', {}, {});
|
|
163
|
+
expect(usage.callCount).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should accumulate total tokens', () => {
|
|
167
|
+
trackTokenUsage(usage, 'tool1', { key: 'value' }, { result: 'ok' });
|
|
168
|
+
const firstTotal = usage.totalTokens;
|
|
169
|
+
expect(firstTotal).toBeGreaterThan(0);
|
|
170
|
+
|
|
171
|
+
trackTokenUsage(usage, 'tool2', { key: 'value' }, { result: 'ok' });
|
|
172
|
+
expect(usage.totalTokens).toBeGreaterThan(firstTotal);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should track by tool name', () => {
|
|
176
|
+
trackTokenUsage(usage, 'add_task', { title: 'Test' }, { success: true });
|
|
177
|
+
trackTokenUsage(usage, 'add_task', { title: 'Test 2' }, { success: true });
|
|
178
|
+
trackTokenUsage(usage, 'complete_task', { id: '123' }, { success: true });
|
|
179
|
+
|
|
180
|
+
expect(usage.byTool['add_task'].calls).toBe(2);
|
|
181
|
+
expect(usage.byTool['complete_task'].calls).toBe(1);
|
|
182
|
+
expect(usage.byTool['add_task'].tokens).toBeGreaterThan(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should track by model when set', () => {
|
|
186
|
+
setCurrentModel(usage, 'opus');
|
|
187
|
+
trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
|
|
188
|
+
|
|
189
|
+
expect(usage.byModel['opus']).toBeDefined();
|
|
190
|
+
expect(usage.byModel['opus'].input).toBeGreaterThan(0);
|
|
191
|
+
expect(usage.byModel['opus'].output).toBeGreaterThan(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should not track by model when not set', () => {
|
|
195
|
+
trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
|
|
196
|
+
|
|
197
|
+
expect(Object.keys(usage.byModel)).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should track multiple models separately', () => {
|
|
201
|
+
setCurrentModel(usage, 'opus');
|
|
202
|
+
trackTokenUsage(usage, 'tool1', { x: 1 }, { y: 2 });
|
|
203
|
+
|
|
204
|
+
setCurrentModel(usage, 'sonnet');
|
|
205
|
+
trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
|
|
206
|
+
trackTokenUsage(usage, 'tool3', { x: 1 }, { y: 2 });
|
|
207
|
+
|
|
208
|
+
expect(Object.keys(usage.byModel)).toHaveLength(2);
|
|
209
|
+
expect(usage.byModel['opus']).toBeDefined();
|
|
210
|
+
expect(usage.byModel['sonnet']).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle empty args and response', () => {
|
|
214
|
+
trackTokenUsage(usage, 'empty_tool', {}, {});
|
|
215
|
+
|
|
216
|
+
expect(usage.callCount).toBe(1);
|
|
217
|
+
expect(usage.totalTokens).toBeGreaterThanOrEqual(2); // At least 2 for {} + {}
|
|
218
|
+
expect(usage.byTool['empty_tool'].calls).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle large args and response', () => {
|
|
222
|
+
const largeArgs = { items: Array(1000).fill('item') };
|
|
223
|
+
const largeResponse = { results: Array(1000).fill({ ok: true }) };
|
|
224
|
+
|
|
225
|
+
trackTokenUsage(usage, 'large_tool', largeArgs, largeResponse);
|
|
226
|
+
|
|
227
|
+
expect(usage.totalTokens).toBeGreaterThan(1000);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// setCurrentModel Tests
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
describe('setCurrentModel', () => {
|
|
236
|
+
it('should set the current model', () => {
|
|
237
|
+
const usage = createTokenUsage();
|
|
238
|
+
|
|
239
|
+
setCurrentModel(usage, 'opus');
|
|
240
|
+
expect(usage.currentModel).toBe('opus');
|
|
241
|
+
|
|
242
|
+
setCurrentModel(usage, 'sonnet');
|
|
243
|
+
expect(usage.currentModel).toBe('sonnet');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should allow clearing the model with null', () => {
|
|
247
|
+
const usage = createTokenUsage();
|
|
248
|
+
|
|
249
|
+
setCurrentModel(usage, 'opus');
|
|
250
|
+
expect(usage.currentModel).toBe('opus');
|
|
251
|
+
|
|
252
|
+
setCurrentModel(usage, null);
|
|
253
|
+
expect(usage.currentModel).toBeNull();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// resetTokenUsage Tests
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
describe('resetTokenUsage', () => {
|
|
262
|
+
it('should reset all tracking data', () => {
|
|
263
|
+
const usage = createTokenUsage();
|
|
264
|
+
|
|
265
|
+
// Add some data
|
|
266
|
+
setCurrentModel(usage, 'opus');
|
|
267
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
268
|
+
trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
|
|
269
|
+
|
|
270
|
+
expect(usage.callCount).toBe(2);
|
|
271
|
+
expect(usage.totalTokens).toBeGreaterThan(0);
|
|
272
|
+
expect(Object.keys(usage.byTool)).toHaveLength(2);
|
|
273
|
+
|
|
274
|
+
// Reset
|
|
275
|
+
resetTokenUsage(usage);
|
|
276
|
+
|
|
277
|
+
expect(usage.callCount).toBe(0);
|
|
278
|
+
expect(usage.totalTokens).toBe(0);
|
|
279
|
+
expect(usage.byTool).toEqual({});
|
|
280
|
+
expect(usage.byModel).toEqual({});
|
|
281
|
+
expect(usage.currentModel).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should allow tracking after reset', () => {
|
|
285
|
+
const usage = createTokenUsage();
|
|
286
|
+
|
|
287
|
+
trackTokenUsage(usage, 'tool1', {}, {});
|
|
288
|
+
resetTokenUsage(usage);
|
|
289
|
+
|
|
290
|
+
trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
|
|
291
|
+
|
|
292
|
+
expect(usage.callCount).toBe(1);
|
|
293
|
+
expect(usage.byTool['tool1']).toBeUndefined();
|
|
294
|
+
expect(usage.byTool['tool2']).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// getTokenUsageSummary Tests
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
describe('getTokenUsageSummary', () => {
|
|
303
|
+
it('should return empty summary for new usage', () => {
|
|
304
|
+
const usage = createTokenUsage();
|
|
305
|
+
const summary = getTokenUsageSummary(usage);
|
|
306
|
+
|
|
307
|
+
expect(summary.total_calls).toBe(0);
|
|
308
|
+
expect(summary.total_tokens).toBe(0);
|
|
309
|
+
expect(summary.average_tokens_per_call).toBe(0);
|
|
310
|
+
expect(summary.by_tool).toEqual({});
|
|
311
|
+
expect(summary.by_model).toEqual({});
|
|
312
|
+
expect(summary.current_model).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should calculate average tokens per call', () => {
|
|
316
|
+
const usage = createTokenUsage();
|
|
317
|
+
|
|
318
|
+
// Track 3 calls with roughly similar sizes
|
|
319
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
320
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
321
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
322
|
+
|
|
323
|
+
const summary = getTokenUsageSummary(usage);
|
|
324
|
+
|
|
325
|
+
expect(summary.total_calls).toBe(3);
|
|
326
|
+
expect(summary.average_tokens_per_call).toBeGreaterThan(0);
|
|
327
|
+
expect(summary.average_tokens_per_call).toBe(
|
|
328
|
+
Math.round(summary.total_tokens / summary.total_calls)
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should include per-tool averages', () => {
|
|
333
|
+
const usage = createTokenUsage();
|
|
334
|
+
|
|
335
|
+
trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
|
|
336
|
+
trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
|
|
337
|
+
|
|
338
|
+
const summary = getTokenUsageSummary(usage);
|
|
339
|
+
|
|
340
|
+
expect(summary.by_tool['small_tool'].calls).toBe(2);
|
|
341
|
+
expect(summary.by_tool['small_tool'].avg).toBeGreaterThan(0);
|
|
342
|
+
expect(summary.by_tool['small_tool'].avg).toBe(
|
|
343
|
+
Math.round(summary.by_tool['small_tool'].tokens / summary.by_tool['small_tool'].calls)
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should include model breakdown', () => {
|
|
348
|
+
const usage = createTokenUsage();
|
|
349
|
+
|
|
350
|
+
setCurrentModel(usage, 'opus');
|
|
351
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
352
|
+
|
|
353
|
+
setCurrentModel(usage, 'haiku');
|
|
354
|
+
trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
|
|
355
|
+
|
|
356
|
+
const summary = getTokenUsageSummary(usage);
|
|
357
|
+
|
|
358
|
+
expect(summary.by_model['opus']).toBeDefined();
|
|
359
|
+
expect(summary.by_model['haiku']).toBeDefined();
|
|
360
|
+
expect(summary.current_model).toBe('haiku');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return a copy of byModel to prevent mutation', () => {
|
|
364
|
+
const usage = createTokenUsage();
|
|
365
|
+
|
|
366
|
+
setCurrentModel(usage, 'opus');
|
|
367
|
+
trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
|
|
368
|
+
|
|
369
|
+
const summary = getTokenUsageSummary(usage);
|
|
370
|
+
|
|
371
|
+
// Modify the summary
|
|
372
|
+
summary.by_model['opus'].input = 999;
|
|
373
|
+
|
|
374
|
+
// Original should be unchanged
|
|
375
|
+
expect(usage.byModel['opus'].input).not.toBe(999);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// Integration Tests
|
|
381
|
+
// ============================================================================
|
|
382
|
+
|
|
383
|
+
describe('Token Tracking Integration', () => {
|
|
384
|
+
it('should track a realistic session workflow', () => {
|
|
385
|
+
const usage = createTokenUsage();
|
|
386
|
+
|
|
387
|
+
// Agent starts with opus model
|
|
388
|
+
setCurrentModel(usage, 'opus');
|
|
389
|
+
|
|
390
|
+
// Start session
|
|
391
|
+
trackTokenUsage(
|
|
392
|
+
usage,
|
|
393
|
+
'start_work_session',
|
|
394
|
+
{ git_url: 'https://github.com/org/repo', model: 'opus' },
|
|
395
|
+
{
|
|
396
|
+
session_id: '123',
|
|
397
|
+
persona: 'Atlas',
|
|
398
|
+
next_task: { id: 'task-1', title: 'Fix bug' },
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// Update task
|
|
403
|
+
trackTokenUsage(
|
|
404
|
+
usage,
|
|
405
|
+
'update_task',
|
|
406
|
+
{ task_id: 'task-1', status: 'in_progress', progress_percentage: 25 },
|
|
407
|
+
{ success: true }
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Complete task
|
|
411
|
+
trackTokenUsage(
|
|
412
|
+
usage,
|
|
413
|
+
'complete_task',
|
|
414
|
+
{ task_id: 'task-1', summary: 'Fixed the bug' },
|
|
415
|
+
{ success: true, next_task: null }
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const summary = getTokenUsageSummary(usage);
|
|
419
|
+
|
|
420
|
+
expect(summary.total_calls).toBe(3);
|
|
421
|
+
expect(summary.by_tool['start_work_session'].calls).toBe(1);
|
|
422
|
+
expect(summary.by_tool['update_task'].calls).toBe(1);
|
|
423
|
+
expect(summary.by_tool['complete_task'].calls).toBe(1);
|
|
424
|
+
expect(summary.by_model['opus']).toBeDefined();
|
|
425
|
+
expect(summary.by_model['opus'].input).toBeGreaterThan(0);
|
|
426
|
+
expect(summary.by_model['opus'].output).toBeGreaterThan(0);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should handle session reset', () => {
|
|
430
|
+
const usage = createTokenUsage();
|
|
431
|
+
|
|
432
|
+
// First session
|
|
433
|
+
setCurrentModel(usage, 'sonnet');
|
|
434
|
+
trackTokenUsage(usage, 'tool1', {}, {});
|
|
435
|
+
trackTokenUsage(usage, 'tool2', {}, {});
|
|
436
|
+
|
|
437
|
+
const firstSummary = getTokenUsageSummary(usage);
|
|
438
|
+
expect(firstSummary.total_calls).toBe(2);
|
|
439
|
+
|
|
440
|
+
// Reset for new session
|
|
441
|
+
resetTokenUsage(usage);
|
|
442
|
+
|
|
443
|
+
// Second session
|
|
444
|
+
setCurrentModel(usage, 'haiku');
|
|
445
|
+
trackTokenUsage(usage, 'tool3', {}, {});
|
|
446
|
+
|
|
447
|
+
const secondSummary = getTokenUsageSummary(usage);
|
|
448
|
+
expect(secondSummary.total_calls).toBe(1);
|
|
449
|
+
expect(secondSummary.by_tool['tool1']).toBeUndefined();
|
|
450
|
+
expect(secondSummary.by_tool['tool3']).toBeDefined();
|
|
451
|
+
expect(secondSummary.current_model).toBe('haiku');
|
|
452
|
+
});
|
|
453
|
+
});
|