rafcode 2.2.0 → 2.4.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.
Files changed (125) hide show
  1. package/CLAUDE.md +19 -4
  2. package/RAF/ahtahs-token-reaper/decisions.md +37 -0
  3. package/RAF/ahtahs-token-reaper/input.md +20 -0
  4. package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
  5. package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
  6. package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
  7. package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
  8. package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
  9. package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
  10. package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
  11. package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
  12. package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
  13. package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
  14. package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
  15. package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -0
  16. package/RAF/ahvrih-rate-forge/decisions.md +70 -0
  17. package/RAF/ahvrih-rate-forge/input.md +44 -0
  18. package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
  19. package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
  20. package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
  21. package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
  22. package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
  23. package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
  24. package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
  25. package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
  26. package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
  27. package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
  28. package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
  29. package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
  30. package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
  31. package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
  32. package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
  33. package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
  34. package/README.md +27 -7
  35. package/dist/commands/config.d.ts.map +1 -1
  36. package/dist/commands/config.js +24 -7
  37. package/dist/commands/config.js.map +1 -1
  38. package/dist/commands/do.d.ts.map +1 -1
  39. package/dist/commands/do.js +122 -27
  40. package/dist/commands/do.js.map +1 -1
  41. package/dist/commands/plan.d.ts.map +1 -1
  42. package/dist/commands/plan.js +79 -3
  43. package/dist/commands/plan.js.map +1 -1
  44. package/dist/core/claude-runner.d.ts +6 -6
  45. package/dist/core/claude-runner.d.ts.map +1 -1
  46. package/dist/core/claude-runner.js +9 -10
  47. package/dist/core/claude-runner.js.map +1 -1
  48. package/dist/core/failure-analyzer.d.ts.map +1 -1
  49. package/dist/core/failure-analyzer.js +3 -3
  50. package/dist/core/failure-analyzer.js.map +1 -1
  51. package/dist/core/pull-request.d.ts.map +1 -1
  52. package/dist/core/pull-request.js +5 -3
  53. package/dist/core/pull-request.js.map +1 -1
  54. package/dist/core/state-derivation.d.ts +5 -0
  55. package/dist/core/state-derivation.d.ts.map +1 -1
  56. package/dist/core/state-derivation.js +14 -4
  57. package/dist/core/state-derivation.js.map +1 -1
  58. package/dist/core/worktree.d.ts +32 -0
  59. package/dist/core/worktree.d.ts.map +1 -1
  60. package/dist/core/worktree.js +215 -0
  61. package/dist/core/worktree.js.map +1 -1
  62. package/dist/prompts/amend.d.ts.map +1 -1
  63. package/dist/prompts/amend.js +26 -11
  64. package/dist/prompts/amend.js.map +1 -1
  65. package/dist/prompts/planning.d.ts.map +1 -1
  66. package/dist/prompts/planning.js +26 -11
  67. package/dist/prompts/planning.js.map +1 -1
  68. package/dist/types/config.d.ts +30 -13
  69. package/dist/types/config.d.ts.map +1 -1
  70. package/dist/types/config.js +14 -10
  71. package/dist/types/config.js.map +1 -1
  72. package/dist/utils/config.d.ts +53 -4
  73. package/dist/utils/config.d.ts.map +1 -1
  74. package/dist/utils/config.js +197 -30
  75. package/dist/utils/config.js.map +1 -1
  76. package/dist/utils/frontmatter.d.ts +43 -0
  77. package/dist/utils/frontmatter.d.ts.map +1 -0
  78. package/dist/utils/frontmatter.js +85 -0
  79. package/dist/utils/frontmatter.js.map +1 -0
  80. package/dist/utils/name-generator.d.ts.map +1 -1
  81. package/dist/utils/name-generator.js +2 -3
  82. package/dist/utils/name-generator.js.map +1 -1
  83. package/dist/utils/session-parser.d.ts +44 -0
  84. package/dist/utils/session-parser.d.ts.map +1 -0
  85. package/dist/utils/session-parser.js +122 -0
  86. package/dist/utils/session-parser.js.map +1 -0
  87. package/dist/utils/terminal-symbols.d.ts +28 -5
  88. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  89. package/dist/utils/terminal-symbols.js +77 -18
  90. package/dist/utils/terminal-symbols.js.map +1 -1
  91. package/dist/utils/token-tracker.d.ts +31 -1
  92. package/dist/utils/token-tracker.d.ts.map +1 -1
  93. package/dist/utils/token-tracker.js +94 -4
  94. package/dist/utils/token-tracker.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/commands/config.ts +26 -7
  97. package/src/commands/do.ts +157 -29
  98. package/src/commands/plan.ts +89 -2
  99. package/src/core/claude-runner.ts +16 -17
  100. package/src/core/failure-analyzer.ts +3 -3
  101. package/src/core/pull-request.ts +5 -3
  102. package/src/core/state-derivation.ts +20 -4
  103. package/src/core/worktree.ts +230 -0
  104. package/src/prompts/amend.ts +26 -11
  105. package/src/prompts/config-docs.md +91 -29
  106. package/src/prompts/planning.ts +26 -11
  107. package/src/types/config.ts +46 -21
  108. package/src/utils/config.ts +222 -33
  109. package/src/utils/frontmatter.ts +110 -0
  110. package/src/utils/name-generator.ts +2 -3
  111. package/src/utils/session-parser.ts +161 -0
  112. package/src/utils/terminal-symbols.ts +105 -18
  113. package/src/utils/token-tracker.ts +109 -4
  114. package/tests/unit/claude-runner-interactive.test.ts +8 -6
  115. package/tests/unit/claude-runner.test.ts +5 -66
  116. package/tests/unit/config-command.test.ts +84 -5
  117. package/tests/unit/config.test.ts +292 -45
  118. package/tests/unit/frontmatter.test.ts +182 -0
  119. package/tests/unit/post-execution-picker.test.ts +5 -0
  120. package/tests/unit/session-parser.test.ts +301 -0
  121. package/tests/unit/terminal-symbols.test.ts +263 -33
  122. package/tests/unit/timer-verbose-integration.test.ts +170 -0
  123. package/tests/unit/token-tracker.test.ts +653 -17
  124. package/tests/unit/validation.test.ts +6 -4
  125. package/tests/unit/worktree.test.ts +242 -0
@@ -8,10 +8,12 @@ import {
8
8
  formatCost,
9
9
  formatTaskTokenSummary,
10
10
  formatTokenTotalSummary,
11
+ formatRateLimitPercentage,
12
+ TokenSummaryOptions,
11
13
  TaskStatus,
12
14
  } from '../../src/utils/terminal-symbols.js';
13
15
  import type { UsageData } from '../../src/types/config.js';
14
- import type { CostBreakdown } from '../../src/utils/token-tracker.js';
16
+ import type { CostBreakdown, TaskUsageEntry } from '../../src/utils/token-tracker.js';
15
17
 
16
18
  describe('Terminal Symbols', () => {
17
19
  describe('SYMBOLS', () => {
@@ -296,38 +298,126 @@ describe('Terminal Symbols', () => {
296
298
  totalCost: total,
297
299
  });
298
300
 
299
- it('should format basic token summary without cache', () => {
300
- const result = formatTaskTokenSummary(makeUsage(), makeCost(0.42));
301
- expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.42');
302
- });
303
-
304
- it('should include cache read tokens', () => {
305
- const result = formatTaskTokenSummary(
306
- makeUsage({ cacheReadInputTokens: 18500 }),
307
- makeCost(0.42)
308
- );
309
- expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read | Est. cost: $0.42');
310
- });
311
-
312
- it('should include cache creation tokens', () => {
313
- const result = formatTaskTokenSummary(
314
- makeUsage({ cacheCreationInputTokens: 5000 }),
315
- makeCost(0.55)
316
- );
317
- expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 5,000 created | Est. cost: $0.55');
318
- });
319
-
320
- it('should include both cache read and creation tokens', () => {
321
- const result = formatTaskTokenSummary(
322
- makeUsage({ cacheReadInputTokens: 18500, cacheCreationInputTokens: 5000 }),
323
- makeCost(0.75)
324
- );
325
- expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read / 5,000 created | Est. cost: $0.75');
326
- });
327
-
328
- it('should format small costs with 4 decimal places', () => {
329
- const result = formatTaskTokenSummary(makeUsage(), makeCost(0.0042));
330
- expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.0042');
301
+ const makeEntry = (usage: UsageData, cost: CostBreakdown, attempts?: UsageData[]): TaskUsageEntry => ({
302
+ taskId: '01',
303
+ usage,
304
+ cost,
305
+ attempts: attempts ?? [usage],
306
+ });
307
+
308
+ describe('single-attempt tasks', () => {
309
+ it('should format basic token summary without cache', () => {
310
+ const usage = makeUsage();
311
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42)));
312
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.42');
313
+ });
314
+
315
+ it('should include cache read tokens', () => {
316
+ const usage = makeUsage({ cacheReadInputTokens: 18500 });
317
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42)));
318
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read | Est. cost: $0.42');
319
+ });
320
+
321
+ it('should include cache creation tokens', () => {
322
+ const usage = makeUsage({ cacheCreationInputTokens: 5000 });
323
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.55)));
324
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 5,000 created | Est. cost: $0.55');
325
+ });
326
+
327
+ it('should include both cache read and creation tokens', () => {
328
+ const usage = makeUsage({ cacheReadInputTokens: 18500, cacheCreationInputTokens: 5000 });
329
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.75)));
330
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cache: 18,500 read / 5,000 created | Est. cost: $0.75');
331
+ });
332
+
333
+ it('should format small costs with 4 decimal places', () => {
334
+ const usage = makeUsage();
335
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.0042)));
336
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.0042');
337
+ });
338
+
339
+ it('should format single-attempt task with empty attempts array as single-line', () => {
340
+ const usage = makeUsage();
341
+ const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42), []));
342
+ expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Est. cost: $0.42');
343
+ });
344
+ });
345
+
346
+ describe('multi-attempt tasks', () => {
347
+ it('should show per-attempt breakdown with costs when calculateAttemptCost provided', () => {
348
+ const attempt1 = makeUsage({ inputTokens: 1234, outputTokens: 567 });
349
+ const attempt2 = makeUsage({ inputTokens: 2345, outputTokens: 890 });
350
+ const totalUsage = makeUsage({ inputTokens: 3579, outputTokens: 1457 });
351
+ const entry = makeEntry(totalUsage, makeCost(0.06), [attempt1, attempt2]);
352
+
353
+ const calculateCost = (usage: UsageData): CostBreakdown => ({
354
+ inputCost: 0,
355
+ outputCost: 0,
356
+ cacheReadCost: 0,
357
+ cacheCreateCost: 0,
358
+ totalCost: usage.inputTokens === 1234 ? 0.02 : 0.04,
359
+ });
360
+
361
+ const result = formatTaskTokenSummary(entry, calculateCost);
362
+ const lines = result.split('\n');
363
+
364
+ expect(lines).toHaveLength(3);
365
+ expect(lines[0]).toBe(' Attempt 1: 1,234 in / 567 out | Est. cost: $0.02');
366
+ expect(lines[1]).toBe(' Attempt 2: 2,345 in / 890 out | Est. cost: $0.04');
367
+ expect(lines[2]).toBe(' Total: 3,579 in / 1,457 out | Est. cost: $0.06');
368
+ });
369
+
370
+ it('should show per-attempt breakdown without costs when no calculateAttemptCost', () => {
371
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200 });
372
+ const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400 });
373
+ const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600 });
374
+ const entry = makeEntry(totalUsage, makeCost(0.05), [attempt1, attempt2]);
375
+
376
+ const result = formatTaskTokenSummary(entry);
377
+ const lines = result.split('\n');
378
+
379
+ expect(lines).toHaveLength(3);
380
+ expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out | Est. cost: $0.00');
381
+ expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out | Est. cost: $0.00');
382
+ expect(lines[2]).toBe(' Total: 3,000 in / 600 out | Est. cost: $0.05');
383
+ });
384
+
385
+ it('should include cache tokens in per-attempt breakdown', () => {
386
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, cacheReadInputTokens: 5000 });
387
+ const attempt2 = makeUsage({ inputTokens: 1500, outputTokens: 300, cacheCreationInputTokens: 2000 });
388
+ const totalUsage = makeUsage({
389
+ inputTokens: 2500,
390
+ outputTokens: 500,
391
+ cacheReadInputTokens: 5000,
392
+ cacheCreationInputTokens: 2000,
393
+ });
394
+ const entry = makeEntry(totalUsage, makeCost(0.08), [attempt1, attempt2]);
395
+
396
+ const result = formatTaskTokenSummary(entry);
397
+ const lines = result.split('\n');
398
+
399
+ expect(lines).toHaveLength(3);
400
+ expect(lines[0]).toContain('Cache: 5,000 read');
401
+ expect(lines[1]).toContain('Cache: 2,000 created');
402
+ expect(lines[2]).toContain('Cache: 5,000 read / 2,000 created');
403
+ });
404
+
405
+ it('should handle three or more attempts', () => {
406
+ const attempt1 = makeUsage({ inputTokens: 500, outputTokens: 100 });
407
+ const attempt2 = makeUsage({ inputTokens: 600, outputTokens: 120 });
408
+ const attempt3 = makeUsage({ inputTokens: 700, outputTokens: 140 });
409
+ const totalUsage = makeUsage({ inputTokens: 1800, outputTokens: 360 });
410
+ const entry = makeEntry(totalUsage, makeCost(0.10), [attempt1, attempt2, attempt3]);
411
+
412
+ const result = formatTaskTokenSummary(entry);
413
+ const lines = result.split('\n');
414
+
415
+ expect(lines).toHaveLength(4);
416
+ expect(lines[0]).toContain('Attempt 1');
417
+ expect(lines[1]).toContain('Attempt 2');
418
+ expect(lines[2]).toContain('Attempt 3');
419
+ expect(lines[3]).toContain('Total');
420
+ });
331
421
  });
332
422
  });
333
423
 
@@ -387,5 +477,145 @@ describe('Terminal Symbols', () => {
387
477
  expect(lines[0]).toContain('──');
388
478
  expect(lines[lines.length - 1]).toContain('──');
389
479
  });
480
+
481
+ it('should include rate limit percentage when option enabled', () => {
482
+ const options: TokenSummaryOptions = {
483
+ showRateLimitEstimate: true,
484
+ rateLimitPercentage: 42.5,
485
+ };
486
+ const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75), options);
487
+ expect(result).toContain('~43% of 5h window');
488
+ });
489
+
490
+ it('should not include rate limit when option disabled', () => {
491
+ const options: TokenSummaryOptions = {
492
+ showRateLimitEstimate: false,
493
+ rateLimitPercentage: 42.5,
494
+ };
495
+ const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75), options);
496
+ expect(result).not.toContain('5h window');
497
+ });
498
+
499
+ it('should hide cache tokens when option disabled', () => {
500
+ const options: TokenSummaryOptions = {
501
+ showCacheTokens: false,
502
+ };
503
+ const result = formatTokenTotalSummary(
504
+ makeUsage({ cacheReadInputTokens: 125000 }),
505
+ makeCost(3.75),
506
+ options
507
+ );
508
+ expect(result).not.toContain('Cache:');
509
+ });
510
+ });
511
+
512
+ describe('formatRateLimitPercentage', () => {
513
+ it('should format zero percentage', () => {
514
+ expect(formatRateLimitPercentage(0)).toBe('~0% of 5h window');
515
+ });
516
+
517
+ it('should format very small percentages with 2 decimals', () => {
518
+ expect(formatRateLimitPercentage(0.05)).toBe('~0.05% of 5h window');
519
+ });
520
+
521
+ it('should format small percentages with 1 decimal', () => {
522
+ expect(formatRateLimitPercentage(0.5)).toBe('~0.5% of 5h window');
523
+ });
524
+
525
+ it('should round percentages >= 1', () => {
526
+ expect(formatRateLimitPercentage(1.5)).toBe('~2% of 5h window');
527
+ expect(formatRateLimitPercentage(42.3)).toBe('~42% of 5h window');
528
+ expect(formatRateLimitPercentage(100)).toBe('~100% of 5h window');
529
+ });
530
+
531
+ it('should handle percentages over 100%', () => {
532
+ expect(formatRateLimitPercentage(150.7)).toBe('~151% of 5h window');
533
+ });
534
+ });
535
+
536
+ describe('formatTaskTokenSummary with options', () => {
537
+ const makeUsage = (overrides: Partial<UsageData> = {}): UsageData => ({
538
+ inputTokens: 5234,
539
+ outputTokens: 1023,
540
+ cacheReadInputTokens: 0,
541
+ cacheCreationInputTokens: 0,
542
+ modelUsage: {},
543
+ ...overrides,
544
+ });
545
+
546
+ const makeCost = (total: number): CostBreakdown => ({
547
+ inputCost: 0,
548
+ outputCost: 0,
549
+ cacheReadCost: 0,
550
+ cacheCreateCost: 0,
551
+ totalCost: total,
552
+ });
553
+
554
+ const makeEntry = (usage: UsageData, cost: CostBreakdown, attempts?: UsageData[]): TaskUsageEntry => ({
555
+ taskId: '01',
556
+ usage,
557
+ cost,
558
+ attempts: attempts ?? [usage],
559
+ });
560
+
561
+ it('should include rate limit percentage in single-attempt summary', () => {
562
+ const usage = makeUsage({ cacheReadInputTokens: 18500 });
563
+ const entry = makeEntry(usage, makeCost(0.42));
564
+ const options: TokenSummaryOptions = {
565
+ showRateLimitEstimate: true,
566
+ rateLimitPercentage: 2.5,
567
+ };
568
+
569
+ const result = formatTaskTokenSummary(entry, undefined, options);
570
+ expect(result).toContain('~3% of 5h window');
571
+ });
572
+
573
+ it('should hide cache tokens in single-attempt summary when disabled', () => {
574
+ const usage = makeUsage({ cacheReadInputTokens: 18500 });
575
+ const entry = makeEntry(usage, makeCost(0.42));
576
+ const options: TokenSummaryOptions = {
577
+ showCacheTokens: false,
578
+ };
579
+
580
+ const result = formatTaskTokenSummary(entry, undefined, options);
581
+ expect(result).not.toContain('Cache:');
582
+ });
583
+
584
+ it('should only show rate limit on total line in multi-attempt summary', () => {
585
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200 });
586
+ const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400 });
587
+ const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600 });
588
+ const entry = makeEntry(totalUsage, makeCost(0.05), [attempt1, attempt2]);
589
+ const options: TokenSummaryOptions = {
590
+ showRateLimitEstimate: true,
591
+ rateLimitPercentage: 1.5,
592
+ };
593
+
594
+ const result = formatTaskTokenSummary(entry, undefined, options);
595
+ const lines = result.split('\n');
596
+
597
+ // Rate limit should only appear on the Total line
598
+ expect(lines[0]).not.toContain('5h window'); // Attempt 1
599
+ expect(lines[1]).not.toContain('5h window'); // Attempt 2
600
+ expect(lines[2]).toContain('~2% of 5h window'); // Total
601
+ });
602
+
603
+ it('should respect showCacheTokens in multi-attempt summary', () => {
604
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, cacheReadInputTokens: 5000 });
605
+ const attempt2 = makeUsage({ inputTokens: 1500, outputTokens: 300, cacheCreationInputTokens: 2000 });
606
+ const totalUsage = makeUsage({
607
+ inputTokens: 2500,
608
+ outputTokens: 500,
609
+ cacheReadInputTokens: 5000,
610
+ cacheCreationInputTokens: 2000,
611
+ });
612
+ const entry = makeEntry(totalUsage, makeCost(0.08), [attempt1, attempt2]);
613
+ const options: TokenSummaryOptions = {
614
+ showCacheTokens: false,
615
+ };
616
+
617
+ const result = formatTaskTokenSummary(entry, undefined, options);
618
+ expect(result).not.toContain('Cache:');
619
+ });
390
620
  });
391
621
  });
@@ -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
+ });