tlc-claude-code 1.2.29 → 1.3.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/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/package.json +1 -1
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/package-lock.json +14 -0
- package/server/package.json +1 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
const { UsageCommand, parseArgs } = await import('./usage-command.js');
|
|
7
|
+
|
|
8
|
+
describe('usage-command', () => {
|
|
9
|
+
let usageCommand;
|
|
10
|
+
let tempDir;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Create temp directory for tests
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'usage-cmd-test-'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Clean up temp directory
|
|
19
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('parseArgs', () => {
|
|
23
|
+
it('parses empty args', () => {
|
|
24
|
+
const result = parseArgs([]);
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
reset: false,
|
|
27
|
+
model: null,
|
|
28
|
+
json: false,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('parses --reset flag', () => {
|
|
33
|
+
const result = parseArgs(['--reset']);
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
reset: true,
|
|
36
|
+
model: null,
|
|
37
|
+
json: false,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('parses --model flag with value', () => {
|
|
42
|
+
const result = parseArgs(['--model', 'openai']);
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
reset: false,
|
|
45
|
+
model: 'openai',
|
|
46
|
+
json: false,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('parses --json flag', () => {
|
|
51
|
+
const result = parseArgs(['--json']);
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
reset: false,
|
|
54
|
+
model: null,
|
|
55
|
+
json: true,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('parses multiple flags together', () => {
|
|
60
|
+
const result = parseArgs(['--model', 'deepseek', '--json']);
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
reset: false,
|
|
63
|
+
model: 'deepseek',
|
|
64
|
+
json: true,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('handles --model= syntax', () => {
|
|
69
|
+
const result = parseArgs(['--model=openai']);
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
reset: false,
|
|
72
|
+
model: 'openai',
|
|
73
|
+
json: false,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('execute with mocked dependencies', () => {
|
|
79
|
+
let mockBudgetTracker;
|
|
80
|
+
let mockUsageHistory;
|
|
81
|
+
let mockBudgetAlerts;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
mockBudgetTracker = {
|
|
85
|
+
getUsage: vi.fn().mockReturnValue({ daily: 5.00, monthly: 50.00, requests: 100 }),
|
|
86
|
+
reset: vi.fn(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
mockUsageHistory = {
|
|
90
|
+
getHistory: vi.fn().mockReturnValue([
|
|
91
|
+
{ date: '2026-01-25', daily: 4.50, monthly: 45.00 },
|
|
92
|
+
{ date: '2026-01-26', daily: 5.00, monthly: 50.00 },
|
|
93
|
+
]),
|
|
94
|
+
getAllModels: vi.fn().mockReturnValue(['openai', 'deepseek']),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
mockBudgetAlerts = {
|
|
98
|
+
checkThresholds: vi.fn().mockReturnValue([]),
|
|
99
|
+
resetAlerts: vi.fn(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
usageCommand = new UsageCommand({
|
|
103
|
+
budgetTracker: mockBudgetTracker,
|
|
104
|
+
usageHistory: mockUsageHistory,
|
|
105
|
+
budgetAlerts: mockBudgetAlerts,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const defaultConfig = {
|
|
110
|
+
openai: { budgetDaily: 10, budgetMonthly: 100 },
|
|
111
|
+
deepseek: { budgetDaily: 5, budgetMonthly: 50 },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it('shows usage summary', () => {
|
|
115
|
+
const result = usageCommand.execute([], defaultConfig);
|
|
116
|
+
|
|
117
|
+
expect(result.success).toBe(true);
|
|
118
|
+
expect(result.output).toContain('Usage Summary');
|
|
119
|
+
expect(result.output).toContain('openai');
|
|
120
|
+
expect(result.output).toContain('deepseek');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('shows history chart in summary', () => {
|
|
124
|
+
const result = usageCommand.execute([], defaultConfig);
|
|
125
|
+
|
|
126
|
+
expect(result.output).toContain('7-Day Usage History');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('filters by model when --model flag provided', () => {
|
|
130
|
+
const result = usageCommand.execute(['--model', 'openai'], defaultConfig);
|
|
131
|
+
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
expect(mockBudgetTracker.getUsage).toHaveBeenCalledWith('openai');
|
|
134
|
+
expect(mockBudgetTracker.getUsage).not.toHaveBeenCalledWith('deepseek');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('outputs JSON format when --json flag provided', () => {
|
|
138
|
+
const result = usageCommand.execute(['--json'], defaultConfig);
|
|
139
|
+
|
|
140
|
+
expect(result.success).toBe(true);
|
|
141
|
+
expect(result.json).toBeDefined();
|
|
142
|
+
expect(result.json.models).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('includes usage data in JSON output', () => {
|
|
146
|
+
const result = usageCommand.execute(['--json'], defaultConfig);
|
|
147
|
+
|
|
148
|
+
expect(result.json.models.openai).toBeDefined();
|
|
149
|
+
expect(result.json.models.openai.daily).toBe(5.00);
|
|
150
|
+
expect(result.json.models.openai.monthly).toBe(50.00);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('includes totals in JSON output', () => {
|
|
154
|
+
const result = usageCommand.execute(['--json'], defaultConfig);
|
|
155
|
+
|
|
156
|
+
expect(result.json.totals).toBeDefined();
|
|
157
|
+
expect(result.json.totals.daily).toBe(10); // 5 + 5
|
|
158
|
+
expect(result.json.totals.monthly).toBe(100); // 50 + 50
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('resets usage when --reset flag provided', () => {
|
|
162
|
+
const result = usageCommand.execute(['--reset'], defaultConfig);
|
|
163
|
+
|
|
164
|
+
expect(result.success).toBe(true);
|
|
165
|
+
expect(result.output).toContain('reset');
|
|
166
|
+
expect(mockBudgetTracker.reset).toHaveBeenCalled();
|
|
167
|
+
expect(mockBudgetAlerts.resetAlerts).toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('resets only specified model with --reset --model', () => {
|
|
171
|
+
usageCommand.execute(['--reset', '--model', 'openai'], defaultConfig);
|
|
172
|
+
|
|
173
|
+
expect(mockBudgetTracker.reset).toHaveBeenCalledWith('openai');
|
|
174
|
+
expect(mockBudgetAlerts.resetAlerts).toHaveBeenCalledWith('openai');
|
|
175
|
+
expect(mockBudgetTracker.reset).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('handles no usage data gracefully', () => {
|
|
179
|
+
mockBudgetTracker.getUsage.mockReturnValue({ daily: 0, monthly: 0, requests: 0 });
|
|
180
|
+
mockUsageHistory.getHistory.mockReturnValue([]);
|
|
181
|
+
|
|
182
|
+
const result = usageCommand.execute([], defaultConfig);
|
|
183
|
+
|
|
184
|
+
expect(result.success).toBe(true);
|
|
185
|
+
// Should still show the summary even with zero usage
|
|
186
|
+
expect(result.output).toContain('Usage Summary');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('shows alerts if threshold crossed', () => {
|
|
190
|
+
const alert = {
|
|
191
|
+
model: 'openai',
|
|
192
|
+
threshold: 0.8,
|
|
193
|
+
currentSpend: 8.00,
|
|
194
|
+
budgetLimit: 10.00,
|
|
195
|
+
percentUsed: 80,
|
|
196
|
+
severity: 'caution',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
mockBudgetAlerts.checkThresholds.mockReturnValue([alert]);
|
|
200
|
+
|
|
201
|
+
const result = usageCommand.execute([], defaultConfig);
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(true);
|
|
204
|
+
expect(result.alerts).toBeDefined();
|
|
205
|
+
expect(result.alerts.length).toBeGreaterThan(0);
|
|
206
|
+
expect(result.alerts[0]).toContain('Budget Alert');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('includes alerts in JSON output', () => {
|
|
210
|
+
const alert = {
|
|
211
|
+
model: 'openai',
|
|
212
|
+
threshold: 0.8,
|
|
213
|
+
currentSpend: 8.00,
|
|
214
|
+
budgetLimit: 10.00,
|
|
215
|
+
percentUsed: 80,
|
|
216
|
+
severity: 'caution',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
mockBudgetAlerts.checkThresholds.mockReturnValue([alert]);
|
|
220
|
+
|
|
221
|
+
const result = usageCommand.execute(['--json'], defaultConfig);
|
|
222
|
+
|
|
223
|
+
expect(result.json.alerts).toBeDefined();
|
|
224
|
+
expect(result.json.alerts.length).toBeGreaterThan(0);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns error for unknown model with --model flag', () => {
|
|
228
|
+
const result = usageCommand.execute(['--model', 'unknown'], defaultConfig);
|
|
229
|
+
|
|
230
|
+
expect(result.success).toBe(false);
|
|
231
|
+
expect(result.error).toContain('unknown');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('getUsageData', () => {
|
|
236
|
+
let mockBudgetTracker;
|
|
237
|
+
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
mockBudgetTracker = {
|
|
240
|
+
getUsage: vi.fn().mockReturnValue({ daily: 5.00, monthly: 50.00, requests: 100 }),
|
|
241
|
+
reset: vi.fn(),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
usageCommand = new UsageCommand({
|
|
245
|
+
budgetTracker: mockBudgetTracker,
|
|
246
|
+
usageHistory: { getHistory: vi.fn(), getAllModels: vi.fn() },
|
|
247
|
+
budgetAlerts: { checkThresholds: vi.fn(), resetAlerts: vi.fn() },
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('collects usage data from all models', () => {
|
|
252
|
+
const data = usageCommand.getUsageData(['openai', 'deepseek']);
|
|
253
|
+
|
|
254
|
+
expect(data.openai).toBeDefined();
|
|
255
|
+
expect(data.deepseek).toBeDefined();
|
|
256
|
+
expect(mockBudgetTracker.getUsage).toHaveBeenCalledWith('openai');
|
|
257
|
+
expect(mockBudgetTracker.getUsage).toHaveBeenCalledWith('deepseek');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns empty object for empty model list', () => {
|
|
261
|
+
const data = usageCommand.getUsageData([]);
|
|
262
|
+
|
|
263
|
+
expect(data).toEqual({});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('getHistoryData', () => {
|
|
268
|
+
let mockUsageHistory;
|
|
269
|
+
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
mockUsageHistory = {
|
|
272
|
+
getHistory: vi.fn().mockReturnValue([{ date: '2026-01-25', daily: 5.0 }]),
|
|
273
|
+
getAllModels: vi.fn(),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
usageCommand = new UsageCommand({
|
|
277
|
+
budgetTracker: { getUsage: vi.fn(), reset: vi.fn() },
|
|
278
|
+
usageHistory: mockUsageHistory,
|
|
279
|
+
budgetAlerts: { checkThresholds: vi.fn(), resetAlerts: vi.fn() },
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('collects history from all models', () => {
|
|
284
|
+
usageCommand.getHistoryData(['openai', 'deepseek']);
|
|
285
|
+
|
|
286
|
+
expect(mockUsageHistory.getHistory).toHaveBeenCalledWith('openai');
|
|
287
|
+
expect(mockUsageHistory.getHistory).toHaveBeenCalledWith('deepseek');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('calculateTotals', () => {
|
|
292
|
+
beforeEach(() => {
|
|
293
|
+
usageCommand = new UsageCommand({
|
|
294
|
+
budgetTracker: { getUsage: vi.fn(), reset: vi.fn() },
|
|
295
|
+
usageHistory: { getHistory: vi.fn(), getAllModels: vi.fn() },
|
|
296
|
+
budgetAlerts: { checkThresholds: vi.fn(), resetAlerts: vi.fn() },
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('calculates totals across models', () => {
|
|
301
|
+
const usageData = {
|
|
302
|
+
openai: { daily: 5, monthly: 50, requests: 100 },
|
|
303
|
+
deepseek: { daily: 3, monthly: 30, requests: 60 },
|
|
304
|
+
};
|
|
305
|
+
const config = {
|
|
306
|
+
openai: { budgetDaily: 10, budgetMonthly: 100 },
|
|
307
|
+
deepseek: { budgetDaily: 5, budgetMonthly: 50 },
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const totals = usageCommand.calculateTotals(usageData, config);
|
|
311
|
+
|
|
312
|
+
expect(totals.daily).toBe(8);
|
|
313
|
+
expect(totals.monthly).toBe(80);
|
|
314
|
+
expect(totals.requests).toBe(160);
|
|
315
|
+
expect(totals.budgetDaily).toBe(15);
|
|
316
|
+
expect(totals.budgetMonthly).toBe(150);
|
|
317
|
+
expect(totals.remainingDaily).toBe(7);
|
|
318
|
+
expect(totals.remainingMonthly).toBe(70);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('handles missing config for model', () => {
|
|
322
|
+
const usageData = {
|
|
323
|
+
unknown: { daily: 1, monthly: 10, requests: 5 },
|
|
324
|
+
};
|
|
325
|
+
const config = {};
|
|
326
|
+
|
|
327
|
+
const totals = usageCommand.calculateTotals(usageData, config);
|
|
328
|
+
|
|
329
|
+
expect(totals.daily).toBe(1);
|
|
330
|
+
expect(totals.budgetDaily).toBe(0);
|
|
331
|
+
expect(totals.remainingDaily).toBe(0); // Can't be negative
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('checkAlerts', () => {
|
|
336
|
+
let mockBudgetAlerts;
|
|
337
|
+
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
mockBudgetAlerts = {
|
|
340
|
+
checkThresholds: vi.fn().mockReturnValue([]),
|
|
341
|
+
resetAlerts: vi.fn(),
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
usageCommand = new UsageCommand({
|
|
345
|
+
budgetTracker: { getUsage: vi.fn(), reset: vi.fn() },
|
|
346
|
+
usageHistory: { getHistory: vi.fn(), getAllModels: vi.fn() },
|
|
347
|
+
budgetAlerts: mockBudgetAlerts,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('checks thresholds for each model', () => {
|
|
352
|
+
const usageData = {
|
|
353
|
+
openai: { daily: 8, monthly: 80 },
|
|
354
|
+
deepseek: { daily: 4, monthly: 40 },
|
|
355
|
+
};
|
|
356
|
+
const config = {
|
|
357
|
+
openai: { budgetDaily: 10, budgetMonthly: 100 },
|
|
358
|
+
deepseek: { budgetDaily: 5, budgetMonthly: 50 },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
usageCommand.checkAlerts(['openai', 'deepseek'], usageData, config);
|
|
362
|
+
|
|
363
|
+
expect(mockBudgetAlerts.checkThresholds).toHaveBeenCalledWith('openai', usageData.openai, config.openai);
|
|
364
|
+
expect(mockBudgetAlerts.checkThresholds).toHaveBeenCalledWith('deepseek', usageData.deepseek, config.deepseek);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('aggregates alerts from all models', () => {
|
|
368
|
+
const alert1 = { model: 'openai', threshold: 0.8 };
|
|
369
|
+
const alert2 = { model: 'deepseek', threshold: 0.5 };
|
|
370
|
+
|
|
371
|
+
mockBudgetAlerts.checkThresholds
|
|
372
|
+
.mockReturnValueOnce([alert1])
|
|
373
|
+
.mockReturnValueOnce([alert2]);
|
|
374
|
+
|
|
375
|
+
const usageData = {
|
|
376
|
+
openai: { daily: 8, monthly: 80 },
|
|
377
|
+
deepseek: { daily: 2.5, monthly: 25 },
|
|
378
|
+
};
|
|
379
|
+
const config = {
|
|
380
|
+
openai: { budgetDaily: 10 },
|
|
381
|
+
deepseek: { budgetDaily: 5 },
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const alerts = usageCommand.checkAlerts(['openai', 'deepseek'], usageData, config);
|
|
385
|
+
|
|
386
|
+
expect(alerts).toHaveLength(2);
|
|
387
|
+
expect(alerts).toContainEqual(alert1);
|
|
388
|
+
expect(alerts).toContainEqual(alert2);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Formatter - Format usage data for display
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BAR_WIDTH = 20;
|
|
6
|
+
const BAR_CHAR = '\u2588'; // Full block character
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format amount as currency with dollar sign and 2 decimals
|
|
10
|
+
* @param {number} amount - Amount in USD
|
|
11
|
+
* @returns {string} Formatted currency string
|
|
12
|
+
*/
|
|
13
|
+
function formatCurrency(amount) {
|
|
14
|
+
return '$' + amount.toLocaleString('en-US', {
|
|
15
|
+
minimumFractionDigits: 2,
|
|
16
|
+
maximumFractionDigits: 2,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format budget percentage
|
|
22
|
+
* @param {number} used - Amount used
|
|
23
|
+
* @param {number} budget - Budget limit
|
|
24
|
+
* @returns {string} Percentage string
|
|
25
|
+
*/
|
|
26
|
+
function formatBudgetPercentage(used, budget) {
|
|
27
|
+
if (budget === 0) {
|
|
28
|
+
return 'N/A';
|
|
29
|
+
}
|
|
30
|
+
const percentage = Math.round((used / budget) * 100);
|
|
31
|
+
return `${percentage}%`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format usage data as a table
|
|
36
|
+
* @param {Object} usageData - Usage data per model { model: { daily, monthly, requests } }
|
|
37
|
+
* @param {Object} budgets - Budget config per model { model: { budgetDaily, budgetMonthly } }
|
|
38
|
+
* @returns {string} Formatted table
|
|
39
|
+
*/
|
|
40
|
+
function formatUsageTable(usageData, budgets) {
|
|
41
|
+
const models = Object.keys(usageData);
|
|
42
|
+
if (models.length === 0) {
|
|
43
|
+
return 'No usage data available.';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Calculate column widths
|
|
47
|
+
const headers = ['Model', 'Daily', 'Budget %', 'Monthly', 'Budget %', 'Requests'];
|
|
48
|
+
const rows = [];
|
|
49
|
+
|
|
50
|
+
for (const model of models) {
|
|
51
|
+
const usage = usageData[model];
|
|
52
|
+
const budget = budgets[model] || { budgetDaily: 0, budgetMonthly: 0 };
|
|
53
|
+
|
|
54
|
+
rows.push([
|
|
55
|
+
model,
|
|
56
|
+
formatCurrency(usage.daily),
|
|
57
|
+
formatBudgetPercentage(usage.daily, budget.budgetDaily),
|
|
58
|
+
formatCurrency(usage.monthly),
|
|
59
|
+
formatBudgetPercentage(usage.monthly, budget.budgetMonthly),
|
|
60
|
+
String(usage.requests),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate column widths
|
|
65
|
+
const colWidths = headers.map((h, i) => {
|
|
66
|
+
const dataWidths = rows.map(r => r[i].length);
|
|
67
|
+
return Math.max(h.length, ...dataWidths);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Build table
|
|
71
|
+
const separator = '+' + colWidths.map(w => '-'.repeat(w + 2)).join('+') + '+';
|
|
72
|
+
const formatRow = (cols) => '| ' + cols.map((c, i) => c.padEnd(colWidths[i])).join(' | ') + ' |';
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
separator,
|
|
76
|
+
formatRow(headers),
|
|
77
|
+
separator,
|
|
78
|
+
...rows.map(formatRow),
|
|
79
|
+
separator,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate ASCII bar chart for usage history
|
|
87
|
+
* @param {Array} history - Array of { date, daily } snapshots
|
|
88
|
+
* @param {number} budgetDaily - Daily budget for scaling
|
|
89
|
+
* @returns {string} ASCII bar chart
|
|
90
|
+
*/
|
|
91
|
+
function generateBarChart(history, budgetDaily) {
|
|
92
|
+
if (!history || history.length === 0) {
|
|
93
|
+
return 'No usage history available.';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lines = [];
|
|
97
|
+
lines.push('7-Day Usage History:');
|
|
98
|
+
lines.push('');
|
|
99
|
+
|
|
100
|
+
// Find max for scaling (use budget or max daily, whichever is larger)
|
|
101
|
+
const maxDaily = Math.max(...history.map(h => h.daily), budgetDaily);
|
|
102
|
+
|
|
103
|
+
for (const entry of history) {
|
|
104
|
+
const dateLabel = entry.date.slice(5); // MM-DD format
|
|
105
|
+
const percentage = maxDaily > 0 ? entry.daily / maxDaily : 0;
|
|
106
|
+
const barLength = Math.round(percentage * BAR_WIDTH);
|
|
107
|
+
const bar = BAR_CHAR.repeat(barLength);
|
|
108
|
+
const amount = formatCurrency(entry.daily);
|
|
109
|
+
|
|
110
|
+
// Mark over budget
|
|
111
|
+
const overBudget = entry.daily > budgetDaily;
|
|
112
|
+
const indicator = overBudget ? ' OVER!' : '';
|
|
113
|
+
|
|
114
|
+
lines.push(`${dateLabel} ${bar.padEnd(BAR_WIDTH)} ${amount}${indicator}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format complete usage summary
|
|
122
|
+
* @param {Object} usageData - Usage data per model
|
|
123
|
+
* @param {Object} budgets - Budget config per model
|
|
124
|
+
* @param {Object} history - History per model { model: [snapshots] }
|
|
125
|
+
* @returns {string} Complete formatted summary
|
|
126
|
+
*/
|
|
127
|
+
function formatUsageSummary(usageData, budgets, history) {
|
|
128
|
+
const models = Object.keys(usageData);
|
|
129
|
+
|
|
130
|
+
if (models.length === 0) {
|
|
131
|
+
return 'No usage data available.';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push('='.repeat(50));
|
|
136
|
+
lines.push('Usage Summary');
|
|
137
|
+
lines.push('='.repeat(50));
|
|
138
|
+
lines.push('');
|
|
139
|
+
|
|
140
|
+
// Usage table
|
|
141
|
+
lines.push(formatUsageTable(usageData, budgets));
|
|
142
|
+
lines.push('');
|
|
143
|
+
|
|
144
|
+
// Calculate totals
|
|
145
|
+
let totalDaily = 0;
|
|
146
|
+
let totalMonthly = 0;
|
|
147
|
+
let totalBudgetDaily = 0;
|
|
148
|
+
let totalBudgetMonthly = 0;
|
|
149
|
+
|
|
150
|
+
for (const model of models) {
|
|
151
|
+
const usage = usageData[model];
|
|
152
|
+
const budget = budgets[model] || { budgetDaily: 0, budgetMonthly: 0 };
|
|
153
|
+
|
|
154
|
+
totalDaily += usage.daily;
|
|
155
|
+
totalMonthly += usage.monthly;
|
|
156
|
+
totalBudgetDaily += budget.budgetDaily;
|
|
157
|
+
totalBudgetMonthly += budget.budgetMonthly;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Totals section
|
|
161
|
+
lines.push('Totals:');
|
|
162
|
+
lines.push(` Daily: ${formatCurrency(totalDaily)} / ${formatCurrency(totalBudgetDaily)} (${formatBudgetPercentage(totalDaily, totalBudgetDaily)})`);
|
|
163
|
+
lines.push(` Monthly: ${formatCurrency(totalMonthly)} / ${formatCurrency(totalBudgetMonthly)} (${formatBudgetPercentage(totalMonthly, totalBudgetMonthly)})`);
|
|
164
|
+
lines.push('');
|
|
165
|
+
|
|
166
|
+
// Remaining budget
|
|
167
|
+
lines.push('Remaining:');
|
|
168
|
+
lines.push(` Daily: ${formatCurrency(Math.max(0, totalBudgetDaily - totalDaily))}`);
|
|
169
|
+
lines.push(` Monthly: ${formatCurrency(Math.max(0, totalBudgetMonthly - totalMonthly))}`);
|
|
170
|
+
lines.push('');
|
|
171
|
+
|
|
172
|
+
// History charts per model
|
|
173
|
+
for (const model of models) {
|
|
174
|
+
const modelHistory = history[model] || [];
|
|
175
|
+
if (modelHistory.length > 0) {
|
|
176
|
+
const budget = budgets[model] || { budgetDaily: 10 };
|
|
177
|
+
lines.push(`${model}:`);
|
|
178
|
+
lines.push(generateBarChart(modelHistory, budget.budgetDaily));
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
formatCurrency,
|
|
188
|
+
formatUsageTable,
|
|
189
|
+
formatBudgetPercentage,
|
|
190
|
+
generateBarChart,
|
|
191
|
+
formatUsageSummary,
|
|
192
|
+
};
|