rafcode 2.3.0 → 2.4.1-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 +21 -4
- package/RAF/ahvrih-rate-forge/decisions.md +70 -0
- package/RAF/ahvrih-rate-forge/input.md +44 -0
- package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
- package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
- package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
- package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
- package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
- package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
- package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
- package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
- package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
- package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
- package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
- package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
- package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
- package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
- 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/README.md +27 -7
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +209 -6
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +140 -21
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +27 -5
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +0 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +4 -9
- 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 +3 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/pull-request.js +3 -3
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/state-derivation.d.ts +5 -0
- package/dist/core/state-derivation.d.ts.map +1 -1
- package/dist/core/state-derivation.js +14 -4
- package/dist/core/state-derivation.js.map +1 -1
- package/dist/core/worktree.d.ts +44 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +247 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +28 -11
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +28 -11
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +30 -13
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +14 -10
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +47 -4
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +176 -30
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +53 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +115 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +9 -19
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/session-parser.d.ts +44 -0
- package/dist/utils/session-parser.d.ts.map +1 -0
- package/dist/utils/session-parser.js +122 -0
- package/dist/utils/session-parser.js.map +1 -0
- package/dist/utils/terminal-symbols.d.ts +22 -3
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +52 -18
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +20 -0
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +57 -2
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +242 -7
- package/src/commands/do.ts +177 -23
- package/src/commands/plan.ts +27 -4
- package/src/core/claude-runner.ts +4 -16
- package/src/core/failure-analyzer.ts +3 -3
- package/src/core/pull-request.ts +3 -3
- package/src/core/state-derivation.ts +20 -4
- package/src/core/worktree.ts +266 -0
- package/src/prompts/amend.ts +28 -11
- package/src/prompts/config-docs.md +91 -29
- package/src/prompts/planning.ts +28 -11
- package/src/types/config.ts +46 -21
- package/src/utils/config.ts +200 -33
- package/src/utils/frontmatter.ts +140 -0
- package/src/utils/name-generator.ts +9 -19
- package/src/utils/terminal-symbols.ts +68 -16
- package/src/utils/token-tracker.ts +65 -2
- package/tests/unit/claude-runner-interactive.test.ts +8 -6
- package/tests/unit/claude-runner.test.ts +5 -66
- 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 +176 -6
- package/tests/unit/config.test.ts +268 -45
- package/tests/unit/frontmatter.test.ts +276 -0
- package/tests/unit/name-generator.test.ts +1 -1
- package/tests/unit/post-execution-picker.test.ts +6 -0
- package/tests/unit/terminal-symbols.test.ts +142 -0
- package/tests/unit/token-tracker.test.ts +304 -1
- package/tests/unit/validation.test.ts +6 -4
- package/tests/unit/worktree.test.ts +309 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { parsePlanFrontmatter } from '../../src/utils/frontmatter.js';
|
|
2
|
+
|
|
3
|
+
describe('parsePlanFrontmatter', () => {
|
|
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)', () => {
|
|
99
|
+
it('should parse effort field', () => {
|
|
100
|
+
const content = `effort: medium
|
|
101
|
+
---
|
|
102
|
+
# Task: Test Task`;
|
|
103
|
+
const result = parsePlanFrontmatter(content);
|
|
104
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
105
|
+
expect(result.frontmatter.effort).toBe('medium');
|
|
106
|
+
expect(result.warnings).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should parse model field', () => {
|
|
110
|
+
const content = `model: sonnet
|
|
111
|
+
---
|
|
112
|
+
# Task: Test Task`;
|
|
113
|
+
const result = parsePlanFrontmatter(content);
|
|
114
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
115
|
+
expect(result.frontmatter.model).toBe('sonnet');
|
|
116
|
+
expect(result.warnings).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should parse both effort and model', () => {
|
|
120
|
+
const content = `effort: high
|
|
121
|
+
model: opus
|
|
122
|
+
---
|
|
123
|
+
# Task: Test Task`;
|
|
124
|
+
const result = parsePlanFrontmatter(content);
|
|
125
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
126
|
+
expect(result.frontmatter.effort).toBe('high');
|
|
127
|
+
expect(result.frontmatter.model).toBe('opus');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should accept all effort levels', () => {
|
|
131
|
+
for (const level of ['low', 'medium', 'high']) {
|
|
132
|
+
const content = `effort: ${level}
|
|
133
|
+
---
|
|
134
|
+
# Task: Test`;
|
|
135
|
+
const result = parsePlanFrontmatter(content);
|
|
136
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
137
|
+
expect(result.frontmatter.effort).toBe(level);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should accept full model IDs', () => {
|
|
142
|
+
const content = `model: claude-opus-4-6
|
|
143
|
+
---
|
|
144
|
+
# Task: Test`;
|
|
145
|
+
const result = parsePlanFrontmatter(content);
|
|
146
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
147
|
+
expect(result.frontmatter.model).toBe('claude-opus-4-6');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should be case-insensitive for effort values', () => {
|
|
151
|
+
const content = `effort: MEDIUM
|
|
152
|
+
---
|
|
153
|
+
# Task: Test`;
|
|
154
|
+
const result = parsePlanFrontmatter(content);
|
|
155
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
156
|
+
expect(result.frontmatter.effort).toBe('medium');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle empty lines in frontmatter', () => {
|
|
160
|
+
const content = `effort: low
|
|
161
|
+
|
|
162
|
+
model: haiku
|
|
163
|
+
---
|
|
164
|
+
# Task: Test`;
|
|
165
|
+
const result = parsePlanFrontmatter(content);
|
|
166
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
167
|
+
expect(result.frontmatter.effort).toBe('low');
|
|
168
|
+
expect(result.frontmatter.model).toBe('haiku');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('no frontmatter', () => {
|
|
173
|
+
it('should return empty for content without delimiter', () => {
|
|
174
|
+
const content = `# Task: Test Task
|
|
175
|
+
|
|
176
|
+
## Objective
|
|
177
|
+
Do something`;
|
|
178
|
+
const result = parsePlanFrontmatter(content);
|
|
179
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
180
|
+
expect(result.frontmatter.effort).toBeUndefined();
|
|
181
|
+
expect(result.frontmatter.model).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return empty for empty content', () => {
|
|
185
|
+
const result = parsePlanFrontmatter('');
|
|
186
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return empty when markdown heading appears before delimiter', () => {
|
|
190
|
+
const content = `# Task: Test Task
|
|
191
|
+
---
|
|
192
|
+
More content`;
|
|
193
|
+
const result = parsePlanFrontmatter(content);
|
|
194
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
195
|
+
expect(result.warnings).toContain('Frontmatter section contains markdown content before closing delimiter');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('warnings', () => {
|
|
200
|
+
it('should warn on unknown frontmatter keys', () => {
|
|
201
|
+
const content = `effort: medium
|
|
202
|
+
unknownKey: value
|
|
203
|
+
---
|
|
204
|
+
# Task: Test`;
|
|
205
|
+
const result = parsePlanFrontmatter(content);
|
|
206
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
207
|
+
expect(result.warnings).toContain('Unknown frontmatter key: "unknownkey"');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should warn on invalid effort value', () => {
|
|
211
|
+
const content = `effort: invalid
|
|
212
|
+
---
|
|
213
|
+
# Task: Test`;
|
|
214
|
+
const result = parsePlanFrontmatter(content);
|
|
215
|
+
expect(result.hasFrontmatter).toBe(false); // No valid frontmatter extracted
|
|
216
|
+
expect(result.warnings.some(w => w.includes('Invalid effort value'))).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should warn on invalid model value', () => {
|
|
220
|
+
const content = `model: gpt-4
|
|
221
|
+
---
|
|
222
|
+
# Task: Test`;
|
|
223
|
+
const result = parsePlanFrontmatter(content);
|
|
224
|
+
expect(result.hasFrontmatter).toBe(false);
|
|
225
|
+
expect(result.warnings.some(w => w.includes('Invalid model value'))).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should collect multiple warnings', () => {
|
|
229
|
+
const content = `effort: invalid
|
|
230
|
+
model: gpt-4
|
|
231
|
+
unknownKey: value
|
|
232
|
+
---
|
|
233
|
+
# Task: Test`;
|
|
234
|
+
const result = parsePlanFrontmatter(content);
|
|
235
|
+
expect(result.warnings.length).toBeGreaterThanOrEqual(3);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('edge cases', () => {
|
|
240
|
+
it('should handle delimiter with content after it', () => {
|
|
241
|
+
const content = `effort: medium
|
|
242
|
+
---
|
|
243
|
+
# Task: Test
|
|
244
|
+
|
|
245
|
+
## Objective
|
|
246
|
+
Do something
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
More content with another delimiter`;
|
|
251
|
+
const result = parsePlanFrontmatter(content);
|
|
252
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
253
|
+
expect(result.frontmatter.effort).toBe('medium');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle whitespace around values', () => {
|
|
257
|
+
const content = `effort: high
|
|
258
|
+
model: sonnet
|
|
259
|
+
---
|
|
260
|
+
# Task: Test`;
|
|
261
|
+
const result = parsePlanFrontmatter(content);
|
|
262
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
263
|
+
expect(result.frontmatter.effort).toBe('high');
|
|
264
|
+
expect(result.frontmatter.model).toBe('sonnet');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle tabs in whitespace', () => {
|
|
268
|
+
const content = `effort:\thigh
|
|
269
|
+
---
|
|
270
|
+
# Task: Test`;
|
|
271
|
+
const result = parsePlanFrontmatter(content);
|
|
272
|
+
expect(result.hasFrontmatter).toBe(true);
|
|
273
|
+
expect(result.frontmatter.effort).toBe('high');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -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 () => {
|
|
@@ -45,6 +45,8 @@ jest.unstable_mockModule('../../src/core/pull-request.js', () => ({
|
|
|
45
45
|
// Mock worktree module
|
|
46
46
|
const mockMergeWorktreeBranch = jest.fn();
|
|
47
47
|
const mockRemoveWorktree = jest.fn();
|
|
48
|
+
const mockPullMainBranch = jest.fn();
|
|
49
|
+
const mockPushMainBranch = jest.fn();
|
|
48
50
|
jest.unstable_mockModule('../../src/core/worktree.js', () => ({
|
|
49
51
|
getRepoRoot: jest.fn(),
|
|
50
52
|
getRepoBasename: jest.fn(),
|
|
@@ -60,6 +62,10 @@ jest.unstable_mockModule('../../src/core/worktree.js', () => ({
|
|
|
60
62
|
branchExists: jest.fn(),
|
|
61
63
|
getWorktreeProjectPath: jest.fn(),
|
|
62
64
|
resolveWorktreeProjectByIdentifier: jest.fn(),
|
|
65
|
+
pullMainBranch: mockPullMainBranch,
|
|
66
|
+
pushMainBranch: mockPushMainBranch,
|
|
67
|
+
detectMainBranch: jest.fn(),
|
|
68
|
+
rebaseOntoMain: jest.fn(),
|
|
63
69
|
}));
|
|
64
70
|
|
|
65
71
|
// Import after mocking
|
|
@@ -8,6 +8,8 @@ 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';
|
|
@@ -475,5 +477,145 @@ describe('Terminal Symbols', () => {
|
|
|
475
477
|
expect(lines[0]).toContain('──');
|
|
476
478
|
expect(lines[lines.length - 1]).toContain('──');
|
|
477
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
|
+
});
|
|
478
620
|
});
|
|
479
621
|
});
|