rafcode 2.1.1 → 2.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/.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/RAF/ahtahs-token-reaper/decisions.md +37 -0
- package/RAF/ahtahs-token-reaper/input.md +20 -0
- package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
- package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
- package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
- package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
- package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
- package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
- package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -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 +195 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +55 -7
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +5 -3
- 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 +9 -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 +65 -7
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +297 -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 +25 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +87 -0
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +55 -0
- package/dist/utils/token-tracker.d.ts.map +1 -0
- package/dist/utils/token-tracker.js +142 -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 +230 -0
- package/src/commands/do.ts +64 -6
- package/src/commands/plan.ts +5 -3
- 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 +9 -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 +357 -21
- package/src/utils/name-generator.ts +84 -71
- package/src/utils/terminal-symbols.ts +103 -0
- package/src/utils/token-tracker.ts +177 -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 +242 -0
- package/tests/unit/config.test.ts +632 -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 +245 -0
- package/tests/unit/timer-verbose-integration.test.ts +170 -0
- package/tests/unit/token-tracker.test.ts +685 -0
- package/tests/unit/verbose-toggle.test.ts +204 -0
|
@@ -4,8 +4,14 @@ import {
|
|
|
4
4
|
formatProjectHeader,
|
|
5
5
|
formatSummary,
|
|
6
6
|
formatProgressBar,
|
|
7
|
+
formatNumber,
|
|
8
|
+
formatCost,
|
|
9
|
+
formatTaskTokenSummary,
|
|
10
|
+
formatTokenTotalSummary,
|
|
7
11
|
TaskStatus,
|
|
8
12
|
} from '../../src/utils/terminal-symbols.js';
|
|
13
|
+
import type { UsageData } from '../../src/types/config.js';
|
|
14
|
+
import type { CostBreakdown, TaskUsageEntry } from '../../src/utils/token-tracker.js';
|
|
9
15
|
|
|
10
16
|
describe('Terminal Symbols', () => {
|
|
11
17
|
describe('SYMBOLS', () => {
|
|
@@ -231,4 +237,243 @@ describe('Terminal Symbols', () => {
|
|
|
231
237
|
expect(result).toBe('✓✗⊘○');
|
|
232
238
|
});
|
|
233
239
|
});
|
|
240
|
+
|
|
241
|
+
describe('formatNumber', () => {
|
|
242
|
+
it('should format small numbers without separators', () => {
|
|
243
|
+
expect(formatNumber(42)).toBe('42');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should format numbers with thousands separators', () => {
|
|
247
|
+
expect(formatNumber(12345)).toBe('12,345');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should format large numbers', () => {
|
|
251
|
+
expect(formatNumber(1234567)).toBe('1,234,567');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should format zero', () => {
|
|
255
|
+
expect(formatNumber(0)).toBe('0');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('formatCost', () => {
|
|
260
|
+
it('should format zero cost', () => {
|
|
261
|
+
expect(formatCost(0)).toBe('$0.00');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should format normal costs with 2 decimals', () => {
|
|
265
|
+
expect(formatCost(1.23)).toBe('$1.23');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should format small costs with 4 decimals', () => {
|
|
269
|
+
expect(formatCost(0.0042)).toBe('$0.0042');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should format costs just at the threshold', () => {
|
|
273
|
+
expect(formatCost(0.01)).toBe('$0.01');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should format costs below threshold', () => {
|
|
277
|
+
expect(formatCost(0.009)).toBe('$0.0090');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('formatTaskTokenSummary', () => {
|
|
282
|
+
const makeUsage = (overrides: Partial<UsageData> = {}): UsageData => ({
|
|
283
|
+
inputTokens: 5234,
|
|
284
|
+
outputTokens: 1023,
|
|
285
|
+
cacheReadInputTokens: 0,
|
|
286
|
+
cacheCreationInputTokens: 0,
|
|
287
|
+
modelUsage: {},
|
|
288
|
+
...overrides,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const makeCost = (total: number): CostBreakdown => ({
|
|
292
|
+
inputCost: 0,
|
|
293
|
+
outputCost: 0,
|
|
294
|
+
cacheReadCost: 0,
|
|
295
|
+
cacheCreateCost: 0,
|
|
296
|
+
totalCost: total,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const makeEntry = (usage: UsageData, cost: CostBreakdown, attempts?: UsageData[]): TaskUsageEntry => ({
|
|
300
|
+
taskId: '01',
|
|
301
|
+
usage,
|
|
302
|
+
cost,
|
|
303
|
+
attempts: attempts ?? [usage],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('single-attempt tasks', () => {
|
|
307
|
+
it('should format basic token summary without cache', () => {
|
|
308
|
+
const usage = makeUsage();
|
|
309
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42)));
|
|
310
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.42');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should include cache read tokens', () => {
|
|
314
|
+
const usage = makeUsage({ cacheReadInputTokens: 18500 });
|
|
315
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42)));
|
|
316
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read | Est. cost: $0.42');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should include cache creation tokens', () => {
|
|
320
|
+
const usage = makeUsage({ cacheCreationInputTokens: 5000 });
|
|
321
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.55)));
|
|
322
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 5,000 created | Est. cost: $0.55');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should include both cache read and creation tokens', () => {
|
|
326
|
+
const usage = makeUsage({ cacheReadInputTokens: 18500, cacheCreationInputTokens: 5000 });
|
|
327
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.75)));
|
|
328
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read / 5,000 created | Est. cost: $0.75');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should format small costs with 4 decimal places', () => {
|
|
332
|
+
const usage = makeUsage();
|
|
333
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.0042)));
|
|
334
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.0042');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should format single-attempt task with empty attempts array as single-line', () => {
|
|
338
|
+
const usage = makeUsage();
|
|
339
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42), []));
|
|
340
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.42');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('multi-attempt tasks', () => {
|
|
345
|
+
it('should show per-attempt breakdown with costs when calculateAttemptCost provided', () => {
|
|
346
|
+
const attempt1 = makeUsage({ inputTokens: 1234, outputTokens: 567 });
|
|
347
|
+
const attempt2 = makeUsage({ inputTokens: 2345, outputTokens: 890 });
|
|
348
|
+
const totalUsage = makeUsage({ inputTokens: 3579, outputTokens: 1457 });
|
|
349
|
+
const entry = makeEntry(totalUsage, makeCost(0.06), [attempt1, attempt2]);
|
|
350
|
+
|
|
351
|
+
const calculateCost = (usage: UsageData): CostBreakdown => ({
|
|
352
|
+
inputCost: 0,
|
|
353
|
+
outputCost: 0,
|
|
354
|
+
cacheReadCost: 0,
|
|
355
|
+
cacheCreateCost: 0,
|
|
356
|
+
totalCost: usage.inputTokens === 1234 ? 0.02 : 0.04,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const result = formatTaskTokenSummary(entry, calculateCost);
|
|
360
|
+
const lines = result.split('\n');
|
|
361
|
+
|
|
362
|
+
expect(lines).toHaveLength(3);
|
|
363
|
+
expect(lines[0]).toBe(' Attempt 1: 1,234 in / 567 out | Est. cost: $0.02');
|
|
364
|
+
expect(lines[1]).toBe(' Attempt 2: 2,345 in / 890 out | Est. cost: $0.04');
|
|
365
|
+
expect(lines[2]).toBe(' Total: 3,579 in / 1,457 out | Est. cost: $0.06');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should show per-attempt breakdown without costs when no calculateAttemptCost', () => {
|
|
369
|
+
const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200 });
|
|
370
|
+
const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400 });
|
|
371
|
+
const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600 });
|
|
372
|
+
const entry = makeEntry(totalUsage, makeCost(0.05), [attempt1, attempt2]);
|
|
373
|
+
|
|
374
|
+
const result = formatTaskTokenSummary(entry);
|
|
375
|
+
const lines = result.split('\n');
|
|
376
|
+
|
|
377
|
+
expect(lines).toHaveLength(3);
|
|
378
|
+
expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out | Est. cost: $0.00');
|
|
379
|
+
expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out | Est. cost: $0.00');
|
|
380
|
+
expect(lines[2]).toBe(' Total: 3,000 in / 600 out | Est. cost: $0.05');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should include cache tokens in per-attempt breakdown', () => {
|
|
384
|
+
const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, cacheReadInputTokens: 5000 });
|
|
385
|
+
const attempt2 = makeUsage({ inputTokens: 1500, outputTokens: 300, cacheCreationInputTokens: 2000 });
|
|
386
|
+
const totalUsage = makeUsage({
|
|
387
|
+
inputTokens: 2500,
|
|
388
|
+
outputTokens: 500,
|
|
389
|
+
cacheReadInputTokens: 5000,
|
|
390
|
+
cacheCreationInputTokens: 2000,
|
|
391
|
+
});
|
|
392
|
+
const entry = makeEntry(totalUsage, makeCost(0.08), [attempt1, attempt2]);
|
|
393
|
+
|
|
394
|
+
const result = formatTaskTokenSummary(entry);
|
|
395
|
+
const lines = result.split('\n');
|
|
396
|
+
|
|
397
|
+
expect(lines).toHaveLength(3);
|
|
398
|
+
expect(lines[0]).toContain('Cache: 5,000 read');
|
|
399
|
+
expect(lines[1]).toContain('Cache: 2,000 created');
|
|
400
|
+
expect(lines[2]).toContain('Cache: 5,000 read / 2,000 created');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should handle three or more attempts', () => {
|
|
404
|
+
const attempt1 = makeUsage({ inputTokens: 500, outputTokens: 100 });
|
|
405
|
+
const attempt2 = makeUsage({ inputTokens: 600, outputTokens: 120 });
|
|
406
|
+
const attempt3 = makeUsage({ inputTokens: 700, outputTokens: 140 });
|
|
407
|
+
const totalUsage = makeUsage({ inputTokens: 1800, outputTokens: 360 });
|
|
408
|
+
const entry = makeEntry(totalUsage, makeCost(0.10), [attempt1, attempt2, attempt3]);
|
|
409
|
+
|
|
410
|
+
const result = formatTaskTokenSummary(entry);
|
|
411
|
+
const lines = result.split('\n');
|
|
412
|
+
|
|
413
|
+
expect(lines).toHaveLength(4);
|
|
414
|
+
expect(lines[0]).toContain('Attempt 1');
|
|
415
|
+
expect(lines[1]).toContain('Attempt 2');
|
|
416
|
+
expect(lines[2]).toContain('Attempt 3');
|
|
417
|
+
expect(lines[3]).toContain('Total');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('formatTokenTotalSummary', () => {
|
|
423
|
+
const makeUsage = (overrides: Partial<UsageData> = {}): UsageData => ({
|
|
424
|
+
inputTokens: 45678,
|
|
425
|
+
outputTokens: 12345,
|
|
426
|
+
cacheReadInputTokens: 0,
|
|
427
|
+
cacheCreationInputTokens: 0,
|
|
428
|
+
modelUsage: {},
|
|
429
|
+
...overrides,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const makeCost = (total: number): CostBreakdown => ({
|
|
433
|
+
inputCost: 0,
|
|
434
|
+
outputCost: 0,
|
|
435
|
+
cacheReadCost: 0,
|
|
436
|
+
cacheCreateCost: 0,
|
|
437
|
+
totalCost: total,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should format total summary without cache', () => {
|
|
441
|
+
const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75));
|
|
442
|
+
expect(result).toContain('Token Usage Summary');
|
|
443
|
+
expect(result).toContain('Total tokens: 45,678 in / 12,345 out');
|
|
444
|
+
expect(result).toContain('Estimated cost: $3.75');
|
|
445
|
+
expect(result).not.toContain('Cache:');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should include cache read in total summary', () => {
|
|
449
|
+
const result = formatTokenTotalSummary(
|
|
450
|
+
makeUsage({ cacheReadInputTokens: 125000 }),
|
|
451
|
+
makeCost(3.75)
|
|
452
|
+
);
|
|
453
|
+
expect(result).toContain('Cache: 125,000 read');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should include cache creation in total summary', () => {
|
|
457
|
+
const result = formatTokenTotalSummary(
|
|
458
|
+
makeUsage({ cacheCreationInputTokens: 8000 }),
|
|
459
|
+
makeCost(3.75)
|
|
460
|
+
);
|
|
461
|
+
expect(result).toContain('Cache: 8,000 created');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should include both cache types in total summary', () => {
|
|
465
|
+
const result = formatTokenTotalSummary(
|
|
466
|
+
makeUsage({ cacheReadInputTokens: 125000, cacheCreationInputTokens: 8000 }),
|
|
467
|
+
makeCost(3.75)
|
|
468
|
+
);
|
|
469
|
+
expect(result).toContain('Cache: 125,000 read / 8,000 created');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should have divider lines', () => {
|
|
473
|
+
const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75));
|
|
474
|
+
const lines = result.split('\n');
|
|
475
|
+
expect(lines[0]).toContain('──');
|
|
476
|
+
expect(lines[lines.length - 1]).toContain('──');
|
|
477
|
+
});
|
|
478
|
+
});
|
|
234
479
|
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createStatusLine } from '../../src/utils/status-line.js';
|
|
3
|
+
import { createTaskTimer } from '../../src/utils/timer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests the interaction between the timer callback, status line, and verbose toggle.
|
|
7
|
+
* This mirrors the logic in do.ts where the onTick callback checks verboseToggle.isVerbose
|
|
8
|
+
* and clears/skips the status line when verbose is on.
|
|
9
|
+
*/
|
|
10
|
+
describe('Timer-Verbose Integration', () => {
|
|
11
|
+
let originalIsTTY: boolean | undefined;
|
|
12
|
+
let originalWrite: typeof process.stdout.write;
|
|
13
|
+
let writeOutput: string[];
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.useFakeTimers();
|
|
17
|
+
originalIsTTY = process.stdout.isTTY;
|
|
18
|
+
originalWrite = process.stdout.write;
|
|
19
|
+
writeOutput = [];
|
|
20
|
+
|
|
21
|
+
// Mock stdout.write to capture output
|
|
22
|
+
process.stdout.isTTY = true;
|
|
23
|
+
process.stdout.write = jest.fn((data: string | Uint8Array) => {
|
|
24
|
+
writeOutput.push(data.toString());
|
|
25
|
+
return true;
|
|
26
|
+
}) as typeof process.stdout.write;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
jest.useRealTimers();
|
|
31
|
+
process.stdout.isTTY = originalIsTTY;
|
|
32
|
+
process.stdout.write = originalWrite;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('onTick callback with verbose check', () => {
|
|
36
|
+
it('should update status line when verbose is off', () => {
|
|
37
|
+
const statusLine = createStatusLine();
|
|
38
|
+
let isVerbose = false;
|
|
39
|
+
|
|
40
|
+
const timer = createTaskTimer((elapsed) => {
|
|
41
|
+
if (isVerbose) {
|
|
42
|
+
statusLine.clear();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
statusLine.update(`Task running: ${elapsed}ms`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
timer.start();
|
|
49
|
+
expect(writeOutput.length).toBeGreaterThan(0);
|
|
50
|
+
expect(writeOutput.some(output => output.includes('Task running'))).toBe(true);
|
|
51
|
+
|
|
52
|
+
timer.stop();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should clear status line and skip update when verbose is toggled on', () => {
|
|
56
|
+
const statusLine = createStatusLine();
|
|
57
|
+
let isVerbose = false;
|
|
58
|
+
|
|
59
|
+
const timer = createTaskTimer((elapsed) => {
|
|
60
|
+
if (isVerbose) {
|
|
61
|
+
statusLine.clear();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
statusLine.update(`Task running: ${elapsed}ms`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
timer.start();
|
|
68
|
+
// Initial tick should update
|
|
69
|
+
expect(writeOutput.some(output => output.includes('Task running: 0ms'))).toBe(true);
|
|
70
|
+
|
|
71
|
+
// Toggle verbose on
|
|
72
|
+
isVerbose = true;
|
|
73
|
+
writeOutput = [];
|
|
74
|
+
|
|
75
|
+
// Next tick should clear the line instead of updating
|
|
76
|
+
jest.advanceTimersByTime(1000);
|
|
77
|
+
|
|
78
|
+
// Should have cleared (written empty/reset) but not updated with task info
|
|
79
|
+
expect(writeOutput.length).toBeGreaterThan(0);
|
|
80
|
+
// The clear writes spaces to overwrite, not "Task running"
|
|
81
|
+
expect(writeOutput.some(output => output.includes('Task running'))).toBe(false);
|
|
82
|
+
|
|
83
|
+
timer.stop();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should resume updating status line when verbose is toggled back off', () => {
|
|
87
|
+
const statusLine = createStatusLine();
|
|
88
|
+
let isVerbose = false;
|
|
89
|
+
|
|
90
|
+
const timer = createTaskTimer((elapsed) => {
|
|
91
|
+
if (isVerbose) {
|
|
92
|
+
statusLine.clear();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
statusLine.update(`Task running: ${elapsed}ms`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
timer.start();
|
|
99
|
+
// Toggle verbose on
|
|
100
|
+
isVerbose = true;
|
|
101
|
+
jest.advanceTimersByTime(1000);
|
|
102
|
+
|
|
103
|
+
// Toggle verbose back off
|
|
104
|
+
isVerbose = false;
|
|
105
|
+
writeOutput = [];
|
|
106
|
+
|
|
107
|
+
// Next tick should update normally
|
|
108
|
+
jest.advanceTimersByTime(1000);
|
|
109
|
+
|
|
110
|
+
expect(writeOutput.some(output => output.includes('Task running'))).toBe(true);
|
|
111
|
+
|
|
112
|
+
timer.stop();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should track elapsed time correctly regardless of verbose state', () => {
|
|
116
|
+
const statusLine = createStatusLine();
|
|
117
|
+
let isVerbose = false;
|
|
118
|
+
|
|
119
|
+
const timer = createTaskTimer((elapsed) => {
|
|
120
|
+
if (isVerbose) {
|
|
121
|
+
statusLine.clear();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
statusLine.update(`Task running: ${elapsed}ms`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
timer.start();
|
|
128
|
+
|
|
129
|
+
// Run some time with verbose off
|
|
130
|
+
jest.advanceTimersByTime(2000);
|
|
131
|
+
expect(timer.getElapsed()).toBeGreaterThanOrEqual(2000);
|
|
132
|
+
|
|
133
|
+
// Toggle verbose on - timer keeps running
|
|
134
|
+
isVerbose = true;
|
|
135
|
+
jest.advanceTimersByTime(3000);
|
|
136
|
+
expect(timer.getElapsed()).toBeGreaterThanOrEqual(5000);
|
|
137
|
+
|
|
138
|
+
// Toggle verbose off - elapsed time is accurate
|
|
139
|
+
isVerbose = false;
|
|
140
|
+
writeOutput = [];
|
|
141
|
+
jest.advanceTimersByTime(1000);
|
|
142
|
+
|
|
143
|
+
// The elapsed time should be around 6000ms now, visible in the output
|
|
144
|
+
expect(writeOutput.some(output => output.includes('Task running'))).toBe(true);
|
|
145
|
+
expect(timer.getElapsed()).toBeGreaterThanOrEqual(6000);
|
|
146
|
+
|
|
147
|
+
timer.stop();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not create timer callback when started with verbose flag', () => {
|
|
151
|
+
// This mirrors the do.ts logic: verbose ? undefined : (elapsed) => {...}
|
|
152
|
+
const initialVerbose = true;
|
|
153
|
+
const onTick = initialVerbose ? undefined : jest.fn();
|
|
154
|
+
|
|
155
|
+
const timer = createTaskTimer(onTick);
|
|
156
|
+
timer.start();
|
|
157
|
+
|
|
158
|
+
jest.advanceTimersByTime(5000);
|
|
159
|
+
|
|
160
|
+
// No callback should have been called
|
|
161
|
+
if (onTick) {
|
|
162
|
+
expect(onTick).not.toHaveBeenCalled();
|
|
163
|
+
}
|
|
164
|
+
// Timer still tracks time
|
|
165
|
+
expect(timer.getElapsed()).toBeGreaterThanOrEqual(5000);
|
|
166
|
+
|
|
167
|
+
timer.stop();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|