rafcode 2.4.0 → 2.5.0-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 +3 -1
- package/CLAUDE.md +7 -5
- package/RAF/ahwidh-quick-fix-gremlin/decisions.md +37 -0
- package/RAF/ahwidh-quick-fix-gremlin/input.md +35 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/01-fix-name-generation-prompt.md +33 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/02-fix-amend-commit-scope.md +43 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/03-fix-diverged-main-branch-sync.md +32 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/04-wire-rate-limit-to-do-command.md +61 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/05-add-config-get-set-flags.md +125 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/06-sync-worktree-branch-before-execution.md +96 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/07-update-frontmatter-format.md +107 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/08-remove-plan-token-report.md +76 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/01-fix-name-generation-prompt.md +52 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/02-fix-amend-commit-scope.md +48 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/03-fix-diverged-main-branch-sync.md +49 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/04-wire-rate-limit-to-do-command.md +78 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/05-add-config-get-set-flags.md +101 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/06-sync-worktree-branch-before-execution.md +92 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/07-update-frontmatter-format.md +105 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/08-remove-plan-token-report.md +50 -0
- package/RAF/ahwqwq-model-whisperer/decisions.md +22 -0
- package/RAF/ahwqwq-model-whisperer/input.md +5 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/01-show-model-on-task-line.md +49 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/02-use-claude-cost-estimation.md +107 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/03-add-plan-resume-flag.md +87 -0
- package/RAF/ahwqwq-model-whisperer/plans/01-show-model-on-task-line.md +45 -0
- package/RAF/ahwqwq-model-whisperer/plans/02-use-claude-cost-estimation.md +115 -0
- package/RAF/ahwqwq-model-whisperer/plans/03-add-plan-resume-flag.md +70 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +209 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +37 -8
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +92 -54
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +8 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +73 -5
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/worktree.d.ts +12 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +33 -1
- package/dist/core/worktree.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +2 -0
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +2 -0
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +3 -1
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +3 -1
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +4 -24
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +0 -24
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +1 -26
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +2 -98
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +13 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +40 -10
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +7 -16
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +7 -16
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +16 -42
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +4 -30
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +17 -98
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +242 -0
- package/src/commands/do.ts +39 -7
- package/src/commands/plan.ts +101 -58
- package/src/core/claude-runner.ts +82 -12
- package/src/core/worktree.ts +37 -1
- package/src/parsers/stream-renderer.ts +4 -0
- package/src/prompts/amend.ts +3 -1
- package/src/prompts/config-docs.md +1 -72
- package/src/prompts/planning.ts +3 -1
- package/src/types/config.ts +4 -52
- package/src/utils/config.ts +2 -112
- package/src/utils/frontmatter.ts +41 -11
- package/src/utils/name-generator.ts +7 -16
- package/src/utils/terminal-symbols.ts +16 -46
- package/src/utils/token-tracker.ts +19 -113
- package/tests/unit/claude-runner.test.ts +1 -0
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +6 -14
- package/tests/unit/commit-planning-artifacts.test.ts +4 -12
- package/tests/unit/config-command.test.ts +161 -0
- package/tests/unit/config.test.ts +6 -148
- package/tests/unit/frontmatter.test.ts +95 -1
- package/tests/unit/name-generator.test.ts +1 -1
- package/tests/unit/post-execution-picker.test.ts +1 -0
- package/tests/unit/stream-renderer.test.ts +82 -0
- package/tests/unit/terminal-symbols.test.ts +86 -124
- package/tests/unit/token-tracker.test.ts +159 -679
- package/tests/unit/worktree.test.ts +68 -1
- package/src/utils/session-parser.ts +0 -161
- package/tests/unit/session-parser.test.ts +0 -301
|
@@ -25,9 +25,6 @@ import {
|
|
|
25
25
|
saveConfig,
|
|
26
26
|
renderCommitMessage,
|
|
27
27
|
isValidModelName,
|
|
28
|
-
resolveModelPricingCategory,
|
|
29
|
-
getPricing,
|
|
30
|
-
getPricingConfig,
|
|
31
28
|
} from '../../src/utils/config.js';
|
|
32
29
|
import { DEFAULT_CONFIG } from '../../src/types/config.js';
|
|
33
30
|
|
|
@@ -538,97 +535,6 @@ describe('Config', () => {
|
|
|
538
535
|
});
|
|
539
536
|
});
|
|
540
537
|
|
|
541
|
-
describe('validateConfig - pricing', () => {
|
|
542
|
-
it('should accept valid pricing config', () => {
|
|
543
|
-
expect(() => validateConfig({
|
|
544
|
-
pricing: {
|
|
545
|
-
opus: { inputPerMTok: 15, outputPerMTok: 75 },
|
|
546
|
-
},
|
|
547
|
-
})).not.toThrow();
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
it('should accept partial pricing override', () => {
|
|
551
|
-
expect(() => validateConfig({
|
|
552
|
-
pricing: {
|
|
553
|
-
haiku: { outputPerMTok: 4 },
|
|
554
|
-
},
|
|
555
|
-
})).not.toThrow();
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it('should reject non-object pricing', () => {
|
|
559
|
-
expect(() => validateConfig({ pricing: 'expensive' })).toThrow('pricing must be an object');
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it('should reject unknown pricing categories', () => {
|
|
563
|
-
expect(() => validateConfig({ pricing: { gpt4: { inputPerMTok: 10 } } })).toThrow('Unknown config key: pricing.gpt4');
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it('should reject non-object category value', () => {
|
|
567
|
-
expect(() => validateConfig({ pricing: { opus: 'expensive' } })).toThrow('pricing.opus must be an object');
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('should reject unknown pricing fields', () => {
|
|
571
|
-
expect(() => validateConfig({ pricing: { opus: { unknownField: 5 } } })).toThrow('Unknown config key: pricing.opus.unknownField');
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it('should reject negative pricing values', () => {
|
|
575
|
-
expect(() => validateConfig({ pricing: { opus: { inputPerMTok: -1 } } })).toThrow('pricing.opus.inputPerMTok must be a non-negative number');
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('should reject non-number pricing values', () => {
|
|
579
|
-
expect(() => validateConfig({ pricing: { opus: { inputPerMTok: 'fifteen' } } })).toThrow('pricing.opus.inputPerMTok must be a non-negative number');
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it('should accept zero pricing values', () => {
|
|
583
|
-
expect(() => validateConfig({ pricing: { haiku: { inputPerMTok: 0 } } })).not.toThrow();
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('should reject Infinity pricing values', () => {
|
|
587
|
-
expect(() => validateConfig({ pricing: { opus: { inputPerMTok: Infinity } } })).toThrow('must be a non-negative number');
|
|
588
|
-
});
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
describe('resolveModelPricingCategory', () => {
|
|
592
|
-
it('should map short aliases directly', () => {
|
|
593
|
-
expect(resolveModelPricingCategory('opus')).toBe('opus');
|
|
594
|
-
expect(resolveModelPricingCategory('sonnet')).toBe('sonnet');
|
|
595
|
-
expect(resolveModelPricingCategory('haiku')).toBe('haiku');
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('should extract family from full model IDs', () => {
|
|
599
|
-
expect(resolveModelPricingCategory('claude-opus-4-6')).toBe('opus');
|
|
600
|
-
expect(resolveModelPricingCategory('claude-sonnet-4-5-20250929')).toBe('sonnet');
|
|
601
|
-
expect(resolveModelPricingCategory('claude-haiku-4-5-20251001')).toBe('haiku');
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it('should return null for unknown model families', () => {
|
|
605
|
-
expect(resolveModelPricingCategory('claude-unknown-3-0')).toBeNull();
|
|
606
|
-
expect(resolveModelPricingCategory('gpt-4')).toBeNull();
|
|
607
|
-
expect(resolveModelPricingCategory('')).toBeNull();
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
describe('resolveConfig - pricing', () => {
|
|
612
|
-
it('should include default pricing when no config file', () => {
|
|
613
|
-
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
614
|
-
expect(config.pricing.opus.inputPerMTok).toBe(15);
|
|
615
|
-
expect(config.pricing.sonnet.inputPerMTok).toBe(3);
|
|
616
|
-
expect(config.pricing.haiku.inputPerMTok).toBe(1);
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('should deep-merge partial pricing override', () => {
|
|
620
|
-
const configPath = path.join(tempDir, 'pricing.json');
|
|
621
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
622
|
-
pricing: { opus: { inputPerMTok: 10 } },
|
|
623
|
-
}));
|
|
624
|
-
|
|
625
|
-
const config = resolveConfig(configPath);
|
|
626
|
-
expect(config.pricing.opus.inputPerMTok).toBe(10);
|
|
627
|
-
expect(config.pricing.opus.outputPerMTok).toBe(75); // default preserved
|
|
628
|
-
expect(config.pricing.sonnet.inputPerMTok).toBe(3); // default preserved
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
538
|
describe('getModelShortName', () => {
|
|
633
539
|
it('should return short aliases as-is', () => {
|
|
634
540
|
expect(getModelShortName('opus')).toBe('opus');
|
|
@@ -709,7 +615,6 @@ describe('Config', () => {
|
|
|
709
615
|
it('should accept valid display config', () => {
|
|
710
616
|
expect(() => validateConfig({
|
|
711
617
|
display: {
|
|
712
|
-
showRateLimitEstimate: true,
|
|
713
618
|
showCacheTokens: false,
|
|
714
619
|
},
|
|
715
620
|
})).not.toThrow();
|
|
@@ -717,7 +622,7 @@ describe('Config', () => {
|
|
|
717
622
|
|
|
718
623
|
it('should accept partial display override', () => {
|
|
719
624
|
expect(() => validateConfig({
|
|
720
|
-
display: {
|
|
625
|
+
display: { showCacheTokens: false },
|
|
721
626
|
})).not.toThrow();
|
|
722
627
|
});
|
|
723
628
|
|
|
@@ -730,78 +635,31 @@ describe('Config', () => {
|
|
|
730
635
|
});
|
|
731
636
|
|
|
732
637
|
it('should reject non-boolean display values', () => {
|
|
733
|
-
expect(() => validateConfig({ display: {
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
describe('validateConfig - rateLimitWindow', () => {
|
|
738
|
-
it('should accept valid rateLimitWindow config', () => {
|
|
739
|
-
expect(() => validateConfig({
|
|
740
|
-
rateLimitWindow: { sonnetTokenCap: 100000 },
|
|
741
|
-
})).not.toThrow();
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
it('should reject non-object rateLimitWindow', () => {
|
|
745
|
-
expect(() => validateConfig({ rateLimitWindow: 88000 })).toThrow('rateLimitWindow must be an object');
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it('should reject unknown rateLimitWindow keys', () => {
|
|
749
|
-
expect(() => validateConfig({ rateLimitWindow: { unknownKey: 50000 } })).toThrow('Unknown config key: rateLimitWindow.unknownKey');
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
it('should reject non-positive sonnetTokenCap', () => {
|
|
753
|
-
expect(() => validateConfig({ rateLimitWindow: { sonnetTokenCap: 0 } })).toThrow('rateLimitWindow.sonnetTokenCap must be a positive number');
|
|
754
|
-
expect(() => validateConfig({ rateLimitWindow: { sonnetTokenCap: -100 } })).toThrow('rateLimitWindow.sonnetTokenCap must be a positive number');
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it('should reject non-number sonnetTokenCap', () => {
|
|
758
|
-
expect(() => validateConfig({ rateLimitWindow: { sonnetTokenCap: '88000' } })).toThrow('rateLimitWindow.sonnetTokenCap must be a positive number');
|
|
638
|
+
expect(() => validateConfig({ display: { showCacheTokens: 'yes' } })).toThrow('display.showCacheTokens must be a boolean');
|
|
759
639
|
});
|
|
760
640
|
});
|
|
761
641
|
|
|
762
|
-
describe('resolveConfig - display
|
|
642
|
+
describe('resolveConfig - display', () => {
|
|
763
643
|
it('should include default display when no config file', () => {
|
|
764
644
|
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
765
|
-
expect(config.display.showRateLimitEstimate).toBe(true);
|
|
766
645
|
expect(config.display.showCacheTokens).toBe(true);
|
|
767
646
|
});
|
|
768
647
|
|
|
769
|
-
it('should include default rateLimitWindow when no config file', () => {
|
|
770
|
-
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
771
|
-
expect(config.rateLimitWindow.sonnetTokenCap).toBe(88000);
|
|
772
|
-
});
|
|
773
|
-
|
|
774
648
|
it('should deep-merge partial display override', () => {
|
|
775
649
|
const configPath = path.join(tempDir, 'display.json');
|
|
776
650
|
fs.writeFileSync(configPath, JSON.stringify({
|
|
777
|
-
display: {
|
|
651
|
+
display: { showCacheTokens: false },
|
|
778
652
|
}));
|
|
779
653
|
|
|
780
654
|
const config = resolveConfig(configPath);
|
|
781
|
-
expect(config.display.
|
|
782
|
-
expect(config.display.showCacheTokens).toBe(true); // default preserved
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
it('should deep-merge partial rateLimitWindow override', () => {
|
|
786
|
-
const configPath = path.join(tempDir, 'rateLimit.json');
|
|
787
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
788
|
-
rateLimitWindow: { sonnetTokenCap: 100000 },
|
|
789
|
-
}));
|
|
790
|
-
|
|
791
|
-
const config = resolveConfig(configPath);
|
|
792
|
-
expect(config.rateLimitWindow.sonnetTokenCap).toBe(100000);
|
|
655
|
+
expect(config.display.showCacheTokens).toBe(false);
|
|
793
656
|
});
|
|
794
657
|
});
|
|
795
658
|
|
|
796
|
-
describe('DEFAULT_CONFIG - display
|
|
659
|
+
describe('DEFAULT_CONFIG - display', () => {
|
|
797
660
|
it('should have default display settings', () => {
|
|
798
|
-
expect(DEFAULT_CONFIG.display.showRateLimitEstimate).toBe(true);
|
|
799
661
|
expect(DEFAULT_CONFIG.display.showCacheTokens).toBe(true);
|
|
800
662
|
});
|
|
801
|
-
|
|
802
|
-
it('should have default rateLimitWindow settings', () => {
|
|
803
|
-
expect(DEFAULT_CONFIG.rateLimitWindow.sonnetTokenCap).toBe(88000);
|
|
804
|
-
});
|
|
805
663
|
});
|
|
806
664
|
|
|
807
665
|
describe('getModelTier', () => {
|
|
@@ -1,7 +1,101 @@
|
|
|
1
1
|
import { parsePlanFrontmatter } from '../../src/utils/frontmatter.js';
|
|
2
2
|
|
|
3
3
|
describe('parsePlanFrontmatter', () => {
|
|
4
|
-
describe('
|
|
4
|
+
describe('standard format (---/---)', () => {
|
|
5
|
+
it('should parse effort field', () => {
|
|
6
|
+
const content = `---
|
|
7
|
+
effort: medium
|
|
8
|
+
---
|
|
9
|
+
# Task: Test Task`;
|
|
10
|
+
const result = parsePlanFrontmatter(content);
|
|
11
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
12
|
+
expect(result.frontmatter.effort).toBe('medium');
|
|
13
|
+
expect(result.warnings).toHaveLength(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should parse model field', () => {
|
|
17
|
+
const content = `---
|
|
18
|
+
model: sonnet
|
|
19
|
+
---
|
|
20
|
+
# Task: Test Task`;
|
|
21
|
+
const result = parsePlanFrontmatter(content);
|
|
22
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
23
|
+
expect(result.frontmatter.model).toBe('sonnet');
|
|
24
|
+
expect(result.warnings).toHaveLength(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should parse both effort and model', () => {
|
|
28
|
+
const content = `---
|
|
29
|
+
effort: high
|
|
30
|
+
model: opus
|
|
31
|
+
---
|
|
32
|
+
# Task: Test Task`;
|
|
33
|
+
const result = parsePlanFrontmatter(content);
|
|
34
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
35
|
+
expect(result.frontmatter.effort).toBe('high');
|
|
36
|
+
expect(result.frontmatter.model).toBe('opus');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle empty frontmatter block', () => {
|
|
40
|
+
const content = `---
|
|
41
|
+
---
|
|
42
|
+
# Task: Test Task`;
|
|
43
|
+
const result = parsePlanFrontmatter(content);
|
|
44
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle whitespace before opening delimiter', () => {
|
|
48
|
+
const content = `
|
|
49
|
+
---
|
|
50
|
+
effort: low
|
|
51
|
+
---
|
|
52
|
+
# Task: Test Task`;
|
|
53
|
+
const result = parsePlanFrontmatter(content);
|
|
54
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
55
|
+
expect(result.frontmatter.effort).toBe('low');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle empty lines in frontmatter', () => {
|
|
59
|
+
const content = `---
|
|
60
|
+
effort: low
|
|
61
|
+
|
|
62
|
+
model: haiku
|
|
63
|
+
---
|
|
64
|
+
# Task: Test`;
|
|
65
|
+
const result = parsePlanFrontmatter(content);
|
|
66
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
67
|
+
expect(result.frontmatter.effort).toBe('low');
|
|
68
|
+
expect(result.frontmatter.model).toBe('haiku');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle opening delimiter with trailing spaces', () => {
|
|
72
|
+
const content = `---
|
|
73
|
+
effort: medium
|
|
74
|
+
---
|
|
75
|
+
# Task: Test Task`;
|
|
76
|
+
const result = parsePlanFrontmatter(content);
|
|
77
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
78
|
+
expect(result.frontmatter.effort).toBe('medium');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return empty for missing closing delimiter', () => {
|
|
82
|
+
const content = `---
|
|
83
|
+
effort: medium
|
|
84
|
+
# Task: Test Task`;
|
|
85
|
+
const result = parsePlanFrontmatter(content);
|
|
86
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return empty for opening delimiter without newline', () => {
|
|
90
|
+
const content = `---effort: medium
|
|
91
|
+
---
|
|
92
|
+
# Task: Test Task`;
|
|
93
|
+
const result = parsePlanFrontmatter(content);
|
|
94
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('legacy format (closing --- only)', () => {
|
|
5
99
|
it('should parse effort field', () => {
|
|
6
100
|
const content = `effort: medium
|
|
7
101
|
---
|
|
@@ -170,7 +170,7 @@ describe('Name Generator', () => {
|
|
|
170
170
|
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
171
171
|
// Verify the prompt contains the multi-name generation prompt
|
|
172
172
|
const promptArg = (mockSpawn.mock.calls[0][1] as string[]).at(-1);
|
|
173
|
-
expect(promptArg).toContain('
|
|
173
|
+
expect(promptArg).toContain('Output EXACTLY 5 project names');
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
it('should handle names with numbering prefixes', async () => {
|
|
@@ -267,6 +267,7 @@ describe('renderStreamEvent', () => {
|
|
|
267
267
|
outputTokens: 500,
|
|
268
268
|
cacheReadInputTokens: 200,
|
|
269
269
|
cacheCreationInputTokens: 100,
|
|
270
|
+
costUsd: 0, // costUSD is extracted from modelUsage data
|
|
270
271
|
});
|
|
271
272
|
});
|
|
272
273
|
|
|
@@ -318,6 +319,87 @@ describe('renderStreamEvent', () => {
|
|
|
318
319
|
expect(result.usageData!.modelUsage['claude-opus-4-6'].inputTokens).toBe(1500);
|
|
319
320
|
expect(result.usageData!.modelUsage['claude-haiku-4-5-20251001'].inputTokens).toBe(500);
|
|
320
321
|
});
|
|
322
|
+
|
|
323
|
+
it('should extract total_cost_usd from result event', () => {
|
|
324
|
+
const line = JSON.stringify({
|
|
325
|
+
type: 'result',
|
|
326
|
+
subtype: 'success',
|
|
327
|
+
result: 'Done',
|
|
328
|
+
usage: {
|
|
329
|
+
input_tokens: 1000,
|
|
330
|
+
output_tokens: 500,
|
|
331
|
+
},
|
|
332
|
+
total_cost_usd: 18.5,
|
|
333
|
+
});
|
|
334
|
+
const result = renderStreamEvent(line);
|
|
335
|
+
expect(result.usageData).toBeDefined();
|
|
336
|
+
expect(result.usageData!.totalCostUsd).toBe(18.5);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should default totalCostUsd to 0 when no cost provided', () => {
|
|
340
|
+
const line = JSON.stringify({
|
|
341
|
+
type: 'result',
|
|
342
|
+
subtype: 'success',
|
|
343
|
+
result: 'Done',
|
|
344
|
+
usage: {
|
|
345
|
+
input_tokens: 1000,
|
|
346
|
+
output_tokens: 500,
|
|
347
|
+
},
|
|
348
|
+
// No cost field - costUSD is in modelUsage, not at top level
|
|
349
|
+
});
|
|
350
|
+
const result = renderStreamEvent(line);
|
|
351
|
+
expect(result.usageData).toBeDefined();
|
|
352
|
+
expect(result.usageData!.totalCostUsd).toBe(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should prioritize total_cost_usd over costUSD', () => {
|
|
356
|
+
const line = JSON.stringify({
|
|
357
|
+
type: 'result',
|
|
358
|
+
subtype: 'success',
|
|
359
|
+
result: 'Done',
|
|
360
|
+
usage: {
|
|
361
|
+
input_tokens: 1000,
|
|
362
|
+
output_tokens: 500,
|
|
363
|
+
},
|
|
364
|
+
total_cost_usd: 18.5,
|
|
365
|
+
costUSD: 20.0,
|
|
366
|
+
});
|
|
367
|
+
const result = renderStreamEvent(line);
|
|
368
|
+
expect(result.usageData).toBeDefined();
|
|
369
|
+
expect(result.usageData!.totalCostUsd).toBe(18.5);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should handle missing cost fields gracefully', () => {
|
|
373
|
+
const line = JSON.stringify({
|
|
374
|
+
type: 'result',
|
|
375
|
+
subtype: 'success',
|
|
376
|
+
result: 'Done',
|
|
377
|
+
usage: {
|
|
378
|
+
input_tokens: 1000,
|
|
379
|
+
output_tokens: 500,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
const result = renderStreamEvent(line);
|
|
383
|
+
expect(result.usageData).toBeDefined();
|
|
384
|
+
// When no cost is provided, defaults to 0
|
|
385
|
+
expect(result.usageData!.totalCostUsd).toBe(0);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should extract zero cost correctly', () => {
|
|
389
|
+
const line = JSON.stringify({
|
|
390
|
+
type: 'result',
|
|
391
|
+
subtype: 'success',
|
|
392
|
+
result: 'Done',
|
|
393
|
+
usage: {
|
|
394
|
+
input_tokens: 0,
|
|
395
|
+
output_tokens: 0,
|
|
396
|
+
},
|
|
397
|
+
total_cost_usd: 0,
|
|
398
|
+
});
|
|
399
|
+
const result = renderStreamEvent(line);
|
|
400
|
+
expect(result.usageData).toBeDefined();
|
|
401
|
+
expect(result.usageData!.totalCostUsd).toBe(0);
|
|
402
|
+
});
|
|
321
403
|
});
|
|
322
404
|
|
|
323
405
|
describe('edge cases', () => {
|