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.
- package/CLAUDE.md +19 -4
- package/RAF/ahtahs-token-reaper/decisions.md +37 -0
- package/RAF/ahtahs-token-reaper/input.md +20 -0
- package/RAF/ahtahs-token-reaper/outcomes/01-extend-token-tracker-data-model.md +42 -0
- package/RAF/ahtahs-token-reaper/outcomes/02-accumulate-usage-in-retry-loop.md +31 -0
- package/RAF/ahtahs-token-reaper/outcomes/03-per-attempt-display-formatting.md +60 -0
- package/RAF/ahtahs-token-reaper/outcomes/04-add-model-name-to-claude-call-logs.md +57 -0
- package/RAF/ahtahs-token-reaper/outcomes/05-handle-invalid-config-in-raf-config.md +46 -0
- package/RAF/ahtahs-token-reaper/outcomes/06-fix-verbose-toggle-timer-display.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/01-extend-token-tracker-data-model.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/02-accumulate-usage-in-retry-loop.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/03-per-attempt-display-formatting.md +43 -0
- package/RAF/ahtahs-token-reaper/plans/04-add-model-name-to-claude-call-logs.md +38 -0
- package/RAF/ahtahs-token-reaper/plans/05-handle-invalid-config-in-raf-config.md +36 -0
- package/RAF/ahtahs-token-reaper/plans/06-fix-verbose-toggle-timer-display.md +40 -0
- package/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/README.md +27 -7
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +24 -7
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +122 -27
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +79 -3
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +6 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +9 -10
- 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.d.ts.map +1 -1
- package/dist/core/pull-request.js +5 -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 +32 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +215 -0
- package/dist/core/worktree.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +26 -11
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +26 -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 +53 -4
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +197 -30
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +43 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +85 -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 +2 -3
- 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 +28 -5
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +77 -18
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +31 -1
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +94 -4
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +26 -7
- package/src/commands/do.ts +157 -29
- package/src/commands/plan.ts +89 -2
- package/src/core/claude-runner.ts +16 -17
- package/src/core/failure-analyzer.ts +3 -3
- package/src/core/pull-request.ts +5 -3
- package/src/core/state-derivation.ts +20 -4
- package/src/core/worktree.ts +230 -0
- package/src/prompts/amend.ts +26 -11
- package/src/prompts/config-docs.md +91 -29
- package/src/prompts/planning.ts +26 -11
- package/src/types/config.ts +46 -21
- package/src/utils/config.ts +222 -33
- package/src/utils/frontmatter.ts +110 -0
- package/src/utils/name-generator.ts +2 -3
- package/src/utils/session-parser.ts +161 -0
- package/src/utils/terminal-symbols.ts +105 -18
- package/src/utils/token-tracker.ts +109 -4
- package/tests/unit/claude-runner-interactive.test.ts +8 -6
- package/tests/unit/claude-runner.test.ts +5 -66
- package/tests/unit/config-command.test.ts +84 -5
- package/tests/unit/config.test.ts +292 -45
- package/tests/unit/frontmatter.test.ts +182 -0
- package/tests/unit/post-execution-picker.test.ts +5 -0
- package/tests/unit/session-parser.test.ts +301 -0
- package/tests/unit/terminal-symbols.test.ts +263 -33
- package/tests/unit/timer-verbose-integration.test.ts +170 -0
- package/tests/unit/token-tracker.test.ts +653 -17
- package/tests/unit/validation.test.ts +6 -4
- package/tests/unit/worktree.test.ts +242 -0
|
@@ -3,17 +3,26 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { createConfigCommand } from '../../src/commands/config.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
validateConfig,
|
|
8
|
+
ConfigValidationError,
|
|
9
|
+
resolveConfig,
|
|
10
|
+
getModel,
|
|
11
|
+
resetConfigCache,
|
|
12
|
+
} from '../../src/utils/config.js';
|
|
13
|
+
import { DEFAULT_CONFIG } from '../../src/types/config.js';
|
|
7
14
|
|
|
8
15
|
describe('Config Command', () => {
|
|
9
16
|
let tempDir: string;
|
|
10
17
|
|
|
11
18
|
beforeEach(() => {
|
|
12
19
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-config-cmd-test-'));
|
|
20
|
+
resetConfigCache();
|
|
13
21
|
});
|
|
14
22
|
|
|
15
23
|
afterEach(() => {
|
|
16
24
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
+
resetConfigCache();
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
describe('Command setup', () => {
|
|
@@ -55,8 +64,8 @@ describe('Config Command', () => {
|
|
|
55
64
|
expect(() => validateConfig(config)).not.toThrow();
|
|
56
65
|
});
|
|
57
66
|
|
|
58
|
-
it('should accept valid config with
|
|
59
|
-
const config = {
|
|
67
|
+
it('should accept valid config with effortMapping override', () => {
|
|
68
|
+
const config = { effortMapping: { low: 'haiku', medium: 'sonnet' } };
|
|
60
69
|
expect(() => validateConfig(config)).not.toThrow();
|
|
61
70
|
});
|
|
62
71
|
|
|
@@ -75,8 +84,8 @@ describe('Config Command', () => {
|
|
|
75
84
|
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
76
85
|
});
|
|
77
86
|
|
|
78
|
-
it('should reject config with invalid
|
|
79
|
-
const config = {
|
|
87
|
+
it('should reject config with invalid effortMapping model', () => {
|
|
88
|
+
const config = { effortMapping: { low: 'gpt-4' } };
|
|
80
89
|
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
81
90
|
});
|
|
82
91
|
|
|
@@ -160,4 +169,74 @@ describe('Config Command', () => {
|
|
|
160
169
|
expect(content).toContain('"worktree": true');
|
|
161
170
|
});
|
|
162
171
|
});
|
|
172
|
+
|
|
173
|
+
describe('Error recovery - invalid config fallback', () => {
|
|
174
|
+
// These tests verify the behaviors that runConfigSession relies on for error recovery
|
|
175
|
+
// The config command catches errors from getModel/getEffort and falls back to defaults
|
|
176
|
+
|
|
177
|
+
it('should throw on invalid JSON when resolving config', () => {
|
|
178
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
179
|
+
fs.writeFileSync(configPath, '{ invalid json }}}');
|
|
180
|
+
|
|
181
|
+
expect(() => resolveConfig(configPath)).toThrow(SyntaxError);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should throw on schema validation failure when resolving config', () => {
|
|
185
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
186
|
+
fs.writeFileSync(configPath, JSON.stringify({ unknownKey: true }));
|
|
187
|
+
|
|
188
|
+
expect(() => resolveConfig(configPath)).toThrow(ConfigValidationError);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should have valid default fallback values for config scenario', () => {
|
|
192
|
+
// These are the values that runConfigSession uses when config loading fails
|
|
193
|
+
expect(DEFAULT_CONFIG.models.config).toBe('sonnet');
|
|
194
|
+
// effortMapping defaults used for per-task model resolution
|
|
195
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('sonnet');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should be able to read raw file contents even when config is invalid JSON', () => {
|
|
199
|
+
// This verifies that getCurrentConfigState can still read the broken file
|
|
200
|
+
// so Claude can see and help fix it
|
|
201
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
202
|
+
const invalidContent = '{ "broken": true, }'; // trailing comma = invalid
|
|
203
|
+
fs.writeFileSync(configPath, invalidContent);
|
|
204
|
+
|
|
205
|
+
// File is readable even though it's invalid JSON
|
|
206
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
207
|
+
expect(content).toBe(invalidContent);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should be able to read raw file contents even when config fails schema validation', () => {
|
|
211
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
212
|
+
const invalidContent = JSON.stringify({ badKey: 'value' }, null, 2);
|
|
213
|
+
fs.writeFileSync(configPath, invalidContent);
|
|
214
|
+
|
|
215
|
+
// File is readable even though it fails validation
|
|
216
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
217
|
+
expect(JSON.parse(content)).toEqual({ badKey: 'value' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('resetConfigCache should clear the cached config', () => {
|
|
221
|
+
// This is used by runConfigSession to clear a broken cached config
|
|
222
|
+
// so subsequent operations don't fail
|
|
223
|
+
const configPath = path.join(tempDir, 'valid.json');
|
|
224
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 99 }));
|
|
225
|
+
|
|
226
|
+
// Load the config
|
|
227
|
+
const config1 = resolveConfig(configPath);
|
|
228
|
+
expect(config1.timeout).toBe(99);
|
|
229
|
+
|
|
230
|
+
// Write different content
|
|
231
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }));
|
|
232
|
+
|
|
233
|
+
// Without reset, we'd still get cached value (but resolveConfig doesn't use cache)
|
|
234
|
+
// This test verifies resetConfigCache exists and can be called
|
|
235
|
+
resetConfigCache();
|
|
236
|
+
|
|
237
|
+
// After reset, we should get new value
|
|
238
|
+
const config2 = resolveConfig(configPath);
|
|
239
|
+
expect(config2.timeout).toBe(120);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
163
242
|
});
|
|
@@ -8,14 +8,19 @@ import {
|
|
|
8
8
|
ConfigValidationError,
|
|
9
9
|
resolveConfig,
|
|
10
10
|
getModel,
|
|
11
|
-
|
|
11
|
+
getEffortMapping,
|
|
12
|
+
resolveEffortToModel,
|
|
13
|
+
getModelTier,
|
|
14
|
+
applyModelCeiling,
|
|
12
15
|
getCommitFormat,
|
|
13
16
|
getCommitPrefix,
|
|
14
17
|
getTimeout,
|
|
15
18
|
getMaxRetries,
|
|
16
19
|
getAutoCommit,
|
|
17
20
|
getWorktreeDefault,
|
|
18
|
-
|
|
21
|
+
getSyncMainBranch,
|
|
22
|
+
getModelShortName,
|
|
23
|
+
resolveFullModelId,
|
|
19
24
|
resetConfigCache,
|
|
20
25
|
saveConfig,
|
|
21
26
|
renderCommitMessage,
|
|
@@ -89,13 +94,12 @@ describe('Config', () => {
|
|
|
89
94
|
it('should accept a full valid config', () => {
|
|
90
95
|
const config = {
|
|
91
96
|
models: { plan: 'opus', execute: 'haiku' },
|
|
92
|
-
|
|
97
|
+
effortMapping: { low: 'haiku', medium: 'sonnet', high: 'opus' },
|
|
93
98
|
timeout: 30,
|
|
94
99
|
maxRetries: 5,
|
|
95
100
|
autoCommit: false,
|
|
96
101
|
worktree: true,
|
|
97
102
|
commitFormat: { prefix: 'MY', task: '{prefix}[{projectId}] {description}' },
|
|
98
|
-
claudeCommand: '/usr/local/bin/claude',
|
|
99
103
|
};
|
|
100
104
|
expect(() => validateConfig(config)).not.toThrow();
|
|
101
105
|
});
|
|
@@ -112,12 +116,16 @@ describe('Config', () => {
|
|
|
112
116
|
expect(() => validateConfig({ unknownKey: 'value' })).toThrow('Unknown config key: unknownKey');
|
|
113
117
|
});
|
|
114
118
|
|
|
119
|
+
it('should reject removed claudeCommand key', () => {
|
|
120
|
+
expect(() => validateConfig({ claudeCommand: 'claude' })).toThrow('Unknown config key: claudeCommand');
|
|
121
|
+
});
|
|
122
|
+
|
|
115
123
|
it('should reject unknown model keys', () => {
|
|
116
124
|
expect(() => validateConfig({ models: { unknownScenario: 'opus' } })).toThrow('Unknown config key: models.unknownScenario');
|
|
117
125
|
});
|
|
118
126
|
|
|
119
|
-
it('should reject unknown
|
|
120
|
-
expect(() => validateConfig({
|
|
127
|
+
it('should reject unknown effortMapping keys', () => {
|
|
128
|
+
expect(() => validateConfig({ effortMapping: { unknownLevel: 'haiku' } })).toThrow('Unknown config key: effortMapping.unknownLevel');
|
|
121
129
|
});
|
|
122
130
|
|
|
123
131
|
it('should reject unknown commitFormat keys', () => {
|
|
@@ -150,9 +158,9 @@ describe('Config', () => {
|
|
|
150
158
|
expect(() => validateConfig({ models: { plan: 123 } })).toThrow('models.plan must be');
|
|
151
159
|
});
|
|
152
160
|
|
|
153
|
-
// Invalid
|
|
154
|
-
it('should reject invalid
|
|
155
|
-
expect(() => validateConfig({
|
|
161
|
+
// Invalid effortMapping values
|
|
162
|
+
it('should reject invalid effortMapping model names', () => {
|
|
163
|
+
expect(() => validateConfig({ effortMapping: { low: 'invalid-model' } })).toThrow('effortMapping.low must be a short alias');
|
|
156
164
|
});
|
|
157
165
|
|
|
158
166
|
// Invalid types for nested objects
|
|
@@ -164,8 +172,8 @@ describe('Config', () => {
|
|
|
164
172
|
expect(() => validateConfig({ models: ['opus'] })).toThrow('models must be an object');
|
|
165
173
|
});
|
|
166
174
|
|
|
167
|
-
it('should reject non-object
|
|
168
|
-
expect(() => validateConfig({
|
|
175
|
+
it('should reject non-object effortMapping', () => {
|
|
176
|
+
expect(() => validateConfig({ effortMapping: 'high' })).toThrow('effortMapping must be an object');
|
|
169
177
|
});
|
|
170
178
|
|
|
171
179
|
it('should reject non-object commitFormat', () => {
|
|
@@ -207,17 +215,13 @@ describe('Config', () => {
|
|
|
207
215
|
expect(() => validateConfig({ worktree: 1 })).toThrow('worktree must be a boolean');
|
|
208
216
|
});
|
|
209
217
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
expect(() => validateConfig({ claudeCommand: '' })).toThrow('claudeCommand must be a non-empty string');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should reject whitespace-only claudeCommand', () => {
|
|
216
|
-
expect(() => validateConfig({ claudeCommand: ' ' })).toThrow('claudeCommand must be a non-empty string');
|
|
218
|
+
it('should reject non-boolean syncMainBranch', () => {
|
|
219
|
+
expect(() => validateConfig({ syncMainBranch: 'yes' })).toThrow('syncMainBranch must be a boolean');
|
|
217
220
|
});
|
|
218
221
|
|
|
219
|
-
it('should
|
|
220
|
-
expect(() => validateConfig({
|
|
222
|
+
it('should accept boolean syncMainBranch', () => {
|
|
223
|
+
expect(() => validateConfig({ syncMainBranch: true })).not.toThrow();
|
|
224
|
+
expect(() => validateConfig({ syncMainBranch: false })).not.toThrow();
|
|
221
225
|
});
|
|
222
226
|
|
|
223
227
|
// Non-string commitFormat values
|
|
@@ -267,13 +271,14 @@ describe('Config', () => {
|
|
|
267
271
|
expect(config.models.failureAnalysis).toBe('haiku'); // default preserved
|
|
268
272
|
});
|
|
269
273
|
|
|
270
|
-
it('should deep-merge partial
|
|
274
|
+
it('should deep-merge partial effortMapping override', () => {
|
|
271
275
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
272
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
276
|
+
fs.writeFileSync(configPath, JSON.stringify({ effortMapping: { medium: 'opus' } }));
|
|
273
277
|
|
|
274
278
|
const config = resolveConfig(configPath);
|
|
275
|
-
expect(config.
|
|
276
|
-
expect(config.
|
|
279
|
+
expect(config.effortMapping.medium).toBe('opus');
|
|
280
|
+
expect(config.effortMapping.low).toBe('haiku'); // default preserved
|
|
281
|
+
expect(config.effortMapping.high).toBe('opus'); // default preserved
|
|
277
282
|
});
|
|
278
283
|
|
|
279
284
|
it('should deep-merge partial commitFormat override', () => {
|
|
@@ -296,6 +301,19 @@ describe('Config', () => {
|
|
|
296
301
|
expect(config.maxRetries).toBe(3); // default preserved
|
|
297
302
|
});
|
|
298
303
|
|
|
304
|
+
it('should override syncMainBranch', () => {
|
|
305
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
306
|
+
fs.writeFileSync(configPath, JSON.stringify({ syncMainBranch: false }));
|
|
307
|
+
|
|
308
|
+
const config = resolveConfig(configPath);
|
|
309
|
+
expect(config.syncMainBranch).toBe(false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should default syncMainBranch to true', () => {
|
|
313
|
+
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
314
|
+
expect(config.syncMainBranch).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
299
317
|
it('should throw on invalid config file', () => {
|
|
300
318
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
301
319
|
fs.writeFileSync(configPath, JSON.stringify({ unknownKey: true }));
|
|
@@ -348,11 +366,12 @@ describe('Config', () => {
|
|
|
348
366
|
expect(config.models.execute).toBe('opus');
|
|
349
367
|
});
|
|
350
368
|
|
|
351
|
-
it('
|
|
369
|
+
it('effortMapping resolves correctly from config', () => {
|
|
352
370
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
353
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
371
|
+
fs.writeFileSync(configPath, JSON.stringify({ effortMapping: { high: 'sonnet' } }));
|
|
354
372
|
const config = resolveConfig(configPath);
|
|
355
|
-
expect(config.
|
|
373
|
+
expect(config.effortMapping.high).toBe('sonnet');
|
|
374
|
+
expect(config.effortMapping.low).toBe('haiku'); // default preserved
|
|
356
375
|
});
|
|
357
376
|
|
|
358
377
|
it('getCommitFormat returns correct format', () => {
|
|
@@ -371,7 +390,6 @@ describe('Config', () => {
|
|
|
371
390
|
expect(config.maxRetries).toBe(3);
|
|
372
391
|
expect(config.autoCommit).toBe(true);
|
|
373
392
|
expect(config.worktree).toBe(false);
|
|
374
|
-
expect(config.claudeCommand).toBe('claude');
|
|
375
393
|
});
|
|
376
394
|
});
|
|
377
395
|
|
|
@@ -385,13 +403,10 @@ describe('Config', () => {
|
|
|
385
403
|
expect(DEFAULT_CONFIG.models.config).toBe('sonnet');
|
|
386
404
|
});
|
|
387
405
|
|
|
388
|
-
it('should have all
|
|
389
|
-
expect(DEFAULT_CONFIG.
|
|
390
|
-
expect(DEFAULT_CONFIG.
|
|
391
|
-
expect(DEFAULT_CONFIG.
|
|
392
|
-
expect(DEFAULT_CONFIG.effort.failureAnalysis).toBe('low');
|
|
393
|
-
expect(DEFAULT_CONFIG.effort.prGeneration).toBe('medium');
|
|
394
|
-
expect(DEFAULT_CONFIG.effort.config).toBe('medium');
|
|
406
|
+
it('should have all effortMapping levels defined', () => {
|
|
407
|
+
expect(DEFAULT_CONFIG.effortMapping.low).toBe('haiku');
|
|
408
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('sonnet');
|
|
409
|
+
expect(DEFAULT_CONFIG.effortMapping.high).toBe('opus');
|
|
395
410
|
});
|
|
396
411
|
|
|
397
412
|
it('should have all commit format fields defined', () => {
|
|
@@ -471,8 +486,10 @@ describe('Config', () => {
|
|
|
471
486
|
expect(DEFAULT_CONFIG.models.prGeneration).toBe('sonnet');
|
|
472
487
|
});
|
|
473
488
|
|
|
474
|
-
it('should default
|
|
475
|
-
expect(DEFAULT_CONFIG.
|
|
489
|
+
it('should default effortMapping to haiku/sonnet/opus', () => {
|
|
490
|
+
expect(DEFAULT_CONFIG.effortMapping.low).toBe('haiku');
|
|
491
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('sonnet');
|
|
492
|
+
expect(DEFAULT_CONFIG.effortMapping.high).toBe('opus');
|
|
476
493
|
});
|
|
477
494
|
|
|
478
495
|
it('should default timeout to 60', () => {
|
|
@@ -491,10 +508,6 @@ describe('Config', () => {
|
|
|
491
508
|
expect(DEFAULT_CONFIG.worktree).toBe(false);
|
|
492
509
|
});
|
|
493
510
|
|
|
494
|
-
it('should default claudeCommand to claude', () => {
|
|
495
|
-
expect(DEFAULT_CONFIG.claudeCommand).toBe('claude');
|
|
496
|
-
});
|
|
497
|
-
|
|
498
511
|
it('should default commit format to match previous hardcoded format', () => {
|
|
499
512
|
// The task format should produce the same output as the old hardcoded format
|
|
500
513
|
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.task, {
|
|
@@ -616,6 +629,49 @@ describe('Config', () => {
|
|
|
616
629
|
});
|
|
617
630
|
});
|
|
618
631
|
|
|
632
|
+
describe('getModelShortName', () => {
|
|
633
|
+
it('should return short aliases as-is', () => {
|
|
634
|
+
expect(getModelShortName('opus')).toBe('opus');
|
|
635
|
+
expect(getModelShortName('sonnet')).toBe('sonnet');
|
|
636
|
+
expect(getModelShortName('haiku')).toBe('haiku');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should extract family from full model IDs', () => {
|
|
640
|
+
expect(getModelShortName('claude-opus-4-6')).toBe('opus');
|
|
641
|
+
expect(getModelShortName('claude-opus-4-5-20251101')).toBe('opus');
|
|
642
|
+
expect(getModelShortName('claude-sonnet-4-5-20250929')).toBe('sonnet');
|
|
643
|
+
expect(getModelShortName('claude-sonnet-4-5')).toBe('sonnet');
|
|
644
|
+
expect(getModelShortName('claude-haiku-4-5-20251001')).toBe('haiku');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should return unknown model IDs as-is', () => {
|
|
648
|
+
expect(getModelShortName('gpt-4')).toBe('gpt-4');
|
|
649
|
+
expect(getModelShortName('claude-unknown-3-0')).toBe('claude-unknown-3-0');
|
|
650
|
+
expect(getModelShortName('')).toBe('');
|
|
651
|
+
expect(getModelShortName('some-random-model')).toBe('some-random-model');
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('resolveFullModelId', () => {
|
|
656
|
+
it('should resolve short aliases to full model IDs', () => {
|
|
657
|
+
expect(resolveFullModelId('opus')).toBe('claude-opus-4-6');
|
|
658
|
+
expect(resolveFullModelId('sonnet')).toBe('claude-sonnet-4-5-20250929');
|
|
659
|
+
expect(resolveFullModelId('haiku')).toBe('claude-haiku-4-5-20251001');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should return full model IDs as-is', () => {
|
|
663
|
+
expect(resolveFullModelId('claude-opus-4-6')).toBe('claude-opus-4-6');
|
|
664
|
+
expect(resolveFullModelId('claude-sonnet-4-5-20250929')).toBe('claude-sonnet-4-5-20250929');
|
|
665
|
+
expect(resolveFullModelId('claude-haiku-4-5-20251001')).toBe('claude-haiku-4-5-20251001');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('should return unknown model strings as-is', () => {
|
|
669
|
+
expect(resolveFullModelId('gpt-4')).toBe('gpt-4');
|
|
670
|
+
expect(resolveFullModelId('claude-unknown-3-0')).toBe('claude-unknown-3-0');
|
|
671
|
+
expect(resolveFullModelId('')).toBe('');
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
619
675
|
describe('config integration - overrides work', () => {
|
|
620
676
|
it('should use custom model when configured', () => {
|
|
621
677
|
const configPath = path.join(tempDir, 'custom-models.json');
|
|
@@ -628,13 +684,14 @@ describe('Config', () => {
|
|
|
628
684
|
expect(config.models.failureAnalysis).toBe('haiku');
|
|
629
685
|
});
|
|
630
686
|
|
|
631
|
-
it('should use custom
|
|
687
|
+
it('should use custom effortMapping when configured', () => {
|
|
632
688
|
const configPath = path.join(tempDir, 'custom-effort.json');
|
|
633
|
-
saveConfig(configPath, {
|
|
689
|
+
saveConfig(configPath, { effortMapping: { high: 'sonnet' } });
|
|
634
690
|
const config = resolveConfig(configPath);
|
|
635
|
-
expect(config.
|
|
691
|
+
expect(config.effortMapping.high).toBe('sonnet');
|
|
636
692
|
// Others should remain at defaults
|
|
637
|
-
expect(config.
|
|
693
|
+
expect(config.effortMapping.low).toBe('haiku');
|
|
694
|
+
expect(config.effortMapping.medium).toBe('sonnet');
|
|
638
695
|
});
|
|
639
696
|
|
|
640
697
|
it('should use custom commit format when configured', () => {
|
|
@@ -647,4 +704,194 @@ describe('Config', () => {
|
|
|
647
704
|
expect(config.commitFormat.plan).toBe(DEFAULT_CONFIG.commitFormat.plan);
|
|
648
705
|
});
|
|
649
706
|
});
|
|
707
|
+
|
|
708
|
+
describe('validateConfig - display', () => {
|
|
709
|
+
it('should accept valid display config', () => {
|
|
710
|
+
expect(() => validateConfig({
|
|
711
|
+
display: {
|
|
712
|
+
showRateLimitEstimate: true,
|
|
713
|
+
showCacheTokens: false,
|
|
714
|
+
},
|
|
715
|
+
})).not.toThrow();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should accept partial display override', () => {
|
|
719
|
+
expect(() => validateConfig({
|
|
720
|
+
display: { showRateLimitEstimate: false },
|
|
721
|
+
})).not.toThrow();
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('should reject non-object display', () => {
|
|
725
|
+
expect(() => validateConfig({ display: 'full' })).toThrow('display must be an object');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should reject unknown display keys', () => {
|
|
729
|
+
expect(() => validateConfig({ display: { unknownKey: true } })).toThrow('Unknown config key: display.unknownKey');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should reject non-boolean display values', () => {
|
|
733
|
+
expect(() => validateConfig({ display: { showRateLimitEstimate: 'yes' } })).toThrow('display.showRateLimitEstimate must be a boolean');
|
|
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');
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe('resolveConfig - display and rateLimitWindow', () => {
|
|
763
|
+
it('should include default display when no config file', () => {
|
|
764
|
+
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
765
|
+
expect(config.display.showRateLimitEstimate).toBe(true);
|
|
766
|
+
expect(config.display.showCacheTokens).toBe(true);
|
|
767
|
+
});
|
|
768
|
+
|
|
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
|
+
it('should deep-merge partial display override', () => {
|
|
775
|
+
const configPath = path.join(tempDir, 'display.json');
|
|
776
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
777
|
+
display: { showRateLimitEstimate: false },
|
|
778
|
+
}));
|
|
779
|
+
|
|
780
|
+
const config = resolveConfig(configPath);
|
|
781
|
+
expect(config.display.showRateLimitEstimate).toBe(false);
|
|
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);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe('DEFAULT_CONFIG - display and rateLimitWindow', () => {
|
|
797
|
+
it('should have default display settings', () => {
|
|
798
|
+
expect(DEFAULT_CONFIG.display.showRateLimitEstimate).toBe(true);
|
|
799
|
+
expect(DEFAULT_CONFIG.display.showCacheTokens).toBe(true);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should have default rateLimitWindow settings', () => {
|
|
803
|
+
expect(DEFAULT_CONFIG.rateLimitWindow.sonnetTokenCap).toBe(88000);
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
describe('getModelTier', () => {
|
|
808
|
+
it('should return correct tier for short aliases', () => {
|
|
809
|
+
expect(getModelTier('haiku')).toBe(1);
|
|
810
|
+
expect(getModelTier('sonnet')).toBe(2);
|
|
811
|
+
expect(getModelTier('opus')).toBe(3);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('should extract tier from full model IDs', () => {
|
|
815
|
+
expect(getModelTier('claude-haiku-4-5-20251001')).toBe(1);
|
|
816
|
+
expect(getModelTier('claude-sonnet-4-5-20250929')).toBe(2);
|
|
817
|
+
expect(getModelTier('claude-opus-4-6')).toBe(3);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should return highest tier for unknown models', () => {
|
|
821
|
+
expect(getModelTier('unknown-model')).toBe(3);
|
|
822
|
+
expect(getModelTier('claude-future-5-0')).toBe(3);
|
|
823
|
+
expect(getModelTier('')).toBe(3);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('applyModelCeiling', () => {
|
|
828
|
+
it('should return resolved model when below ceiling', () => {
|
|
829
|
+
expect(applyModelCeiling('haiku', 'sonnet')).toBe('haiku');
|
|
830
|
+
expect(applyModelCeiling('haiku', 'opus')).toBe('haiku');
|
|
831
|
+
expect(applyModelCeiling('sonnet', 'opus')).toBe('sonnet');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should return ceiling model when above ceiling', () => {
|
|
835
|
+
expect(applyModelCeiling('opus', 'sonnet')).toBe('sonnet');
|
|
836
|
+
expect(applyModelCeiling('opus', 'haiku')).toBe('haiku');
|
|
837
|
+
expect(applyModelCeiling('sonnet', 'haiku')).toBe('haiku');
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should return resolved model when at ceiling', () => {
|
|
841
|
+
expect(applyModelCeiling('sonnet', 'sonnet')).toBe('sonnet');
|
|
842
|
+
expect(applyModelCeiling('opus', 'opus')).toBe('opus');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should work with full model IDs', () => {
|
|
846
|
+
expect(applyModelCeiling('claude-opus-4-6', 'sonnet')).toBe('sonnet');
|
|
847
|
+
expect(applyModelCeiling('claude-haiku-4-5-20251001', 'claude-opus-4-6')).toBe('claude-haiku-4-5-20251001');
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe('resolveEffortToModel', () => {
|
|
852
|
+
it('should resolve effort levels to default models', () => {
|
|
853
|
+
const configPath = path.join(tempDir, 'default.json');
|
|
854
|
+
// Use default config
|
|
855
|
+
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
856
|
+
expect(config.effortMapping.low).toBe('haiku');
|
|
857
|
+
expect(config.effortMapping.medium).toBe('sonnet');
|
|
858
|
+
expect(config.effortMapping.high).toBe('opus');
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe('validateConfig - effortMapping', () => {
|
|
863
|
+
it('should accept valid effortMapping config', () => {
|
|
864
|
+
expect(() => validateConfig({
|
|
865
|
+
effortMapping: {
|
|
866
|
+
low: 'haiku',
|
|
867
|
+
medium: 'sonnet',
|
|
868
|
+
high: 'opus',
|
|
869
|
+
},
|
|
870
|
+
})).not.toThrow();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should accept partial effortMapping override', () => {
|
|
874
|
+
expect(() => validateConfig({
|
|
875
|
+
effortMapping: { high: 'sonnet' },
|
|
876
|
+
})).not.toThrow();
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('should accept full model IDs in effortMapping', () => {
|
|
880
|
+
expect(() => validateConfig({
|
|
881
|
+
effortMapping: { low: 'claude-haiku-4-5-20251001' },
|
|
882
|
+
})).not.toThrow();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('should reject invalid model names in effortMapping', () => {
|
|
886
|
+
expect(() => validateConfig({
|
|
887
|
+
effortMapping: { low: 'gpt-4' },
|
|
888
|
+
})).toThrow('effortMapping.low must be a short alias');
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should reject unknown keys in effortMapping', () => {
|
|
892
|
+
expect(() => validateConfig({
|
|
893
|
+
effortMapping: { extra: 'haiku' },
|
|
894
|
+
})).toThrow('Unknown config key: effortMapping.extra');
|
|
895
|
+
});
|
|
896
|
+
});
|
|
650
897
|
});
|