rafcode 2.1.1 → 2.2.0
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/.claude/settings.local.json +4 -1
- package/CLAUDE.md +59 -11
- package/RAF/ahslfe-config-wizard/decisions.md +34 -0
- package/RAF/ahslfe-config-wizard/input.md +1 -0
- package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
- package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
- package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
- package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
- package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
- package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
- package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
- package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
- package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
- package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
- package/RAF/ahstvo-token-tracker/decisions.md +44 -0
- package/RAF/ahstvo-token-tracker/input.md +3 -0
- package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
- package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
- package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
- package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
- package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
- package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
- package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
- package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
- package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
- package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
- package/README.md +34 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +173 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +47 -6
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +3 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +19 -2
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +43 -96
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +6 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +10 -3
- package/dist/core/git.js.map +1 -1
- package/dist/core/pull-request.d.ts +1 -1
- package/dist/core/pull-request.d.ts.map +1 -1
- package/dist/core/pull-request.js +7 -4
- package/dist/core/pull-request.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +16 -1
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +34 -4
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/execution.d.ts.map +1 -1
- package/dist/prompts/execution.js +11 -1
- package/dist/prompts/execution.js.map +1 -1
- package/dist/types/config.d.ts +95 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +63 -3
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +59 -7
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +276 -21
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/name-generator.d.ts +3 -7
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +75 -61
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +21 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +62 -0
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +45 -0
- package/dist/utils/token-tracker.d.ts.map +1 -0
- package/dist/utils/token-tracker.js +107 -0
- package/dist/utils/token-tracker.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -5
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +10 -6
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/verbose-toggle.d.ts +33 -0
- package/dist/utils/verbose-toggle.d.ts.map +1 -0
- package/dist/utils/verbose-toggle.js +94 -0
- package/dist/utils/verbose-toggle.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/config.ts +204 -0
- package/src/commands/do.ts +56 -5
- package/src/commands/plan.ts +3 -2
- package/src/core/claude-runner.ts +59 -115
- package/src/core/failure-analyzer.ts +6 -3
- package/src/core/git.ts +10 -3
- package/src/core/pull-request.ts +7 -4
- package/src/index.ts +2 -0
- package/src/parsers/stream-renderer.ts +54 -4
- package/src/prompts/config-docs.md +331 -0
- package/src/prompts/execution.ts +13 -1
- package/src/types/config.ts +156 -7
- package/src/utils/config.ts +335 -21
- package/src/utils/name-generator.ts +84 -71
- package/src/utils/terminal-symbols.ts +68 -0
- package/src/utils/token-tracker.ts +135 -0
- package/src/utils/validation.ts +15 -10
- package/src/utils/verbose-toggle.ts +103 -0
- package/tests/unit/claude-runner.test.ts +171 -7
- package/tests/unit/config-command.test.ts +163 -0
- package/tests/unit/config.test.ts +608 -30
- package/tests/unit/name-generator.test.ts +99 -75
- package/tests/unit/pull-request.test.ts +2 -0
- package/tests/unit/stream-renderer.test.ts +83 -0
- package/tests/unit/terminal-symbols.test.ts +157 -0
- package/tests/unit/token-tracker.test.ts +352 -0
- package/tests/unit/verbose-toggle.test.ts +204 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { TokenTracker, CostBreakdown } from '../../src/utils/token-tracker.js';
|
|
2
|
+
import { UsageData, PricingConfig, DEFAULT_CONFIG } from '../../src/types/config.js';
|
|
3
|
+
|
|
4
|
+
function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
|
|
5
|
+
return {
|
|
6
|
+
inputTokens: 0,
|
|
7
|
+
outputTokens: 0,
|
|
8
|
+
cacheReadInputTokens: 0,
|
|
9
|
+
cacheCreationInputTokens: 0,
|
|
10
|
+
modelUsage: {},
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const testPricing: PricingConfig = DEFAULT_CONFIG.pricing;
|
|
16
|
+
|
|
17
|
+
describe('TokenTracker', () => {
|
|
18
|
+
describe('calculateCost', () => {
|
|
19
|
+
it('should calculate cost for opus model usage', () => {
|
|
20
|
+
const tracker = new TokenTracker(testPricing);
|
|
21
|
+
const usage = makeUsage({
|
|
22
|
+
inputTokens: 1_000_000,
|
|
23
|
+
outputTokens: 500_000,
|
|
24
|
+
cacheReadInputTokens: 200_000,
|
|
25
|
+
cacheCreationInputTokens: 100_000,
|
|
26
|
+
modelUsage: {
|
|
27
|
+
'claude-opus-4-6': {
|
|
28
|
+
inputTokens: 1_000_000,
|
|
29
|
+
outputTokens: 500_000,
|
|
30
|
+
cacheReadInputTokens: 200_000,
|
|
31
|
+
cacheCreationInputTokens: 100_000,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const cost = tracker.calculateCost(usage);
|
|
37
|
+
expect(cost.inputCost).toBeCloseTo(15); // 1M * $15/MTok
|
|
38
|
+
expect(cost.outputCost).toBeCloseTo(37.5); // 0.5M * $75/MTok
|
|
39
|
+
expect(cost.cacheReadCost).toBeCloseTo(0.3); // 0.2M * $1.5/MTok
|
|
40
|
+
expect(cost.cacheCreateCost).toBeCloseTo(1.875); // 0.1M * $18.75/MTok
|
|
41
|
+
expect(cost.totalCost).toBeCloseTo(15 + 37.5 + 0.3 + 1.875);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should calculate cost for sonnet model usage', () => {
|
|
45
|
+
const tracker = new TokenTracker(testPricing);
|
|
46
|
+
const usage = makeUsage({
|
|
47
|
+
inputTokens: 1_000_000,
|
|
48
|
+
outputTokens: 1_000_000,
|
|
49
|
+
modelUsage: {
|
|
50
|
+
'claude-sonnet-4-5-20250929': {
|
|
51
|
+
inputTokens: 1_000_000,
|
|
52
|
+
outputTokens: 1_000_000,
|
|
53
|
+
cacheReadInputTokens: 0,
|
|
54
|
+
cacheCreationInputTokens: 0,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const cost = tracker.calculateCost(usage);
|
|
60
|
+
expect(cost.inputCost).toBeCloseTo(3); // 1M * $3/MTok
|
|
61
|
+
expect(cost.outputCost).toBeCloseTo(15); // 1M * $15/MTok
|
|
62
|
+
expect(cost.totalCost).toBeCloseTo(18);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should calculate cost for haiku model usage', () => {
|
|
66
|
+
const tracker = new TokenTracker(testPricing);
|
|
67
|
+
const usage = makeUsage({
|
|
68
|
+
inputTokens: 2_000_000,
|
|
69
|
+
outputTokens: 1_000_000,
|
|
70
|
+
modelUsage: {
|
|
71
|
+
'claude-haiku-4-5-20251001': {
|
|
72
|
+
inputTokens: 2_000_000,
|
|
73
|
+
outputTokens: 1_000_000,
|
|
74
|
+
cacheReadInputTokens: 0,
|
|
75
|
+
cacheCreationInputTokens: 0,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const cost = tracker.calculateCost(usage);
|
|
81
|
+
expect(cost.inputCost).toBeCloseTo(2); // 2M * $1/MTok
|
|
82
|
+
expect(cost.outputCost).toBeCloseTo(5); // 1M * $5/MTok
|
|
83
|
+
expect(cost.totalCost).toBeCloseTo(7);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle multi-model usage in a single task', () => {
|
|
87
|
+
const tracker = new TokenTracker(testPricing);
|
|
88
|
+
const usage = makeUsage({
|
|
89
|
+
inputTokens: 2_000_000,
|
|
90
|
+
outputTokens: 1_500_000,
|
|
91
|
+
modelUsage: {
|
|
92
|
+
'claude-opus-4-6': {
|
|
93
|
+
inputTokens: 1_000_000,
|
|
94
|
+
outputTokens: 500_000,
|
|
95
|
+
cacheReadInputTokens: 0,
|
|
96
|
+
cacheCreationInputTokens: 0,
|
|
97
|
+
},
|
|
98
|
+
'claude-haiku-4-5-20251001': {
|
|
99
|
+
inputTokens: 1_000_000,
|
|
100
|
+
outputTokens: 1_000_000,
|
|
101
|
+
cacheReadInputTokens: 0,
|
|
102
|
+
cacheCreationInputTokens: 0,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const cost = tracker.calculateCost(usage);
|
|
108
|
+
// Opus: 1M*$15 + 0.5M*$75 = $15 + $37.5
|
|
109
|
+
// Haiku: 1M*$1 + 1M*$5 = $1 + $5
|
|
110
|
+
expect(cost.inputCost).toBeCloseTo(16); // 15 + 1
|
|
111
|
+
expect(cost.outputCost).toBeCloseTo(42.5); // 37.5 + 5
|
|
112
|
+
expect(cost.totalCost).toBeCloseTo(58.5);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should fallback to sonnet pricing when no model breakdown', () => {
|
|
116
|
+
const tracker = new TokenTracker(testPricing);
|
|
117
|
+
const usage = makeUsage({
|
|
118
|
+
inputTokens: 1_000_000,
|
|
119
|
+
outputTokens: 1_000_000,
|
|
120
|
+
modelUsage: {},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const cost = tracker.calculateCost(usage);
|
|
124
|
+
expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
|
|
125
|
+
expect(cost.outputCost).toBeCloseTo(15);
|
|
126
|
+
expect(cost.totalCost).toBeCloseTo(18);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should fallback to sonnet pricing for unknown model families', () => {
|
|
130
|
+
const tracker = new TokenTracker(testPricing);
|
|
131
|
+
const usage = makeUsage({
|
|
132
|
+
inputTokens: 1_000_000,
|
|
133
|
+
outputTokens: 1_000_000,
|
|
134
|
+
modelUsage: {
|
|
135
|
+
'claude-unknown-3-0': {
|
|
136
|
+
inputTokens: 1_000_000,
|
|
137
|
+
outputTokens: 1_000_000,
|
|
138
|
+
cacheReadInputTokens: 0,
|
|
139
|
+
cacheCreationInputTokens: 0,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const cost = tracker.calculateCost(usage);
|
|
145
|
+
expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
|
|
146
|
+
expect(cost.outputCost).toBeCloseTo(15);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return zero cost for zero tokens', () => {
|
|
150
|
+
const tracker = new TokenTracker(testPricing);
|
|
151
|
+
const usage = makeUsage();
|
|
152
|
+
const cost = tracker.calculateCost(usage);
|
|
153
|
+
expect(cost.totalCost).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should apply cache read discount correctly', () => {
|
|
157
|
+
const tracker = new TokenTracker(testPricing);
|
|
158
|
+
const usage = makeUsage({
|
|
159
|
+
cacheReadInputTokens: 1_000_000,
|
|
160
|
+
modelUsage: {
|
|
161
|
+
'claude-sonnet-4-5': {
|
|
162
|
+
inputTokens: 0,
|
|
163
|
+
outputTokens: 0,
|
|
164
|
+
cacheReadInputTokens: 1_000_000,
|
|
165
|
+
cacheCreationInputTokens: 0,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const cost = tracker.calculateCost(usage);
|
|
171
|
+
// Cache read: 1M * $0.30/MTok = $0.30 (90% off $3 input price)
|
|
172
|
+
expect(cost.cacheReadCost).toBeCloseTo(0.3);
|
|
173
|
+
expect(cost.totalCost).toBeCloseTo(0.3);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('addTask and accumulation', () => {
|
|
178
|
+
it('should accumulate usage across multiple tasks', () => {
|
|
179
|
+
const tracker = new TokenTracker(testPricing);
|
|
180
|
+
|
|
181
|
+
tracker.addTask('01', makeUsage({
|
|
182
|
+
inputTokens: 500_000,
|
|
183
|
+
outputTokens: 200_000,
|
|
184
|
+
modelUsage: {
|
|
185
|
+
'claude-opus-4-6': {
|
|
186
|
+
inputTokens: 500_000,
|
|
187
|
+
outputTokens: 200_000,
|
|
188
|
+
cacheReadInputTokens: 0,
|
|
189
|
+
cacheCreationInputTokens: 0,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
tracker.addTask('02', makeUsage({
|
|
195
|
+
inputTokens: 300_000,
|
|
196
|
+
outputTokens: 100_000,
|
|
197
|
+
modelUsage: {
|
|
198
|
+
'claude-opus-4-6': {
|
|
199
|
+
inputTokens: 300_000,
|
|
200
|
+
outputTokens: 100_000,
|
|
201
|
+
cacheReadInputTokens: 0,
|
|
202
|
+
cacheCreationInputTokens: 0,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const totals = tracker.getTotals();
|
|
208
|
+
expect(totals.usage.inputTokens).toBe(800_000);
|
|
209
|
+
expect(totals.usage.outputTokens).toBe(300_000);
|
|
210
|
+
expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(800_000);
|
|
211
|
+
expect(totals.usage.modelUsage['claude-opus-4-6']?.outputTokens).toBe(300_000);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should accumulate costs across multiple tasks', () => {
|
|
215
|
+
const tracker = new TokenTracker(testPricing);
|
|
216
|
+
|
|
217
|
+
const entry1 = tracker.addTask('01', makeUsage({
|
|
218
|
+
inputTokens: 1_000_000,
|
|
219
|
+
outputTokens: 1_000_000,
|
|
220
|
+
modelUsage: {
|
|
221
|
+
'claude-sonnet-4-5': {
|
|
222
|
+
inputTokens: 1_000_000,
|
|
223
|
+
outputTokens: 1_000_000,
|
|
224
|
+
cacheReadInputTokens: 0,
|
|
225
|
+
cacheCreationInputTokens: 0,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
const entry2 = tracker.addTask('02', makeUsage({
|
|
231
|
+
inputTokens: 1_000_000,
|
|
232
|
+
outputTokens: 1_000_000,
|
|
233
|
+
modelUsage: {
|
|
234
|
+
'claude-sonnet-4-5': {
|
|
235
|
+
inputTokens: 1_000_000,
|
|
236
|
+
outputTokens: 1_000_000,
|
|
237
|
+
cacheReadInputTokens: 0,
|
|
238
|
+
cacheCreationInputTokens: 0,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
const totals = tracker.getTotals();
|
|
244
|
+
// Each task: $3 input + $15 output = $18
|
|
245
|
+
expect(entry1.cost.totalCost).toBeCloseTo(18);
|
|
246
|
+
expect(entry2.cost.totalCost).toBeCloseTo(18);
|
|
247
|
+
expect(totals.cost.totalCost).toBeCloseTo(36);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should accumulate multi-model usage across tasks', () => {
|
|
251
|
+
const tracker = new TokenTracker(testPricing);
|
|
252
|
+
|
|
253
|
+
tracker.addTask('01', makeUsage({
|
|
254
|
+
inputTokens: 1_000_000,
|
|
255
|
+
outputTokens: 500_000,
|
|
256
|
+
modelUsage: {
|
|
257
|
+
'claude-opus-4-6': {
|
|
258
|
+
inputTokens: 1_000_000,
|
|
259
|
+
outputTokens: 500_000,
|
|
260
|
+
cacheReadInputTokens: 0,
|
|
261
|
+
cacheCreationInputTokens: 0,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
tracker.addTask('02', makeUsage({
|
|
267
|
+
inputTokens: 500_000,
|
|
268
|
+
outputTokens: 200_000,
|
|
269
|
+
modelUsage: {
|
|
270
|
+
'claude-haiku-4-5-20251001': {
|
|
271
|
+
inputTokens: 500_000,
|
|
272
|
+
outputTokens: 200_000,
|
|
273
|
+
cacheReadInputTokens: 0,
|
|
274
|
+
cacheCreationInputTokens: 0,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const totals = tracker.getTotals();
|
|
280
|
+
expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(1_000_000);
|
|
281
|
+
expect(totals.usage.modelUsage['claude-haiku-4-5-20251001']?.inputTokens).toBe(500_000);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should return empty totals when no tasks added', () => {
|
|
285
|
+
const tracker = new TokenTracker(testPricing);
|
|
286
|
+
const totals = tracker.getTotals();
|
|
287
|
+
expect(totals.usage.inputTokens).toBe(0);
|
|
288
|
+
expect(totals.usage.outputTokens).toBe(0);
|
|
289
|
+
expect(totals.cost.totalCost).toBe(0);
|
|
290
|
+
expect(Object.keys(totals.usage.modelUsage)).toHaveLength(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return per-task entries', () => {
|
|
294
|
+
const tracker = new TokenTracker(testPricing);
|
|
295
|
+
tracker.addTask('01', makeUsage({ inputTokens: 100 }));
|
|
296
|
+
tracker.addTask('02', makeUsage({ inputTokens: 200 }));
|
|
297
|
+
|
|
298
|
+
const entries = tracker.getEntries();
|
|
299
|
+
expect(entries).toHaveLength(2);
|
|
300
|
+
expect(entries[0].taskId).toBe('01');
|
|
301
|
+
expect(entries[1].taskId).toBe('02');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('addTask returns the entry with cost', () => {
|
|
305
|
+
const tracker = new TokenTracker(testPricing);
|
|
306
|
+
const entry = tracker.addTask('01', makeUsage({
|
|
307
|
+
inputTokens: 1_000_000,
|
|
308
|
+
modelUsage: {
|
|
309
|
+
'claude-opus-4-6': {
|
|
310
|
+
inputTokens: 1_000_000,
|
|
311
|
+
outputTokens: 0,
|
|
312
|
+
cacheReadInputTokens: 0,
|
|
313
|
+
cacheCreationInputTokens: 0,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
expect(entry.taskId).toBe('01');
|
|
319
|
+
expect(entry.cost.inputCost).toBeCloseTo(15);
|
|
320
|
+
expect(entry.cost.totalCost).toBeCloseTo(15);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('custom pricing', () => {
|
|
325
|
+
it('should use custom pricing config', () => {
|
|
326
|
+
const customPricing: PricingConfig = {
|
|
327
|
+
opus: { inputPerMTok: 10, outputPerMTok: 50, cacheReadPerMTok: 1, cacheCreatePerMTok: 12.5 },
|
|
328
|
+
sonnet: { inputPerMTok: 2, outputPerMTok: 10, cacheReadPerMTok: 0.2, cacheCreatePerMTok: 2.5 },
|
|
329
|
+
haiku: { inputPerMTok: 0.5, outputPerMTok: 2.5, cacheReadPerMTok: 0.05, cacheCreatePerMTok: 0.625 },
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const tracker = new TokenTracker(customPricing);
|
|
333
|
+
const usage = makeUsage({
|
|
334
|
+
inputTokens: 1_000_000,
|
|
335
|
+
outputTokens: 1_000_000,
|
|
336
|
+
modelUsage: {
|
|
337
|
+
'claude-opus-4-6': {
|
|
338
|
+
inputTokens: 1_000_000,
|
|
339
|
+
outputTokens: 1_000_000,
|
|
340
|
+
cacheReadInputTokens: 0,
|
|
341
|
+
cacheCreationInputTokens: 0,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const cost = tracker.calculateCost(usage);
|
|
347
|
+
expect(cost.inputCost).toBeCloseTo(10); // 1M * $10/MTok
|
|
348
|
+
expect(cost.outputCost).toBeCloseTo(50); // 1M * $50/MTok
|
|
349
|
+
expect(cost.totalCost).toBeCloseTo(60);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
// Mock logger to capture output
|
|
5
|
+
const mockDim = jest.fn();
|
|
6
|
+
jest.unstable_mockModule('../../src/utils/logger.js', () => ({
|
|
7
|
+
logger: {
|
|
8
|
+
dim: mockDim,
|
|
9
|
+
info: jest.fn(),
|
|
10
|
+
debug: jest.fn(),
|
|
11
|
+
warn: jest.fn(),
|
|
12
|
+
error: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const { VerboseToggle } = await import('../../src/utils/verbose-toggle.js');
|
|
17
|
+
|
|
18
|
+
describe('VerboseToggle', () => {
|
|
19
|
+
// Save original stdin properties
|
|
20
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
21
|
+
const originalSetRawMode = process.stdin.setRawMode;
|
|
22
|
+
const originalResume = process.stdin.resume;
|
|
23
|
+
const originalPause = process.stdin.pause;
|
|
24
|
+
|
|
25
|
+
let mockSetRawMode: jest.Mock;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
mockSetRawMode = jest.fn();
|
|
30
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true, configurable: true });
|
|
31
|
+
(process.stdin as any).setRawMode = mockSetRawMode;
|
|
32
|
+
(process.stdin as any).resume = jest.fn();
|
|
33
|
+
(process.stdin as any).pause = jest.fn();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// Restore original stdin
|
|
38
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, writable: true, configurable: true });
|
|
39
|
+
if (originalSetRawMode) {
|
|
40
|
+
(process.stdin as any).setRawMode = originalSetRawMode;
|
|
41
|
+
}
|
|
42
|
+
(process.stdin as any).resume = originalResume;
|
|
43
|
+
(process.stdin as any).pause = originalPause;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('initializes with the provided verbose state', () => {
|
|
47
|
+
const toggle = new VerboseToggle(true);
|
|
48
|
+
expect(toggle.isVerbose).toBe(true);
|
|
49
|
+
|
|
50
|
+
const toggle2 = new VerboseToggle(false);
|
|
51
|
+
expect(toggle2.isVerbose).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('is not active before start()', () => {
|
|
55
|
+
const toggle = new VerboseToggle(false);
|
|
56
|
+
expect(toggle.isActive).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('becomes active after start() on a TTY', () => {
|
|
60
|
+
const toggle = new VerboseToggle(false);
|
|
61
|
+
toggle.start();
|
|
62
|
+
expect(toggle.isActive).toBe(true);
|
|
63
|
+
expect(mockSetRawMode).toHaveBeenCalledWith(true);
|
|
64
|
+
toggle.stop();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shows toggle hint on start', () => {
|
|
68
|
+
const toggle = new VerboseToggle(false);
|
|
69
|
+
toggle.start();
|
|
70
|
+
expect(mockDim).toHaveBeenCalledWith(' Press Tab to toggle verbose mode');
|
|
71
|
+
toggle.stop();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('skips start when stdin is not a TTY', () => {
|
|
75
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, writable: true, configurable: true });
|
|
76
|
+
const toggle = new VerboseToggle(false);
|
|
77
|
+
toggle.start();
|
|
78
|
+
expect(toggle.isActive).toBe(false);
|
|
79
|
+
expect(mockSetRawMode).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('toggles verbose state on Tab keypress', () => {
|
|
83
|
+
const toggle = new VerboseToggle(false);
|
|
84
|
+
toggle.start();
|
|
85
|
+
|
|
86
|
+
// Simulate Tab key (0x09)
|
|
87
|
+
process.stdin.emit('data', Buffer.from([0x09]));
|
|
88
|
+
expect(toggle.isVerbose).toBe(true);
|
|
89
|
+
expect(mockDim).toHaveBeenCalledWith(' [verbose: on]');
|
|
90
|
+
|
|
91
|
+
// Toggle back
|
|
92
|
+
process.stdin.emit('data', Buffer.from([0x09]));
|
|
93
|
+
expect(toggle.isVerbose).toBe(false);
|
|
94
|
+
expect(mockDim).toHaveBeenCalledWith(' [verbose: off]');
|
|
95
|
+
|
|
96
|
+
toggle.stop();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('emits SIGINT on Ctrl+C keypress', () => {
|
|
100
|
+
const toggle = new VerboseToggle(false);
|
|
101
|
+
toggle.start();
|
|
102
|
+
|
|
103
|
+
const sigintHandler = jest.fn();
|
|
104
|
+
process.once('SIGINT', sigintHandler);
|
|
105
|
+
|
|
106
|
+
// Simulate Ctrl+C (0x03)
|
|
107
|
+
process.stdin.emit('data', Buffer.from([0x03]));
|
|
108
|
+
expect(sigintHandler).toHaveBeenCalled();
|
|
109
|
+
|
|
110
|
+
toggle.stop();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('ignores non-Tab, non-Ctrl+C keypresses', () => {
|
|
114
|
+
const toggle = new VerboseToggle(false);
|
|
115
|
+
toggle.start();
|
|
116
|
+
|
|
117
|
+
// Simulate 'a' keypress
|
|
118
|
+
process.stdin.emit('data', Buffer.from([0x61]));
|
|
119
|
+
expect(toggle.isVerbose).toBe(false);
|
|
120
|
+
|
|
121
|
+
toggle.stop();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles multiple bytes in a single data event', () => {
|
|
125
|
+
const toggle = new VerboseToggle(false);
|
|
126
|
+
toggle.start();
|
|
127
|
+
|
|
128
|
+
// Two Tab keys in one buffer
|
|
129
|
+
process.stdin.emit('data', Buffer.from([0x09, 0x09]));
|
|
130
|
+
// Should toggle twice → back to false
|
|
131
|
+
expect(toggle.isVerbose).toBe(false);
|
|
132
|
+
|
|
133
|
+
toggle.stop();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('restores stdin on stop()', () => {
|
|
137
|
+
const toggle = new VerboseToggle(false);
|
|
138
|
+
toggle.start();
|
|
139
|
+
toggle.stop();
|
|
140
|
+
|
|
141
|
+
expect(toggle.isActive).toBe(false);
|
|
142
|
+
expect(mockSetRawMode).toHaveBeenCalledWith(false);
|
|
143
|
+
expect(process.stdin.pause).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('is safe to call stop() multiple times', () => {
|
|
147
|
+
const toggle = new VerboseToggle(false);
|
|
148
|
+
toggle.start();
|
|
149
|
+
toggle.stop();
|
|
150
|
+
toggle.stop(); // should not throw
|
|
151
|
+
expect(toggle.isActive).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('is safe to call start() multiple times', () => {
|
|
155
|
+
const toggle = new VerboseToggle(false);
|
|
156
|
+
toggle.start();
|
|
157
|
+
toggle.start(); // should not start again
|
|
158
|
+
expect(mockSetRawMode).toHaveBeenCalledTimes(1);
|
|
159
|
+
toggle.stop();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does not respond to keypress after stop()', () => {
|
|
163
|
+
const toggle = new VerboseToggle(false);
|
|
164
|
+
toggle.start();
|
|
165
|
+
toggle.stop();
|
|
166
|
+
|
|
167
|
+
// Clear mocks to check no new calls
|
|
168
|
+
mockDim.mockClear();
|
|
169
|
+
|
|
170
|
+
// This should not trigger any toggle
|
|
171
|
+
process.stdin.emit('data', Buffer.from([0x09]));
|
|
172
|
+
expect(toggle.isVerbose).toBe(false);
|
|
173
|
+
// Only the hint message was logged (already cleared by mockClear), no toggle messages
|
|
174
|
+
expect(mockDim).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('works correctly across multiple tasks (stop and restart)', () => {
|
|
178
|
+
const toggle = new VerboseToggle(false);
|
|
179
|
+
|
|
180
|
+
// First task
|
|
181
|
+
toggle.start();
|
|
182
|
+
process.stdin.emit('data', Buffer.from([0x09]));
|
|
183
|
+
expect(toggle.isVerbose).toBe(true);
|
|
184
|
+
toggle.stop();
|
|
185
|
+
|
|
186
|
+
// State persists after stop
|
|
187
|
+
expect(toggle.isVerbose).toBe(true);
|
|
188
|
+
|
|
189
|
+
// Restart for second task
|
|
190
|
+
toggle.start();
|
|
191
|
+
expect(toggle.isVerbose).toBe(true); // Still true from previous toggle
|
|
192
|
+
process.stdin.emit('data', Buffer.from([0x09]));
|
|
193
|
+
expect(toggle.isVerbose).toBe(false);
|
|
194
|
+
toggle.stop();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('handles setRawMode throwing an error', () => {
|
|
198
|
+
mockSetRawMode.mockImplementation(() => { throw new Error('Cannot set raw mode'); });
|
|
199
|
+
const toggle = new VerboseToggle(false);
|
|
200
|
+
toggle.start();
|
|
201
|
+
// Should gracefully skip activation
|
|
202
|
+
expect(toggle.isActive).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|