rafcode 2.4.1-0 → 2.5.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.md +4 -4
- 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/RAF/ahwvrz-legacy-sunset/decisions.md +10 -0
- package/RAF/ahwvrz-legacy-sunset/input.md +10 -0
- package/RAF/ahwvrz-legacy-sunset/outcomes/01-remove-migrate-command.md +30 -0
- package/RAF/ahwvrz-legacy-sunset/outcomes/02-fix-resume-worktree-resolution.md +62 -0
- package/RAF/ahwvrz-legacy-sunset/plans/01-remove-migrate-command.md +65 -0
- package/RAF/ahwvrz-legacy-sunset/plans/02-fix-resume-worktree-resolution.md +72 -0
- package/README.md +0 -17
- package/dist/commands/do.js +13 -15
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +98 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +8 -0
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +72 -0
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.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 +4 -1
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +4 -28
- 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/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/do.ts +14 -15
- package/src/commands/plan.ts +112 -1
- package/src/core/claude-runner.ts +81 -0
- package/src/index.ts +0 -2
- 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 +4 -1
- package/src/types/config.ts +4 -57
- package/src/utils/config.ts +2 -112
- 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/config-command.test.ts +4 -13
- package/tests/unit/config.test.ts +6 -148
- package/tests/unit/plan-resume-worktree-resolution.test.ts +153 -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/src/commands/migrate.ts +0 -269
- package/tests/unit/migrate-command.test.ts +0 -197
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { UsageData
|
|
2
|
-
import { resolveModelPricingCategory, getPricingConfig, getRateLimitWindowConfig } from './config.js';
|
|
1
|
+
import { UsageData } from '../types/config.js';
|
|
3
2
|
|
|
4
3
|
/** Cost breakdown for a single task or accumulated total. */
|
|
5
4
|
export interface CostBreakdown {
|
|
6
|
-
inputCost: number;
|
|
7
|
-
outputCost: number;
|
|
8
|
-
cacheReadCost: number;
|
|
9
|
-
cacheCreateCost: number;
|
|
10
5
|
totalCost: number;
|
|
11
6
|
}
|
|
12
7
|
|
|
@@ -25,23 +20,11 @@ export interface TaskUsageEntry {
|
|
|
25
20
|
* Sum multiple CostBreakdown objects into a single total.
|
|
26
21
|
*/
|
|
27
22
|
export function sumCostBreakdowns(costs: CostBreakdown[]): CostBreakdown {
|
|
28
|
-
|
|
29
|
-
inputCost: 0,
|
|
30
|
-
outputCost: 0,
|
|
31
|
-
cacheReadCost: 0,
|
|
32
|
-
cacheCreateCost: 0,
|
|
33
|
-
totalCost: 0,
|
|
34
|
-
};
|
|
35
|
-
|
|
23
|
+
let totalCost = 0;
|
|
36
24
|
for (const cost of costs) {
|
|
37
|
-
|
|
38
|
-
result.outputCost += cost.outputCost;
|
|
39
|
-
result.cacheReadCost += cost.cacheReadCost;
|
|
40
|
-
result.cacheCreateCost += cost.cacheCreateCost;
|
|
41
|
-
result.totalCost += cost.totalCost;
|
|
25
|
+
totalCost += cost.totalCost;
|
|
42
26
|
}
|
|
43
|
-
|
|
44
|
-
return result;
|
|
27
|
+
return { totalCost };
|
|
45
28
|
}
|
|
46
29
|
|
|
47
30
|
/**
|
|
@@ -55,6 +38,7 @@ export function accumulateUsage(attempts: UsageData[]): UsageData {
|
|
|
55
38
|
cacheReadInputTokens: 0,
|
|
56
39
|
cacheCreationInputTokens: 0,
|
|
57
40
|
modelUsage: {},
|
|
41
|
+
totalCostUsd: 0,
|
|
58
42
|
};
|
|
59
43
|
|
|
60
44
|
for (const attempt of attempts) {
|
|
@@ -71,39 +55,39 @@ export function accumulateUsage(attempts: UsageData[]): UsageData {
|
|
|
71
55
|
existing.outputTokens += modelUsage.outputTokens;
|
|
72
56
|
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
73
57
|
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
58
|
+
existing.costUsd += modelUsage.costUsd;
|
|
74
59
|
} else {
|
|
75
60
|
result.modelUsage[modelId] = { ...modelUsage };
|
|
76
61
|
}
|
|
77
62
|
}
|
|
63
|
+
|
|
64
|
+
// Sum totalCostUsd across attempts
|
|
65
|
+
result.totalCostUsd += attempt.totalCostUsd;
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
return result;
|
|
81
69
|
}
|
|
82
70
|
|
|
83
71
|
/**
|
|
84
|
-
* Accumulates token usage across multiple task executions
|
|
85
|
-
* using configurable per-model pricing.
|
|
72
|
+
* Accumulates token usage across multiple task executions using Claude-provided cost data.
|
|
86
73
|
*/
|
|
87
74
|
export class TokenTracker {
|
|
88
75
|
private entries: TaskUsageEntry[] = [];
|
|
89
|
-
private pricingConfig: PricingConfig;
|
|
90
76
|
|
|
91
|
-
constructor(
|
|
92
|
-
|
|
77
|
+
constructor() {
|
|
78
|
+
// No pricing config needed - costs come from Claude
|
|
93
79
|
}
|
|
94
80
|
|
|
95
81
|
/**
|
|
96
82
|
* Record usage data from a completed task.
|
|
97
83
|
* Accepts an array of UsageData (one per attempt) and accumulates them.
|
|
98
|
-
*
|
|
99
|
-
* have modelUsage and others only have aggregate fields.
|
|
84
|
+
* Costs are summed from Claude-provided totalCostUsd values.
|
|
100
85
|
*/
|
|
101
86
|
addTask(taskId: string, attempts: UsageData[]): TaskUsageEntry {
|
|
102
87
|
const usage = accumulateUsage(attempts);
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const cost = sumCostBreakdowns(perAttemptCosts);
|
|
88
|
+
// Sum costs from Claude-provided totalCostUsd
|
|
89
|
+
const totalCost = attempts.reduce((sum, attempt) => sum + attempt.totalCostUsd, 0);
|
|
90
|
+
const cost: CostBreakdown = { totalCost };
|
|
107
91
|
const entry: TaskUsageEntry = { taskId, usage, cost, attempts };
|
|
108
92
|
this.entries.push(entry);
|
|
109
93
|
return entry;
|
|
@@ -126,12 +110,9 @@ export class TokenTracker {
|
|
|
126
110
|
cacheReadInputTokens: 0,
|
|
127
111
|
cacheCreationInputTokens: 0,
|
|
128
112
|
modelUsage: {},
|
|
113
|
+
totalCostUsd: 0,
|
|
129
114
|
};
|
|
130
115
|
const totalCost: CostBreakdown = {
|
|
131
|
-
inputCost: 0,
|
|
132
|
-
outputCost: 0,
|
|
133
|
-
cacheReadCost: 0,
|
|
134
|
-
cacheCreateCost: 0,
|
|
135
116
|
totalCost: 0,
|
|
136
117
|
};
|
|
137
118
|
|
|
@@ -140,6 +121,7 @@ export class TokenTracker {
|
|
|
140
121
|
totalUsage.outputTokens += entry.usage.outputTokens;
|
|
141
122
|
totalUsage.cacheReadInputTokens += entry.usage.cacheReadInputTokens;
|
|
142
123
|
totalUsage.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
|
|
124
|
+
totalUsage.totalCostUsd += entry.usage.totalCostUsd;
|
|
143
125
|
|
|
144
126
|
// Merge per-model usage
|
|
145
127
|
for (const [modelId, modelUsage] of Object.entries(entry.usage.modelUsage)) {
|
|
@@ -149,92 +131,16 @@ export class TokenTracker {
|
|
|
149
131
|
existing.outputTokens += modelUsage.outputTokens;
|
|
150
132
|
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
151
133
|
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
134
|
+
existing.costUsd += modelUsage.costUsd;
|
|
152
135
|
} else {
|
|
153
136
|
totalUsage.modelUsage[modelId] = { ...modelUsage };
|
|
154
137
|
}
|
|
155
138
|
}
|
|
156
139
|
|
|
157
|
-
totalCost.inputCost += entry.cost.inputCost;
|
|
158
|
-
totalCost.outputCost += entry.cost.outputCost;
|
|
159
|
-
totalCost.cacheReadCost += entry.cost.cacheReadCost;
|
|
160
|
-
totalCost.cacheCreateCost += entry.cost.cacheCreateCost;
|
|
161
140
|
totalCost.totalCost += entry.cost.totalCost;
|
|
162
141
|
}
|
|
163
142
|
|
|
164
143
|
return { usage: totalUsage, cost: totalCost };
|
|
165
144
|
}
|
|
166
145
|
|
|
167
|
-
/**
|
|
168
|
-
* Calculate cost for a given UsageData using per-model pricing.
|
|
169
|
-
* Uses per-model breakdown when available, falls back to aggregate with sonnet pricing.
|
|
170
|
-
*/
|
|
171
|
-
calculateCost(usage: UsageData): CostBreakdown {
|
|
172
|
-
const result: CostBreakdown = {
|
|
173
|
-
inputCost: 0,
|
|
174
|
-
outputCost: 0,
|
|
175
|
-
cacheReadCost: 0,
|
|
176
|
-
cacheCreateCost: 0,
|
|
177
|
-
totalCost: 0,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const modelEntries = Object.entries(usage.modelUsage);
|
|
181
|
-
|
|
182
|
-
if (modelEntries.length > 0) {
|
|
183
|
-
// Use per-model breakdown for accurate pricing
|
|
184
|
-
for (const [modelId, modelUsage] of modelEntries) {
|
|
185
|
-
const category = resolveModelPricingCategory(modelId);
|
|
186
|
-
const pricing = this.pricingConfig[category ?? 'sonnet'];
|
|
187
|
-
|
|
188
|
-
result.inputCost += (modelUsage.inputTokens / 1_000_000) * pricing.inputPerMTok;
|
|
189
|
-
result.outputCost += (modelUsage.outputTokens / 1_000_000) * pricing.outputPerMTok;
|
|
190
|
-
result.cacheReadCost += (modelUsage.cacheReadInputTokens / 1_000_000) * pricing.cacheReadPerMTok;
|
|
191
|
-
result.cacheCreateCost += (modelUsage.cacheCreationInputTokens / 1_000_000) * pricing.cacheCreatePerMTok;
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
// Fallback: use aggregate totals with sonnet pricing
|
|
195
|
-
const pricing = this.pricingConfig.sonnet;
|
|
196
|
-
result.inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPerMTok;
|
|
197
|
-
result.outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPerMTok;
|
|
198
|
-
result.cacheReadCost = (usage.cacheReadInputTokens / 1_000_000) * pricing.cacheReadPerMTok;
|
|
199
|
-
result.cacheCreateCost = (usage.cacheCreationInputTokens / 1_000_000) * pricing.cacheCreatePerMTok;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
result.totalCost = result.inputCost + result.outputCost + result.cacheReadCost + result.cacheCreateCost;
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Calculate the 5h rate limit window percentage for a given cost.
|
|
208
|
-
* Converts cost to Sonnet-equivalent tokens using the configured Sonnet pricing,
|
|
209
|
-
* then divides by the configured cap.
|
|
210
|
-
*
|
|
211
|
-
* @param totalCost - The total cost in dollars
|
|
212
|
-
* @param sonnetTokenCap - Optional override for the Sonnet-equivalent token cap (defaults to config value)
|
|
213
|
-
* @returns The percentage of the 5h window consumed (0-100+)
|
|
214
|
-
*/
|
|
215
|
-
calculateRateLimitPercentage(totalCost: number, sonnetTokenCap?: number): number {
|
|
216
|
-
if (totalCost === 0) return 0;
|
|
217
|
-
|
|
218
|
-
// Get the configured cap or use the provided override
|
|
219
|
-
const cap = sonnetTokenCap ?? getRateLimitWindowConfig().sonnetTokenCap;
|
|
220
|
-
|
|
221
|
-
// Calculate the average Sonnet cost per token
|
|
222
|
-
// Using the average of input and output pricing (simplified approach)
|
|
223
|
-
const sonnetPricing = this.pricingConfig.sonnet;
|
|
224
|
-
const avgSonnetCostPerToken = (sonnetPricing.inputPerMTok + sonnetPricing.outputPerMTok) / 2 / 1_000_000;
|
|
225
|
-
|
|
226
|
-
// Convert cost to Sonnet-equivalent tokens
|
|
227
|
-
const sonnetEquivalentTokens = totalCost / avgSonnetCostPerToken;
|
|
228
|
-
|
|
229
|
-
// Calculate percentage
|
|
230
|
-
return (sonnetEquivalentTokens / cap) * 100;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Get the cumulative 5h window percentage across all recorded tasks.
|
|
235
|
-
*/
|
|
236
|
-
getCumulativeRateLimitPercentage(sonnetTokenCap?: number): number {
|
|
237
|
-
const totals = this.getTotals();
|
|
238
|
-
return this.calculateRateLimitPercentage(totals.cost.totalCost, sonnetTokenCap);
|
|
239
|
-
}
|
|
240
146
|
}
|
|
@@ -277,18 +277,10 @@ describe('Config Command', () => {
|
|
|
277
277
|
|
|
278
278
|
it('should handle nested keys', () => {
|
|
279
279
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
280
|
-
fs.writeFileSync(configPath, JSON.stringify({ display: {
|
|
280
|
+
fs.writeFileSync(configPath, JSON.stringify({ display: { showCacheTokens: false } }, null, 2));
|
|
281
281
|
|
|
282
282
|
const config = resolveConfig(configPath);
|
|
283
|
-
expect(config.display.
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('should handle deeply nested pricing keys', () => {
|
|
287
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
288
|
-
fs.writeFileSync(configPath, JSON.stringify({ pricing: { opus: { inputPerMTok: 20 } } }, null, 2));
|
|
289
|
-
|
|
290
|
-
const config = resolveConfig(configPath);
|
|
291
|
-
expect(config.pricing.opus.inputPerMTok).toBe(20);
|
|
283
|
+
expect(config.display.showCacheTokens).toBe(false);
|
|
292
284
|
});
|
|
293
285
|
});
|
|
294
286
|
|
|
@@ -401,12 +393,11 @@ describe('Config Command', () => {
|
|
|
401
393
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
402
394
|
|
|
403
395
|
// Set a nested value
|
|
404
|
-
const userConfig = { display: {
|
|
396
|
+
const userConfig = { display: { showCacheTokens: false } };
|
|
405
397
|
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
406
398
|
|
|
407
399
|
const config = resolveConfig(configPath);
|
|
408
|
-
expect(config.display.
|
|
409
|
-
expect(config.display.showCacheTokens).toBe(DEFAULT_CONFIG.display.showCacheTokens);
|
|
400
|
+
expect(config.display.showCacheTokens).toBe(false);
|
|
410
401
|
});
|
|
411
402
|
});
|
|
412
403
|
});
|
|
@@ -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', () => {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for worktree resolution in the --resume command.
|
|
5
|
+
*
|
|
6
|
+
* When `raf plan --resume <identifier>` is run, it should:
|
|
7
|
+
* 1. Search worktrees first
|
|
8
|
+
* 2. Fall back to main repo if not found in worktree
|
|
9
|
+
* 3. Auto-detect worktree mode without requiring --worktree flag
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
describe('Plan Resume - Worktree Resolution Logic', () => {
|
|
13
|
+
describe('resolution flow logic', () => {
|
|
14
|
+
it('should prioritize worktree over main repo when both exist', () => {
|
|
15
|
+
// This test verifies the conceptual resolution order used in runResumeCommand:
|
|
16
|
+
// 1. Try worktree resolution via resolveWorktreeProjectByIdentifier()
|
|
17
|
+
// 2. If found and valid → use worktree path and worktree root as CWD
|
|
18
|
+
// 3. If not found → fall back to main repo resolution
|
|
19
|
+
|
|
20
|
+
// Simulating the flow:
|
|
21
|
+
const worktreeFound = true;
|
|
22
|
+
const worktreeValid = true;
|
|
23
|
+
|
|
24
|
+
let useWorktree = false;
|
|
25
|
+
let useMainRepo = false;
|
|
26
|
+
|
|
27
|
+
// Step 1: Try worktree
|
|
28
|
+
if (worktreeFound && worktreeValid) {
|
|
29
|
+
useWorktree = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Step 2: Fall back to main repo if worktree not used
|
|
33
|
+
if (!useWorktree) {
|
|
34
|
+
useMainRepo = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(useWorktree).toBe(true);
|
|
38
|
+
expect(useMainRepo).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should fall back to main repo when worktree is invalid', () => {
|
|
42
|
+
const worktreeFound = true;
|
|
43
|
+
const worktreeValid = false;
|
|
44
|
+
|
|
45
|
+
let useWorktree = false;
|
|
46
|
+
let useMainRepo = false;
|
|
47
|
+
|
|
48
|
+
if (worktreeFound && worktreeValid) {
|
|
49
|
+
useWorktree = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!useWorktree) {
|
|
53
|
+
useMainRepo = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expect(useWorktree).toBe(false);
|
|
57
|
+
expect(useMainRepo).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should fall back to main repo when worktree not found', () => {
|
|
61
|
+
const worktreeFound = false;
|
|
62
|
+
|
|
63
|
+
let useWorktree = false;
|
|
64
|
+
let useMainRepo = false;
|
|
65
|
+
|
|
66
|
+
if (worktreeFound) {
|
|
67
|
+
useWorktree = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!useWorktree) {
|
|
71
|
+
useMainRepo = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(useWorktree).toBe(false);
|
|
75
|
+
expect(useMainRepo).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('resumeCwd determination', () => {
|
|
80
|
+
it('should set resumeCwd to worktree root when project found in valid worktree', () => {
|
|
81
|
+
// Simulated values
|
|
82
|
+
const worktreeRoot = '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset';
|
|
83
|
+
const projectPath = '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset/RAF/ahwvrz-legacy-sunset';
|
|
84
|
+
|
|
85
|
+
const worktreeValid = true;
|
|
86
|
+
let resumeCwd: string | undefined;
|
|
87
|
+
|
|
88
|
+
if (worktreeValid) {
|
|
89
|
+
resumeCwd = worktreeRoot;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(resumeCwd).toBe(worktreeRoot);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should set resumeCwd to project path when using main repo', () => {
|
|
96
|
+
const mainRepoProjectPath = '/Users/user/myapp/RAF/ahwvrz-legacy-sunset';
|
|
97
|
+
|
|
98
|
+
const worktreeFound = false;
|
|
99
|
+
let resumeCwd: string | undefined;
|
|
100
|
+
|
|
101
|
+
if (!worktreeFound) {
|
|
102
|
+
resumeCwd = mainRepoProjectPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(resumeCwd).toBe(mainRepoProjectPath);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('variable initialization', () => {
|
|
110
|
+
it('should handle undefined variables correctly when worktree is found', () => {
|
|
111
|
+
let projectPath: string | undefined;
|
|
112
|
+
let resumeCwd: string | undefined;
|
|
113
|
+
let folderName: string | undefined;
|
|
114
|
+
|
|
115
|
+
// Simulate worktree resolution
|
|
116
|
+
const worktreeResolution = {
|
|
117
|
+
folder: 'ahwvrz-legacy-sunset',
|
|
118
|
+
worktreeRoot: '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (worktreeResolution) {
|
|
122
|
+
folderName = worktreeResolution.folder;
|
|
123
|
+
projectPath = `/path/to/${folderName}`;
|
|
124
|
+
resumeCwd = worktreeResolution.worktreeRoot;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
expect(folderName).toBeDefined();
|
|
128
|
+
expect(projectPath).toBeDefined();
|
|
129
|
+
expect(resumeCwd).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should handle undefined variables correctly when falling back to main repo', () => {
|
|
133
|
+
let projectPath: string | undefined;
|
|
134
|
+
let resumeCwd: string | undefined;
|
|
135
|
+
let folderName: string | undefined;
|
|
136
|
+
|
|
137
|
+
// Worktree resolution returns null
|
|
138
|
+
const worktreeResolution = null;
|
|
139
|
+
|
|
140
|
+
// Skip worktree assignment since it's null
|
|
141
|
+
if (!projectPath && !worktreeResolution) {
|
|
142
|
+
// Main repo resolution
|
|
143
|
+
projectPath = '/Users/user/myapp/RAF/ahwvrz-legacy-sunset';
|
|
144
|
+
folderName = 'ahwvrz-legacy-sunset';
|
|
145
|
+
resumeCwd = projectPath;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
expect(folderName).toBeDefined();
|
|
149
|
+
expect(projectPath).toBeDefined();
|
|
150
|
+
expect(resumeCwd).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|