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
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
ConfigValidationError,
|
|
9
9
|
resolveConfig,
|
|
10
10
|
getModel,
|
|
11
|
-
getEffort,
|
|
12
11
|
resetConfigCache,
|
|
13
12
|
} from '../../src/utils/config.js';
|
|
14
13
|
import { DEFAULT_CONFIG } from '../../src/types/config.js';
|
|
@@ -51,6 +50,18 @@ describe('Config Command', () => {
|
|
|
51
50
|
expect(resetOption).toBeDefined();
|
|
52
51
|
});
|
|
53
52
|
|
|
53
|
+
it('should have a --get option', () => {
|
|
54
|
+
const cmd = createConfigCommand();
|
|
55
|
+
const getOption = cmd.options.find((o) => o.long === '--get');
|
|
56
|
+
expect(getOption).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should have a --set option', () => {
|
|
60
|
+
const cmd = createConfigCommand();
|
|
61
|
+
const setOption = cmd.options.find((o) => o.long === '--set');
|
|
62
|
+
expect(setOption).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
54
65
|
it('should register in a parent program', () => {
|
|
55
66
|
const program = new Command();
|
|
56
67
|
program.addCommand(createConfigCommand());
|
|
@@ -65,8 +76,8 @@ describe('Config Command', () => {
|
|
|
65
76
|
expect(() => validateConfig(config)).not.toThrow();
|
|
66
77
|
});
|
|
67
78
|
|
|
68
|
-
it('should accept valid config with
|
|
69
|
-
const config = {
|
|
79
|
+
it('should accept valid config with effortMapping override', () => {
|
|
80
|
+
const config = { effortMapping: { low: 'haiku', medium: 'sonnet' } };
|
|
70
81
|
expect(() => validateConfig(config)).not.toThrow();
|
|
71
82
|
});
|
|
72
83
|
|
|
@@ -85,8 +96,8 @@ describe('Config Command', () => {
|
|
|
85
96
|
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
86
97
|
});
|
|
87
98
|
|
|
88
|
-
it('should reject config with invalid
|
|
89
|
-
const config = {
|
|
99
|
+
it('should reject config with invalid effortMapping model', () => {
|
|
100
|
+
const config = { effortMapping: { low: 'gpt-4' } };
|
|
90
101
|
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
91
102
|
});
|
|
92
103
|
|
|
@@ -192,7 +203,8 @@ describe('Config Command', () => {
|
|
|
192
203
|
it('should have valid default fallback values for config scenario', () => {
|
|
193
204
|
// These are the values that runConfigSession uses when config loading fails
|
|
194
205
|
expect(DEFAULT_CONFIG.models.config).toBe('sonnet');
|
|
195
|
-
|
|
206
|
+
// effortMapping defaults used for per-task model resolution
|
|
207
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('sonnet');
|
|
196
208
|
});
|
|
197
209
|
|
|
198
210
|
it('should be able to read raw file contents even when config is invalid JSON', () => {
|
|
@@ -239,4 +251,162 @@ describe('Config Command', () => {
|
|
|
239
251
|
expect(config2.timeout).toBe(120);
|
|
240
252
|
});
|
|
241
253
|
});
|
|
254
|
+
|
|
255
|
+
describe('--get flag', () => {
|
|
256
|
+
it('should return full config when no key is provided', () => {
|
|
257
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
258
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }, null, 2));
|
|
259
|
+
|
|
260
|
+
const config = resolveConfig(configPath);
|
|
261
|
+
expect(config.timeout).toBe(120);
|
|
262
|
+
expect(config.models.execute).toBe(DEFAULT_CONFIG.models.execute);
|
|
263
|
+
|
|
264
|
+
// Verify full config has all expected top-level keys
|
|
265
|
+
expect(config).toHaveProperty('models');
|
|
266
|
+
expect(config).toHaveProperty('effortMapping');
|
|
267
|
+
expect(config).toHaveProperty('timeout');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return specific value for dot-notation key', () => {
|
|
271
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
272
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: { plan: 'sonnet' } }, null, 2));
|
|
273
|
+
|
|
274
|
+
const config = resolveConfig(configPath);
|
|
275
|
+
expect(config.models.plan).toBe('sonnet');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle nested keys', () => {
|
|
279
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
280
|
+
fs.writeFileSync(configPath, JSON.stringify({ display: { showRateLimitEstimate: false } }, null, 2));
|
|
281
|
+
|
|
282
|
+
const config = resolveConfig(configPath);
|
|
283
|
+
expect(config.display.showRateLimitEstimate).toBe(false);
|
|
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);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('--set flag', () => {
|
|
296
|
+
it('should set a string value', () => {
|
|
297
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
298
|
+
|
|
299
|
+
// Start with empty config
|
|
300
|
+
expect(fs.existsSync(configPath)).toBe(false);
|
|
301
|
+
|
|
302
|
+
// Simulate setting models.plan to sonnet
|
|
303
|
+
const userConfig: Record<string, unknown> = {};
|
|
304
|
+
const keys = 'models.plan'.split('.');
|
|
305
|
+
let current: Record<string, unknown> = userConfig;
|
|
306
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
307
|
+
const key = keys[i]!;
|
|
308
|
+
current[key] = {};
|
|
309
|
+
current = current[key] as Record<string, unknown>;
|
|
310
|
+
}
|
|
311
|
+
current[keys[keys.length - 1]!] = 'sonnet';
|
|
312
|
+
|
|
313
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
314
|
+
|
|
315
|
+
const config = resolveConfig(configPath);
|
|
316
|
+
expect(config.models.plan).toBe('sonnet');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should set a number value', () => {
|
|
320
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
321
|
+
|
|
322
|
+
const userConfig = { timeout: 120 };
|
|
323
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
324
|
+
|
|
325
|
+
const config = resolveConfig(configPath);
|
|
326
|
+
expect(config.timeout).toBe(120);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should set a boolean value', () => {
|
|
330
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
331
|
+
|
|
332
|
+
const userConfig = { autoCommit: false };
|
|
333
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
334
|
+
|
|
335
|
+
const config = resolveConfig(configPath);
|
|
336
|
+
expect(config.autoCommit).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should remove key when value matches default', () => {
|
|
340
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
341
|
+
|
|
342
|
+
// Set a non-default value first
|
|
343
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: { plan: 'sonnet' } }, null, 2));
|
|
344
|
+
let config = resolveConfig(configPath);
|
|
345
|
+
expect(config.models.plan).toBe('sonnet');
|
|
346
|
+
|
|
347
|
+
// Now set back to default (opus)
|
|
348
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
349
|
+
config = resolveConfig(configPath);
|
|
350
|
+
expect(config.models.plan).toBe(DEFAULT_CONFIG.models.plan);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should remove empty parent objects after key removal', () => {
|
|
354
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
355
|
+
|
|
356
|
+
// Start with a models override
|
|
357
|
+
const userConfig = { models: { plan: 'sonnet' } };
|
|
358
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
359
|
+
|
|
360
|
+
// Remove the override (simulating setting to default)
|
|
361
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
362
|
+
|
|
363
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
364
|
+
const parsed = JSON.parse(content);
|
|
365
|
+
|
|
366
|
+
// Should be empty object
|
|
367
|
+
expect(Object.keys(parsed).length).toBe(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should validate config after modification', () => {
|
|
371
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
372
|
+
|
|
373
|
+
// Valid config
|
|
374
|
+
const validConfig = { models: { execute: 'sonnet' } };
|
|
375
|
+
fs.writeFileSync(configPath, JSON.stringify(validConfig, null, 2));
|
|
376
|
+
expect(() => validateConfig(validConfig)).not.toThrow();
|
|
377
|
+
|
|
378
|
+
// Invalid config
|
|
379
|
+
const invalidConfig = { models: { execute: 'invalid-model' } };
|
|
380
|
+
fs.writeFileSync(configPath, JSON.stringify(invalidConfig, null, 2));
|
|
381
|
+
expect(() => validateConfig(invalidConfig)).toThrow(ConfigValidationError);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should delete config file when it becomes empty', () => {
|
|
385
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
386
|
+
|
|
387
|
+
// Create a config file
|
|
388
|
+
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }, null, 2));
|
|
389
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
390
|
+
|
|
391
|
+
// Simulate removing all keys (setting everything to defaults)
|
|
392
|
+
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
393
|
+
|
|
394
|
+
// Check if file still exists (in the actual implementation, empty configs are deleted)
|
|
395
|
+
// For this test, we just verify the file operations work
|
|
396
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
397
|
+
expect(JSON.parse(content)).toEqual({});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should handle nested value updates', () => {
|
|
401
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
402
|
+
|
|
403
|
+
// Set a nested value
|
|
404
|
+
const userConfig = { display: { showRateLimitEstimate: false } };
|
|
405
|
+
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
406
|
+
|
|
407
|
+
const config = resolveConfig(configPath);
|
|
408
|
+
expect(config.display.showRateLimitEstimate).toBe(false);
|
|
409
|
+
expect(config.display.showCacheTokens).toBe(DEFAULT_CONFIG.display.showCacheTokens);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
242
412
|
});
|
|
@@ -8,15 +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,
|
|
19
22
|
getModelShortName,
|
|
23
|
+
resolveFullModelId,
|
|
20
24
|
resetConfigCache,
|
|
21
25
|
saveConfig,
|
|
22
26
|
renderCommitMessage,
|
|
@@ -90,13 +94,12 @@ describe('Config', () => {
|
|
|
90
94
|
it('should accept a full valid config', () => {
|
|
91
95
|
const config = {
|
|
92
96
|
models: { plan: 'opus', execute: 'haiku' },
|
|
93
|
-
|
|
97
|
+
effortMapping: { low: 'haiku', medium: 'sonnet', high: 'opus' },
|
|
94
98
|
timeout: 30,
|
|
95
99
|
maxRetries: 5,
|
|
96
100
|
autoCommit: false,
|
|
97
101
|
worktree: true,
|
|
98
102
|
commitFormat: { prefix: 'MY', task: '{prefix}[{projectId}] {description}' },
|
|
99
|
-
claudeCommand: '/usr/local/bin/claude',
|
|
100
103
|
};
|
|
101
104
|
expect(() => validateConfig(config)).not.toThrow();
|
|
102
105
|
});
|
|
@@ -113,12 +116,16 @@ describe('Config', () => {
|
|
|
113
116
|
expect(() => validateConfig({ unknownKey: 'value' })).toThrow('Unknown config key: unknownKey');
|
|
114
117
|
});
|
|
115
118
|
|
|
119
|
+
it('should reject removed claudeCommand key', () => {
|
|
120
|
+
expect(() => validateConfig({ claudeCommand: 'claude' })).toThrow('Unknown config key: claudeCommand');
|
|
121
|
+
});
|
|
122
|
+
|
|
116
123
|
it('should reject unknown model keys', () => {
|
|
117
124
|
expect(() => validateConfig({ models: { unknownScenario: 'opus' } })).toThrow('Unknown config key: models.unknownScenario');
|
|
118
125
|
});
|
|
119
126
|
|
|
120
|
-
it('should reject unknown
|
|
121
|
-
expect(() => validateConfig({
|
|
127
|
+
it('should reject unknown effortMapping keys', () => {
|
|
128
|
+
expect(() => validateConfig({ effortMapping: { unknownLevel: 'haiku' } })).toThrow('Unknown config key: effortMapping.unknownLevel');
|
|
122
129
|
});
|
|
123
130
|
|
|
124
131
|
it('should reject unknown commitFormat keys', () => {
|
|
@@ -151,9 +158,9 @@ describe('Config', () => {
|
|
|
151
158
|
expect(() => validateConfig({ models: { plan: 123 } })).toThrow('models.plan must be');
|
|
152
159
|
});
|
|
153
160
|
|
|
154
|
-
// Invalid
|
|
155
|
-
it('should reject invalid
|
|
156
|
-
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');
|
|
157
164
|
});
|
|
158
165
|
|
|
159
166
|
// Invalid types for nested objects
|
|
@@ -165,8 +172,8 @@ describe('Config', () => {
|
|
|
165
172
|
expect(() => validateConfig({ models: ['opus'] })).toThrow('models must be an object');
|
|
166
173
|
});
|
|
167
174
|
|
|
168
|
-
it('should reject non-object
|
|
169
|
-
expect(() => validateConfig({
|
|
175
|
+
it('should reject non-object effortMapping', () => {
|
|
176
|
+
expect(() => validateConfig({ effortMapping: 'high' })).toThrow('effortMapping must be an object');
|
|
170
177
|
});
|
|
171
178
|
|
|
172
179
|
it('should reject non-object commitFormat', () => {
|
|
@@ -208,17 +215,13 @@ describe('Config', () => {
|
|
|
208
215
|
expect(() => validateConfig({ worktree: 1 })).toThrow('worktree must be a boolean');
|
|
209
216
|
});
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
expect(() => validateConfig({ claudeCommand: '' })).toThrow('claudeCommand must be a non-empty string');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should reject whitespace-only claudeCommand', () => {
|
|
217
|
-
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');
|
|
218
220
|
});
|
|
219
221
|
|
|
220
|
-
it('should
|
|
221
|
-
expect(() => validateConfig({
|
|
222
|
+
it('should accept boolean syncMainBranch', () => {
|
|
223
|
+
expect(() => validateConfig({ syncMainBranch: true })).not.toThrow();
|
|
224
|
+
expect(() => validateConfig({ syncMainBranch: false })).not.toThrow();
|
|
222
225
|
});
|
|
223
226
|
|
|
224
227
|
// Non-string commitFormat values
|
|
@@ -268,13 +271,14 @@ describe('Config', () => {
|
|
|
268
271
|
expect(config.models.failureAnalysis).toBe('haiku'); // default preserved
|
|
269
272
|
});
|
|
270
273
|
|
|
271
|
-
it('should deep-merge partial
|
|
274
|
+
it('should deep-merge partial effortMapping override', () => {
|
|
272
275
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
273
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
276
|
+
fs.writeFileSync(configPath, JSON.stringify({ effortMapping: { medium: 'opus' } }));
|
|
274
277
|
|
|
275
278
|
const config = resolveConfig(configPath);
|
|
276
|
-
expect(config.
|
|
277
|
-
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
|
|
278
282
|
});
|
|
279
283
|
|
|
280
284
|
it('should deep-merge partial commitFormat override', () => {
|
|
@@ -297,6 +301,19 @@ describe('Config', () => {
|
|
|
297
301
|
expect(config.maxRetries).toBe(3); // default preserved
|
|
298
302
|
});
|
|
299
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
|
+
|
|
300
317
|
it('should throw on invalid config file', () => {
|
|
301
318
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
302
319
|
fs.writeFileSync(configPath, JSON.stringify({ unknownKey: true }));
|
|
@@ -349,11 +366,12 @@ describe('Config', () => {
|
|
|
349
366
|
expect(config.models.execute).toBe('opus');
|
|
350
367
|
});
|
|
351
368
|
|
|
352
|
-
it('
|
|
369
|
+
it('effortMapping resolves correctly from config', () => {
|
|
353
370
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
354
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
371
|
+
fs.writeFileSync(configPath, JSON.stringify({ effortMapping: { high: 'sonnet' } }));
|
|
355
372
|
const config = resolveConfig(configPath);
|
|
356
|
-
expect(config.
|
|
373
|
+
expect(config.effortMapping.high).toBe('sonnet');
|
|
374
|
+
expect(config.effortMapping.low).toBe('haiku'); // default preserved
|
|
357
375
|
});
|
|
358
376
|
|
|
359
377
|
it('getCommitFormat returns correct format', () => {
|
|
@@ -372,7 +390,6 @@ describe('Config', () => {
|
|
|
372
390
|
expect(config.maxRetries).toBe(3);
|
|
373
391
|
expect(config.autoCommit).toBe(true);
|
|
374
392
|
expect(config.worktree).toBe(false);
|
|
375
|
-
expect(config.claudeCommand).toBe('claude');
|
|
376
393
|
});
|
|
377
394
|
});
|
|
378
395
|
|
|
@@ -386,13 +403,10 @@ describe('Config', () => {
|
|
|
386
403
|
expect(DEFAULT_CONFIG.models.config).toBe('sonnet');
|
|
387
404
|
});
|
|
388
405
|
|
|
389
|
-
it('should have all
|
|
390
|
-
expect(DEFAULT_CONFIG.
|
|
391
|
-
expect(DEFAULT_CONFIG.
|
|
392
|
-
expect(DEFAULT_CONFIG.
|
|
393
|
-
expect(DEFAULT_CONFIG.effort.failureAnalysis).toBe('low');
|
|
394
|
-
expect(DEFAULT_CONFIG.effort.prGeneration).toBe('medium');
|
|
395
|
-
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');
|
|
396
410
|
});
|
|
397
411
|
|
|
398
412
|
it('should have all commit format fields defined', () => {
|
|
@@ -472,8 +486,10 @@ describe('Config', () => {
|
|
|
472
486
|
expect(DEFAULT_CONFIG.models.prGeneration).toBe('sonnet');
|
|
473
487
|
});
|
|
474
488
|
|
|
475
|
-
it('should default
|
|
476
|
-
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');
|
|
477
493
|
});
|
|
478
494
|
|
|
479
495
|
it('should default timeout to 60', () => {
|
|
@@ -492,10 +508,6 @@ describe('Config', () => {
|
|
|
492
508
|
expect(DEFAULT_CONFIG.worktree).toBe(false);
|
|
493
509
|
});
|
|
494
510
|
|
|
495
|
-
it('should default claudeCommand to claude', () => {
|
|
496
|
-
expect(DEFAULT_CONFIG.claudeCommand).toBe('claude');
|
|
497
|
-
});
|
|
498
|
-
|
|
499
511
|
it('should default commit format to match previous hardcoded format', () => {
|
|
500
512
|
// The task format should produce the same output as the old hardcoded format
|
|
501
513
|
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.task, {
|
|
@@ -640,6 +652,26 @@ describe('Config', () => {
|
|
|
640
652
|
});
|
|
641
653
|
});
|
|
642
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
|
+
|
|
643
675
|
describe('config integration - overrides work', () => {
|
|
644
676
|
it('should use custom model when configured', () => {
|
|
645
677
|
const configPath = path.join(tempDir, 'custom-models.json');
|
|
@@ -652,13 +684,14 @@ describe('Config', () => {
|
|
|
652
684
|
expect(config.models.failureAnalysis).toBe('haiku');
|
|
653
685
|
});
|
|
654
686
|
|
|
655
|
-
it('should use custom
|
|
687
|
+
it('should use custom effortMapping when configured', () => {
|
|
656
688
|
const configPath = path.join(tempDir, 'custom-effort.json');
|
|
657
|
-
saveConfig(configPath, {
|
|
689
|
+
saveConfig(configPath, { effortMapping: { high: 'sonnet' } });
|
|
658
690
|
const config = resolveConfig(configPath);
|
|
659
|
-
expect(config.
|
|
691
|
+
expect(config.effortMapping.high).toBe('sonnet');
|
|
660
692
|
// Others should remain at defaults
|
|
661
|
-
expect(config.
|
|
693
|
+
expect(config.effortMapping.low).toBe('haiku');
|
|
694
|
+
expect(config.effortMapping.medium).toBe('sonnet');
|
|
662
695
|
});
|
|
663
696
|
|
|
664
697
|
it('should use custom commit format when configured', () => {
|
|
@@ -671,4 +704,194 @@ describe('Config', () => {
|
|
|
671
704
|
expect(config.commitFormat.plan).toBe(DEFAULT_CONFIG.commitFormat.plan);
|
|
672
705
|
});
|
|
673
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
|
+
});
|
|
674
897
|
});
|