rafcode 3.0.0 → 3.8.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 +0 -1
- package/RAF/38-dual-wielder/decisions.md +9 -0
- package/RAF/38-dual-wielder/input.md +6 -1
- package/RAF/38-dual-wielder/outcomes/8-e2e-test-codex-provider.md +139 -0
- package/RAF/38-dual-wielder/plans/8-e2e-test-codex-provider.md +95 -0
- package/RAF/39-pathless-rover/decisions.md +16 -0
- package/RAF/39-pathless-rover/input.md +2 -0
- package/RAF/39-pathless-rover/outcomes/1-fix-codex-stream-renderer.md +21 -0
- package/RAF/39-pathless-rover/outcomes/2-wire-provider-flag.md +28 -0
- package/RAF/39-pathless-rover/outcomes/3-remove-worktree-flag-do.md +41 -0
- package/RAF/39-pathless-rover/outcomes/4-remove-worktree-flag-plan-amend.md +30 -0
- package/RAF/39-pathless-rover/outcomes/5-update-prompts-and-docs.md +26 -0
- package/RAF/39-pathless-rover/plans/1-fix-codex-stream-renderer.md +43 -0
- package/RAF/39-pathless-rover/plans/2-wire-provider-flag.md +48 -0
- package/RAF/39-pathless-rover/plans/3-remove-worktree-flag-do.md +41 -0
- package/RAF/39-pathless-rover/plans/4-remove-worktree-flag-plan-amend.md +43 -0
- package/RAF/39-pathless-rover/plans/5-update-prompts-and-docs.md +31 -0
- package/RAF/40-numeric-order-fix/decisions.md +7 -0
- package/RAF/40-numeric-order-fix/input.md +19 -0
- package/RAF/40-numeric-order-fix/outcomes/1-fix-numeric-sort-order.md +18 -0
- package/RAF/40-numeric-order-fix/outcomes/2-add-npm-keywords.md +10 -0
- package/RAF/40-numeric-order-fix/plans/1-fix-numeric-sort-order.md +48 -0
- package/RAF/40-numeric-order-fix/plans/2-add-npm-keywords.md +23 -0
- package/RAF/41-echo-chamber/decisions.md +13 -0
- package/RAF/41-echo-chamber/input.md +4 -0
- package/RAF/41-echo-chamber/outcomes/1-update-codex-model-defaults.md +24 -0
- package/RAF/41-echo-chamber/outcomes/2-e2e-test-codex-provider.md +74 -0
- package/RAF/41-echo-chamber/plans/1-update-codex-model-defaults.md +28 -0
- package/RAF/41-echo-chamber/plans/2-e2e-test-codex-provider.md +103 -0
- package/RAF/42-patch-parade/decisions.md +29 -0
- package/RAF/42-patch-parade/input.md +9 -0
- package/RAF/42-patch-parade/outcomes/1-fix-codex-model-resolution.md +36 -0
- package/RAF/42-patch-parade/outcomes/2-fix-provider-aware-name-generation.md +31 -0
- package/RAF/42-patch-parade/outcomes/3-fix-codex-error-event-rendering.md +32 -0
- package/RAF/42-patch-parade/outcomes/4-update-cli-help-docs.md +28 -0
- package/RAF/42-patch-parade/outcomes/5-update-default-codex-models-to-gpt-5-4.md +33 -0
- package/RAF/42-patch-parade/outcomes/6-unify-model-config-schema.md +89 -0
- package/RAF/42-patch-parade/plans/1-fix-codex-model-resolution.md +35 -0
- package/RAF/42-patch-parade/plans/2-fix-provider-aware-name-generation.md +38 -0
- package/RAF/42-patch-parade/plans/3-fix-codex-error-event-rendering.md +32 -0
- package/RAF/42-patch-parade/plans/4-update-cli-help-docs.md +31 -0
- package/RAF/42-patch-parade/plans/5-update-default-codex-models-to-gpt-5-4.md +35 -0
- package/RAF/42-patch-parade/plans/6-unify-model-config-schema.md +46 -0
- package/RAF/43-swiss-army/decisions.md +34 -0
- package/RAF/43-swiss-army/input.md +7 -0
- package/RAF/43-swiss-army/outcomes/1-fix-model-validation.md +21 -0
- package/RAF/43-swiss-army/outcomes/2-update-commit-format.md +31 -0
- package/RAF/43-swiss-army/outcomes/3-wire-reasoning-effort.md +28 -0
- package/RAF/43-swiss-army/outcomes/4-remove-provider-flag.md +27 -0
- package/RAF/43-swiss-army/outcomes/5-config-wizard-validation.md +23 -0
- package/RAF/43-swiss-army/outcomes/6-add-fast-mode.md +32 -0
- package/RAF/43-swiss-army/outcomes/7-config-preset.md +31 -0
- package/RAF/43-swiss-army/plans/1-fix-model-validation.md +38 -0
- package/RAF/43-swiss-army/plans/2-update-commit-format.md +46 -0
- package/RAF/43-swiss-army/plans/3-wire-reasoning-effort.md +39 -0
- package/RAF/43-swiss-army/plans/4-remove-provider-flag.md +43 -0
- package/RAF/43-swiss-army/plans/5-config-wizard-validation.md +42 -0
- package/RAF/43-swiss-army/plans/6-add-fast-mode.md +46 -0
- package/RAF/43-swiss-army/plans/7-config-preset.md +51 -0
- package/RAF/44-config-api-change/decisions.md +22 -0
- package/RAF/44-config-api-change/input.md +5 -0
- package/RAF/44-config-api-change/outcomes/1-restructure-config-subcommands.md +19 -0
- package/RAF/44-config-api-change/outcomes/2-move-preset-under-config.md +17 -0
- package/RAF/44-config-api-change/outcomes/3-update-existing-tests-for-config-api.md +14 -0
- package/RAF/44-config-api-change/outcomes/4-update-config-command-docs.md +11 -0
- package/RAF/44-config-api-change/outcomes/5-fix-codex-name-generation.md +18 -0
- package/RAF/44-config-api-change/plans/1-restructure-config-subcommands.md +37 -0
- package/RAF/44-config-api-change/plans/2-move-preset-under-config.md +38 -0
- package/RAF/44-config-api-change/plans/3-update-existing-tests-for-config-api.md +38 -0
- package/RAF/44-config-api-change/plans/4-update-config-command-docs.md +36 -0
- package/RAF/44-config-api-change/plans/5-fix-codex-name-generation.md +49 -0
- package/RAF/45-signal-cairn/decisions.md +7 -0
- package/RAF/45-signal-cairn/input.md +2 -0
- package/RAF/45-signal-cairn/outcomes/1-rename-provider-to-harness.md +19 -0
- package/RAF/45-signal-cairn/outcomes/2-normalize-model-display-names.md +18 -0
- package/RAF/45-signal-cairn/plans/1-rename-provider-to-harness.md +40 -0
- package/RAF/45-signal-cairn/plans/2-normalize-model-display-names.md +41 -0
- package/RAF/45-signal-lantern/decisions.md +10 -0
- package/RAF/45-signal-lantern/input.md +2 -0
- package/RAF/45-signal-lantern/outcomes/1-add-effort-and-fast-to-do-model-display.md +15 -0
- package/RAF/45-signal-lantern/outcomes/2-capture-codex-post-run-token-usage.md +15 -0
- package/RAF/45-signal-lantern/outcomes/3-show-codex-token-summaries-without-fake-cost.md +14 -0
- package/RAF/45-signal-lantern/plans/1-add-effort-and-fast-to-do-model-display.md +38 -0
- package/RAF/45-signal-lantern/plans/2-capture-codex-post-run-token-usage.md +37 -0
- package/RAF/45-signal-lantern/plans/3-show-codex-token-summaries-without-fake-cost.md +40 -0
- package/RAF/46-lantern-arc/decisions.md +19 -0
- package/RAF/46-lantern-arc/input.md +6 -0
- package/RAF/46-lantern-arc/outcomes/1-remove-spark-alias.md +16 -0
- package/RAF/46-lantern-arc/outcomes/2-clean-up-worktree-plan-command.md +30 -0
- package/RAF/46-lantern-arc/outcomes/3-fix-token-usage-accumulation.md +32 -0
- package/RAF/46-lantern-arc/outcomes/4-display-effort-in-compact-mode.md +22 -0
- package/RAF/46-lantern-arc/outcomes/5-codex-fast-mode-research.md +38 -0
- package/RAF/46-lantern-arc/outcomes/6-optimize-llm-prompts.md +39 -0
- package/RAF/46-lantern-arc/plans/1-remove-spark-alias.md +38 -0
- package/RAF/46-lantern-arc/plans/2-clean-up-worktree-plan-command.md +33 -0
- package/RAF/46-lantern-arc/plans/3-fix-token-usage-accumulation.md +33 -0
- package/RAF/46-lantern-arc/plans/4-display-effort-in-compact-mode.md +28 -0
- package/RAF/46-lantern-arc/plans/5-codex-fast-mode-research.md +34 -0
- package/RAF/46-lantern-arc/plans/6-optimize-llm-prompts.md +48 -0
- package/RAF/47-signal-trim/decisions.md +13 -0
- package/RAF/47-signal-trim/input.md +2 -0
- package/RAF/47-signal-trim/plans/1-remove-cache-from-status.md +73 -0
- package/README.md +50 -63
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +47 -49
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts +2 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +91 -230
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +54 -259
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/preset.d.ts +3 -0
- package/dist/commands/preset.d.ts.map +1 -0
- package/dist/commands/preset.js +158 -0
- package/dist/commands/preset.js.map +1 -0
- package/dist/core/claude-runner.d.ts +2 -0
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +36 -12
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/codex-runner.d.ts +1 -0
- package/dist/core/codex-runner.d.ts.map +1 -1
- package/dist/core/codex-runner.js +26 -7
- package/dist/core/codex-runner.js.map +1 -1
- package/dist/core/failure-analyzer.js +2 -1
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/git.d.ts +2 -2
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +53 -3
- package/dist/core/git.js.map +1 -1
- package/dist/core/project-manager.d.ts.map +1 -1
- package/dist/core/project-manager.js +2 -2
- package/dist/core/project-manager.js.map +1 -1
- package/dist/core/pull-request.js +5 -5
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/runner-factory.d.ts +4 -4
- package/dist/core/runner-factory.d.ts.map +1 -1
- package/dist/core/runner-factory.js +8 -8
- package/dist/core/runner-factory.js.map +1 -1
- package/dist/core/runner-interface.d.ts +1 -1
- package/dist/core/runner-types.d.ts +17 -4
- package/dist/core/runner-types.d.ts.map +1 -1
- package/dist/core/state-derivation.js +3 -3
- package/dist/core/state-derivation.js.map +1 -1
- package/dist/parsers/codex-stream-renderer.d.ts +28 -4
- package/dist/parsers/codex-stream-renderer.d.ts.map +1 -1
- package/dist/parsers/codex-stream-renderer.js +110 -0
- package/dist/parsers/codex-stream-renderer.js.map +1 -1
- package/dist/prompts/amend.d.ts +0 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +31 -104
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/execution.d.ts.map +1 -1
- package/dist/prompts/execution.js +17 -34
- package/dist/prompts/execution.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +23 -123
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +33 -32
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +14 -28
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +36 -16
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +209 -104
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +25 -12
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/paths.d.ts +5 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +9 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +15 -2
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +36 -4
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +6 -1
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +84 -51
- package/dist/utils/token-tracker.js.map +1 -1
- package/dist/utils/validation.d.ts +1 -2
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +4 -25
- package/dist/utils/validation.js.map +1 -1
- package/package.json +7 -2
- package/src/commands/config.ts +60 -63
- package/src/commands/do.ts +96 -262
- package/src/commands/plan.ts +55 -279
- package/src/commands/preset.ts +186 -0
- package/src/core/claude-runner.ts +45 -5
- package/src/core/codex-runner.ts +32 -7
- package/src/core/failure-analyzer.ts +2 -1
- package/src/core/git.ts +57 -3
- package/src/core/project-manager.ts +2 -1
- package/src/core/pull-request.ts +5 -5
- package/src/core/runner-factory.ts +9 -9
- package/src/core/runner-interface.ts +1 -1
- package/src/core/runner-types.ts +17 -4
- package/src/core/state-derivation.ts +3 -3
- package/src/parsers/codex-stream-renderer.ts +149 -4
- package/src/prompts/amend.ts +30 -105
- package/src/prompts/config-docs.md +206 -62
- package/src/prompts/execution.ts +17 -34
- package/src/prompts/planning.ts +23 -124
- package/src/types/config.ts +47 -59
- package/src/utils/config.ts +248 -115
- package/src/utils/name-generator.ts +29 -13
- package/src/utils/paths.ts +10 -0
- package/src/utils/terminal-symbols.ts +46 -6
- package/src/utils/token-tracker.ts +96 -57
- package/src/utils/validation.ts +5 -30
- package/tests/unit/amend-prompt.test.ts +3 -2
- package/tests/unit/claude-runner-interactive.test.ts +21 -3
- package/tests/unit/claude-runner.test.ts +39 -0
- package/tests/unit/codex-runner.test.ts +163 -0
- package/tests/unit/codex-stream-renderer.test.ts +127 -0
- package/tests/unit/command-output.test.ts +57 -0
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +24 -7
- package/tests/unit/commit-planning-artifacts.test.ts +26 -4
- package/tests/unit/config-command.test.ts +215 -303
- package/tests/unit/config.test.ts +319 -235
- package/tests/unit/dependency-integration.test.ts +27 -1
- package/tests/unit/do-model-display.test.ts +35 -0
- package/tests/unit/execution-prompt.test.ts +49 -19
- package/tests/unit/name-generator.test.ts +82 -12
- package/tests/unit/plan-command-auto-flag.test.ts +7 -10
- package/tests/unit/plan-command.test.ts +14 -17
- package/tests/unit/planning-prompt.test.ts +9 -8
- package/tests/unit/terminal-symbols.test.ts +94 -3
- package/tests/unit/token-tracker.test.ts +180 -1
- package/tests/unit/validation.test.ts +9 -41
- package/tests/unit/worktree-flag-override.test.ts +0 -186
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
|
+
import { jest } from '@jest/globals';
|
|
4
5
|
import {
|
|
5
6
|
getClaudeModel,
|
|
6
7
|
getClaudeSettingsPath,
|
|
@@ -19,12 +20,15 @@ import {
|
|
|
19
20
|
getAutoCommit,
|
|
20
21
|
getWorktreeDefault,
|
|
21
22
|
getSyncMainBranch,
|
|
23
|
+
getModelDisplayName,
|
|
22
24
|
getModelShortName,
|
|
25
|
+
formatModelDisplay,
|
|
23
26
|
resolveFullModelId,
|
|
24
27
|
resetConfigCache,
|
|
25
28
|
saveConfig,
|
|
26
29
|
renderCommitMessage,
|
|
27
30
|
isValidModelName,
|
|
31
|
+
collectConfigValidationWarnings,
|
|
28
32
|
} from '../../src/utils/config.js';
|
|
29
33
|
import { DEFAULT_CONFIG } from '../../src/types/config.js';
|
|
30
34
|
|
|
@@ -88,10 +92,17 @@ describe('Config', () => {
|
|
|
88
92
|
expect(() => validateConfig({})).not.toThrow();
|
|
89
93
|
});
|
|
90
94
|
|
|
91
|
-
it('should accept a full valid config', () => {
|
|
95
|
+
it('should accept a full valid config with model entries', () => {
|
|
92
96
|
const config = {
|
|
93
|
-
models: {
|
|
94
|
-
|
|
97
|
+
models: {
|
|
98
|
+
plan: { model: 'opus', harness: 'claude' },
|
|
99
|
+
execute: { model: 'haiku', harness: 'claude' },
|
|
100
|
+
},
|
|
101
|
+
effortMapping: {
|
|
102
|
+
low: { model: 'sonnet', harness: 'claude' },
|
|
103
|
+
medium: { model: 'opus', harness: 'claude' },
|
|
104
|
+
high: { model: 'opus', harness: 'claude' },
|
|
105
|
+
},
|
|
95
106
|
timeout: 30,
|
|
96
107
|
maxRetries: 5,
|
|
97
108
|
autoCommit: false,
|
|
@@ -101,6 +112,37 @@ describe('Config', () => {
|
|
|
101
112
|
expect(() => validateConfig(config)).not.toThrow();
|
|
102
113
|
});
|
|
103
114
|
|
|
115
|
+
it('should accept mixed-harness model entries', () => {
|
|
116
|
+
const config = {
|
|
117
|
+
models: {
|
|
118
|
+
plan: { model: 'opus', harness: 'claude' },
|
|
119
|
+
execute: { model: 'gpt-5.4', harness: 'codex' },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
expect(() => validateConfig(config)).not.toThrow();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should accept model entries with reasoningEffort', () => {
|
|
126
|
+
const config = {
|
|
127
|
+
models: {
|
|
128
|
+
plan: { model: 'opus', harness: 'claude', reasoningEffort: 'high' },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
expect(() => validateConfig(config)).not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should warn when fast mode is set on codex entries', () => {
|
|
135
|
+
const validated = validateConfig({
|
|
136
|
+
models: {
|
|
137
|
+
execute: { model: 'gpt-5.4', harness: 'codex', fast: true },
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(collectConfigValidationWarnings(validated)).toEqual([
|
|
142
|
+
'models.execute.fast is enabled but ignored because Codex does not support fast mode',
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
104
146
|
it('should reject non-object config', () => {
|
|
105
147
|
expect(() => validateConfig(null)).toThrow(ConfigValidationError);
|
|
106
148
|
expect(() => validateConfig('string')).toThrow(ConfigValidationError);
|
|
@@ -118,46 +160,78 @@ describe('Config', () => {
|
|
|
118
160
|
});
|
|
119
161
|
|
|
120
162
|
it('should reject unknown model keys', () => {
|
|
121
|
-
expect(() => validateConfig({ models: { unknownScenario: 'opus' } })).toThrow('Unknown config key: models.unknownScenario');
|
|
163
|
+
expect(() => validateConfig({ models: { unknownScenario: { model: 'opus', harness: 'claude' } } })).toThrow('Unknown config key: models.unknownScenario');
|
|
122
164
|
});
|
|
123
165
|
|
|
124
166
|
it('should reject unknown effortMapping keys', () => {
|
|
125
|
-
expect(() => validateConfig({ effortMapping: { unknownLevel: 'haiku' } })).toThrow('Unknown config key: effortMapping.unknownLevel');
|
|
167
|
+
expect(() => validateConfig({ effortMapping: { unknownLevel: { model: 'haiku', harness: 'claude' } } })).toThrow('Unknown config key: effortMapping.unknownLevel');
|
|
126
168
|
});
|
|
127
169
|
|
|
128
170
|
it('should reject unknown commitFormat keys', () => {
|
|
129
171
|
expect(() => validateConfig({ commitFormat: { unknownKey: 'val' } })).toThrow('Unknown config key: commitFormat.unknownKey');
|
|
130
172
|
});
|
|
131
173
|
|
|
132
|
-
//
|
|
133
|
-
it('should
|
|
134
|
-
expect(() => validateConfig({
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
// Removed legacy keys
|
|
175
|
+
it('should reject removed provider key with helpful message', () => {
|
|
176
|
+
expect(() => validateConfig({ provider: 'claude' })).toThrow('Top-level "provider" has been removed');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should reject removed codexModels key with helpful message', () => {
|
|
180
|
+
expect(() => validateConfig({ codexModels: { plan: 'gpt-5.4' } })).toThrow('"codexModels" has been removed');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should reject removed codexEffortMapping key with helpful message', () => {
|
|
184
|
+
expect(() => validateConfig({ codexEffortMapping: { low: 'gpt-5.4' } })).toThrow('"codexEffortMapping" has been removed');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Model entry validation
|
|
188
|
+
it('should reject string model values (old schema)', () => {
|
|
189
|
+
expect(() => validateConfig({ models: { plan: 'opus' } })).toThrow('must be a model entry object');
|
|
137
190
|
});
|
|
138
191
|
|
|
139
|
-
it('should
|
|
140
|
-
expect(() => validateConfig({ models: { plan: 'claude
|
|
141
|
-
expect(() => validateConfig({ models: { plan: 'claude-opus-4' } })).not.toThrow();
|
|
192
|
+
it('should reject model entries missing model field', () => {
|
|
193
|
+
expect(() => validateConfig({ models: { plan: { harness: 'claude' } } })).toThrow('models.plan.model is required');
|
|
142
194
|
});
|
|
143
195
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(() => validateConfig({ models: { plan: 'gpt-4' } })).toThrow('models.plan must be');
|
|
196
|
+
it('should reject model entries missing harness field', () => {
|
|
197
|
+
expect(() => validateConfig({ models: { plan: { model: 'opus' } } })).toThrow('models.plan.harness is required');
|
|
147
198
|
});
|
|
148
199
|
|
|
149
|
-
it('should reject
|
|
150
|
-
expect(() => validateConfig({ models: { plan: '
|
|
151
|
-
expect(() => validateConfig({ models: { plan: 'not-a-model' } })).toThrow('models.plan must be');
|
|
200
|
+
it('should reject invalid model names in model entries', () => {
|
|
201
|
+
expect(() => validateConfig({ models: { plan: { model: 'invalid', harness: 'claude' } } })).toThrow('models.plan.model must be a valid model name');
|
|
152
202
|
});
|
|
153
203
|
|
|
154
|
-
it('should reject
|
|
155
|
-
expect(() => validateConfig({ models: { plan:
|
|
204
|
+
it('should reject invalid harness in model entries', () => {
|
|
205
|
+
expect(() => validateConfig({ models: { plan: { model: 'opus', harness: 'openai' } } })).toThrow('models.plan.harness must be one of');
|
|
156
206
|
});
|
|
157
207
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
208
|
+
it('should reject invalid reasoningEffort in model entries', () => {
|
|
209
|
+
expect(() => validateConfig({ models: { plan: { model: 'opus', harness: 'claude', reasoningEffort: 'ultra' } } })).toThrow('models.plan.reasoningEffort must be one of');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should reject unknown keys in model entries', () => {
|
|
213
|
+
expect(() => validateConfig({ models: { plan: { model: 'opus', harness: 'claude', unknown: true } } })).toThrow('Unknown config key: models.plan.unknown');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Valid full model IDs in entries
|
|
217
|
+
it('should accept full model IDs in entries', () => {
|
|
218
|
+
expect(() => validateConfig({ models: { plan: { model: 'claude-opus-4-5-20251101', harness: 'claude' } } })).not.toThrow();
|
|
219
|
+
expect(() => validateConfig({ models: { execute: { model: 'gpt-5.4', harness: 'codex' } } })).not.toThrow();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// effortMapping validation
|
|
223
|
+
it('should reject string effortMapping values (old schema)', () => {
|
|
224
|
+
expect(() => validateConfig({ effortMapping: { low: 'sonnet' } })).toThrow('must be a model entry object');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should accept valid effortMapping model entries', () => {
|
|
228
|
+
expect(() => validateConfig({
|
|
229
|
+
effortMapping: {
|
|
230
|
+
low: { model: 'sonnet', harness: 'claude' },
|
|
231
|
+
medium: { model: 'opus', harness: 'claude' },
|
|
232
|
+
high: { model: 'gpt-5.4', harness: 'codex' },
|
|
233
|
+
},
|
|
234
|
+
})).not.toThrow();
|
|
161
235
|
});
|
|
162
236
|
|
|
163
237
|
// Invalid types for nested objects
|
|
@@ -242,6 +316,13 @@ describe('Config', () => {
|
|
|
242
316
|
expect(isValidModelName('claude-opus-4')).toBe(true);
|
|
243
317
|
});
|
|
244
318
|
|
|
319
|
+
it('should accept Codex model names', () => {
|
|
320
|
+
expect(isValidModelName('gpt-5.4')).toBe(true);
|
|
321
|
+
expect(isValidModelName('gpt-5.3-codex')).toBe(true);
|
|
322
|
+
expect(isValidModelName('codex')).toBe(true);
|
|
323
|
+
expect(isValidModelName('gpt54')).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
245
326
|
it('should reject invalid strings', () => {
|
|
246
327
|
expect(isValidModelName('gpt-4')).toBe(false);
|
|
247
328
|
expect(isValidModelName('random-string')).toBe(false);
|
|
@@ -260,22 +341,27 @@ describe('Config', () => {
|
|
|
260
341
|
|
|
261
342
|
it('should deep-merge partial models override', () => {
|
|
262
343
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
263
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
344
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
345
|
+
models: { plan: { model: 'haiku', harness: 'claude' } },
|
|
346
|
+
}));
|
|
264
347
|
|
|
265
348
|
const config = resolveConfig(configPath);
|
|
266
|
-
expect(config.models.plan).toBe('haiku');
|
|
267
|
-
expect(config.models.
|
|
268
|
-
expect(config.models.
|
|
349
|
+
expect(config.models.plan.model).toBe('haiku');
|
|
350
|
+
expect(config.models.plan.harness).toBe('claude');
|
|
351
|
+
expect(config.models.execute.model).toBe('opus'); // default preserved
|
|
352
|
+
expect(config.models.failureAnalysis.model).toBe('haiku'); // default preserved
|
|
269
353
|
});
|
|
270
354
|
|
|
271
355
|
it('should deep-merge partial effortMapping override', () => {
|
|
272
356
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
273
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
357
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
358
|
+
effortMapping: { medium: { model: 'opus', harness: 'claude' } },
|
|
359
|
+
}));
|
|
274
360
|
|
|
275
361
|
const config = resolveConfig(configPath);
|
|
276
|
-
expect(config.effortMapping.medium).toBe('opus');
|
|
277
|
-
expect(config.effortMapping.low).toBe('sonnet'); // default preserved
|
|
278
|
-
expect(config.effortMapping.high).toBe('opus'); // default preserved
|
|
362
|
+
expect(config.effortMapping.medium.model).toBe('opus');
|
|
363
|
+
expect(config.effortMapping.low.model).toBe('sonnet'); // default preserved
|
|
364
|
+
expect(config.effortMapping.high.model).toBe('opus'); // default preserved
|
|
279
365
|
});
|
|
280
366
|
|
|
281
367
|
it('should deep-merge partial commitFormat override', () => {
|
|
@@ -318,21 +404,53 @@ describe('Config', () => {
|
|
|
318
404
|
expect(() => resolveConfig(configPath)).toThrow(ConfigValidationError);
|
|
319
405
|
});
|
|
320
406
|
|
|
321
|
-
it('should
|
|
407
|
+
it('should not mutate DEFAULT_CONFIG', () => {
|
|
408
|
+
const configPath = path.join(tempDir, 'raf.config.json');
|
|
409
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
410
|
+
models: { plan: { model: 'haiku', harness: 'claude' } },
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
resolveConfig(configPath);
|
|
414
|
+
expect(DEFAULT_CONFIG.models.plan.model).toBe('opus');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should support Codex model entries in config', () => {
|
|
322
418
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
323
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
419
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
420
|
+
models: {
|
|
421
|
+
execute: { model: 'gpt-5.4', harness: 'codex' },
|
|
422
|
+
},
|
|
423
|
+
effortMapping: {
|
|
424
|
+
high: { model: 'gpt-5.4', harness: 'codex' },
|
|
425
|
+
},
|
|
426
|
+
}));
|
|
324
427
|
|
|
325
428
|
const config = resolveConfig(configPath);
|
|
326
|
-
expect(config.models.
|
|
327
|
-
expect(config.models.execute).toBe('
|
|
429
|
+
expect(config.models.execute.model).toBe('gpt-5.4');
|
|
430
|
+
expect(config.models.execute.harness).toBe('codex');
|
|
431
|
+
expect(config.effortMapping.high.model).toBe('gpt-5.4');
|
|
432
|
+
expect(config.effortMapping.high.harness).toBe('codex');
|
|
433
|
+
// Claude defaults preserved for unoverridden entries
|
|
434
|
+
expect(config.models.plan.harness).toBe('claude');
|
|
328
435
|
});
|
|
329
436
|
|
|
330
|
-
it('should
|
|
437
|
+
it('should warn and ignore fast mode on codex entries', () => {
|
|
331
438
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
332
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
439
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
440
|
+
models: {
|
|
441
|
+
execute: { model: 'gpt-5.4', harness: 'codex', fast: true },
|
|
442
|
+
},
|
|
443
|
+
}));
|
|
333
444
|
|
|
334
|
-
|
|
335
|
-
|
|
445
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
446
|
+
const config = resolveConfig(configPath);
|
|
447
|
+
|
|
448
|
+
expect(config.models.execute.fast).toBeUndefined();
|
|
449
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
450
|
+
expect.stringContaining('models.execute.fast is enabled but ignored because Codex does not support fast mode')
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
warnSpy.mockRestore();
|
|
336
454
|
});
|
|
337
455
|
});
|
|
338
456
|
|
|
@@ -354,26 +472,30 @@ describe('Config', () => {
|
|
|
354
472
|
});
|
|
355
473
|
|
|
356
474
|
describe('helper accessors', () => {
|
|
357
|
-
it('getModel returns correct model for scenario', () => {
|
|
475
|
+
it('getModel returns correct model entry for scenario', () => {
|
|
358
476
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
359
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
360
|
-
|
|
477
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
478
|
+
models: { plan: { model: 'haiku', harness: 'claude' } },
|
|
479
|
+
}));
|
|
361
480
|
const config = resolveConfig(configPath);
|
|
362
|
-
expect(config.models.plan).toBe('haiku');
|
|
363
|
-
expect(config.models.
|
|
481
|
+
expect(config.models.plan.model).toBe('haiku');
|
|
482
|
+
expect(config.models.plan.harness).toBe('claude');
|
|
483
|
+
expect(config.models.execute.model).toBe('opus');
|
|
364
484
|
});
|
|
365
485
|
|
|
366
486
|
it('effortMapping resolves correctly from config', () => {
|
|
367
487
|
const configPath = path.join(tempDir, 'raf.config.json');
|
|
368
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
488
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
489
|
+
effortMapping: { high: { model: 'sonnet', harness: 'claude' } },
|
|
490
|
+
}));
|
|
369
491
|
const config = resolveConfig(configPath);
|
|
370
|
-
expect(config.effortMapping.high).toBe('sonnet');
|
|
371
|
-
expect(config.effortMapping.low).toBe('sonnet'); // default preserved
|
|
492
|
+
expect(config.effortMapping.high.model).toBe('sonnet');
|
|
493
|
+
expect(config.effortMapping.low.model).toBe('sonnet'); // default preserved
|
|
372
494
|
});
|
|
373
495
|
|
|
374
496
|
it('getCommitFormat returns correct format', () => {
|
|
375
497
|
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
376
|
-
expect(config.commitFormat.task).toBe('{prefix}[{
|
|
498
|
+
expect(config.commitFormat.task).toBe('{prefix}[{projectName}:{taskId}] {description}');
|
|
377
499
|
});
|
|
378
500
|
|
|
379
501
|
it('getCommitPrefix returns prefix', () => {
|
|
@@ -391,19 +513,19 @@ describe('Config', () => {
|
|
|
391
513
|
});
|
|
392
514
|
|
|
393
515
|
describe('DEFAULT_CONFIG', () => {
|
|
394
|
-
it('should have all model scenarios defined', () => {
|
|
395
|
-
expect(DEFAULT_CONFIG.models.plan).
|
|
396
|
-
expect(DEFAULT_CONFIG.models.execute).
|
|
397
|
-
expect(DEFAULT_CONFIG.models.nameGeneration).
|
|
398
|
-
expect(DEFAULT_CONFIG.models.failureAnalysis).
|
|
399
|
-
expect(DEFAULT_CONFIG.models.prGeneration).
|
|
400
|
-
expect(DEFAULT_CONFIG.models.config).
|
|
516
|
+
it('should have all model scenarios defined as ModelEntry objects', () => {
|
|
517
|
+
expect(DEFAULT_CONFIG.models.plan).toEqual({ model: 'opus', harness: 'claude' });
|
|
518
|
+
expect(DEFAULT_CONFIG.models.execute).toEqual({ model: 'opus', harness: 'claude' });
|
|
519
|
+
expect(DEFAULT_CONFIG.models.nameGeneration).toEqual({ model: 'sonnet', harness: 'claude' });
|
|
520
|
+
expect(DEFAULT_CONFIG.models.failureAnalysis).toEqual({ model: 'haiku', harness: 'claude' });
|
|
521
|
+
expect(DEFAULT_CONFIG.models.prGeneration).toEqual({ model: 'sonnet', harness: 'claude' });
|
|
522
|
+
expect(DEFAULT_CONFIG.models.config).toEqual({ model: 'sonnet', harness: 'claude' });
|
|
401
523
|
});
|
|
402
524
|
|
|
403
|
-
it('should have all effortMapping levels defined', () => {
|
|
404
|
-
expect(DEFAULT_CONFIG.effortMapping.low).
|
|
405
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).
|
|
406
|
-
expect(DEFAULT_CONFIG.effortMapping.high).
|
|
525
|
+
it('should have all effortMapping levels defined as ModelEntry objects', () => {
|
|
526
|
+
expect(DEFAULT_CONFIG.effortMapping.low).toEqual({ model: 'sonnet', harness: 'claude' });
|
|
527
|
+
expect(DEFAULT_CONFIG.effortMapping.medium).toEqual({ model: 'opus', harness: 'claude' });
|
|
528
|
+
expect(DEFAULT_CONFIG.effortMapping.high).toEqual({ model: 'opus', harness: 'claude' });
|
|
407
529
|
});
|
|
408
530
|
|
|
409
531
|
it('should have all commit format fields defined', () => {
|
|
@@ -412,6 +534,12 @@ describe('Config', () => {
|
|
|
412
534
|
expect(DEFAULT_CONFIG.commitFormat.amend).toContain('{prefix}');
|
|
413
535
|
expect(DEFAULT_CONFIG.commitFormat.prefix).toBe('RAF');
|
|
414
536
|
});
|
|
537
|
+
|
|
538
|
+
it('should not have harness, codexModels, or codexEffortMapping fields', () => {
|
|
539
|
+
expect('provider' in DEFAULT_CONFIG).toBe(false);
|
|
540
|
+
expect('codexModels' in DEFAULT_CONFIG).toBe(false);
|
|
541
|
+
expect('codexEffortMapping' in DEFAULT_CONFIG).toBe(false);
|
|
542
|
+
});
|
|
415
543
|
});
|
|
416
544
|
|
|
417
545
|
describe('renderCommitMessage', () => {
|
|
@@ -433,29 +561,44 @@ describe('Config', () => {
|
|
|
433
561
|
it('should handle plan commit format', () => {
|
|
434
562
|
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.plan, {
|
|
435
563
|
prefix: 'RAF',
|
|
436
|
-
projectId: '
|
|
564
|
+
projectId: 'my-project',
|
|
437
565
|
projectName: 'my-project',
|
|
566
|
+
description: 'add auth system',
|
|
438
567
|
});
|
|
439
|
-
expect(result).toBe('RAF[
|
|
568
|
+
expect(result).toBe('RAF[my-project] Plan: add auth system');
|
|
440
569
|
});
|
|
441
570
|
|
|
442
571
|
it('should handle amend commit format', () => {
|
|
443
572
|
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.amend, {
|
|
444
573
|
prefix: 'RAF',
|
|
445
|
-
projectId: '
|
|
574
|
+
projectId: 'my-project',
|
|
446
575
|
projectName: 'my-project',
|
|
576
|
+
description: 'fix-login, add-tests',
|
|
447
577
|
});
|
|
448
|
-
expect(result).toBe('RAF[
|
|
578
|
+
expect(result).toBe('RAF[my-project] Amend: fix-login, add-tests');
|
|
449
579
|
});
|
|
450
580
|
|
|
451
581
|
it('should handle task commit format', () => {
|
|
452
582
|
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.task, {
|
|
453
583
|
prefix: 'RAF',
|
|
454
|
-
projectId: '
|
|
584
|
+
projectId: 'my-project',
|
|
585
|
+
projectName: 'my-project',
|
|
455
586
|
taskId: '10',
|
|
456
587
|
description: 'Fix bug',
|
|
457
588
|
});
|
|
458
|
-
expect(result).toBe('RAF[
|
|
589
|
+
expect(result).toBe('RAF[my-project:10] Fix bug');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should support {projectId} as backwards compat alias for projectName', () => {
|
|
593
|
+
// Old-style template with {projectId} should still work
|
|
594
|
+
const result = renderCommitMessage('{prefix}[{projectId}:{taskId}] {description}', {
|
|
595
|
+
prefix: 'RAF',
|
|
596
|
+
projectId: 'my-project',
|
|
597
|
+
projectName: 'my-project',
|
|
598
|
+
taskId: '01',
|
|
599
|
+
description: 'Add feature',
|
|
600
|
+
});
|
|
601
|
+
expect(result).toBe('RAF[my-project:01] Add feature');
|
|
459
602
|
});
|
|
460
603
|
|
|
461
604
|
it('should handle empty variables gracefully', () => {
|
|
@@ -474,67 +617,6 @@ describe('Config', () => {
|
|
|
474
617
|
});
|
|
475
618
|
});
|
|
476
619
|
|
|
477
|
-
describe('config integration - defaults match previous hardcoded values', () => {
|
|
478
|
-
it('should default models to match previous hardcoded values', () => {
|
|
479
|
-
expect(DEFAULT_CONFIG.models.execute).toBe('opus');
|
|
480
|
-
expect(DEFAULT_CONFIG.models.plan).toBe('opus');
|
|
481
|
-
expect(DEFAULT_CONFIG.models.nameGeneration).toBe('sonnet');
|
|
482
|
-
expect(DEFAULT_CONFIG.models.failureAnalysis).toBe('haiku');
|
|
483
|
-
expect(DEFAULT_CONFIG.models.prGeneration).toBe('sonnet');
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('should default effortMapping to sonnet/opus/opus', () => {
|
|
487
|
-
expect(DEFAULT_CONFIG.effortMapping.low).toBe('sonnet');
|
|
488
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('opus');
|
|
489
|
-
expect(DEFAULT_CONFIG.effortMapping.high).toBe('opus');
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('should default timeout to 60', () => {
|
|
493
|
-
expect(DEFAULT_CONFIG.timeout).toBe(60);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it('should default maxRetries to 3', () => {
|
|
497
|
-
expect(DEFAULT_CONFIG.maxRetries).toBe(3);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('should default autoCommit to true', () => {
|
|
501
|
-
expect(DEFAULT_CONFIG.autoCommit).toBe(true);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
it('should default worktree to false', () => {
|
|
505
|
-
expect(DEFAULT_CONFIG.worktree).toBe(false);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('should default commit format to match previous hardcoded format', () => {
|
|
509
|
-
// The task format should produce the same output as the old hardcoded format
|
|
510
|
-
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.task, {
|
|
511
|
-
prefix: DEFAULT_CONFIG.commitFormat.prefix,
|
|
512
|
-
projectId: '3',
|
|
513
|
-
taskId: '01',
|
|
514
|
-
description: 'Add feature',
|
|
515
|
-
});
|
|
516
|
-
expect(result).toBe('RAF[3:01] Add feature');
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it('should default plan commit format to match previous hardcoded format', () => {
|
|
520
|
-
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.plan, {
|
|
521
|
-
prefix: DEFAULT_CONFIG.commitFormat.prefix,
|
|
522
|
-
projectId: '3',
|
|
523
|
-
projectName: 'my-project',
|
|
524
|
-
});
|
|
525
|
-
expect(result).toBe('RAF[3] Plan: my-project');
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
it('should default amend commit format to match previous hardcoded format', () => {
|
|
529
|
-
const result = renderCommitMessage(DEFAULT_CONFIG.commitFormat.amend, {
|
|
530
|
-
prefix: DEFAULT_CONFIG.commitFormat.prefix,
|
|
531
|
-
projectId: '3',
|
|
532
|
-
projectName: 'my-project',
|
|
533
|
-
});
|
|
534
|
-
expect(result).toBe('RAF[3] Amend: my-project');
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
620
|
describe('getModelShortName', () => {
|
|
539
621
|
it('should return short aliases as-is', () => {
|
|
540
622
|
expect(getModelShortName('opus')).toBe('opus');
|
|
@@ -558,6 +640,36 @@ describe('Config', () => {
|
|
|
558
640
|
});
|
|
559
641
|
});
|
|
560
642
|
|
|
643
|
+
describe('getModelDisplayName', () => {
|
|
644
|
+
it('should preserve concise Claude aliases', () => {
|
|
645
|
+
expect(getModelDisplayName('opus')).toBe('opus');
|
|
646
|
+
expect(getModelDisplayName('claude-sonnet-4-5-20250929')).toBe('sonnet');
|
|
647
|
+
expect(getModelDisplayName('claude-haiku-4-5-20251001')).toBe('haiku');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should normalize compact Codex aliases to canonical display names', () => {
|
|
651
|
+
expect(getModelDisplayName('gpt54')).toBe('gpt-5.4');
|
|
652
|
+
expect(getModelDisplayName('gpt-5.4')).toBe('gpt-5.4');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should keep readable Codex aliases that are already concise labels', () => {
|
|
656
|
+
expect(getModelDisplayName('codex')).toBe('codex');
|
|
657
|
+
expect(getModelDisplayName('gpt-5.3-codex')).toBe('codex');
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe('formatModelDisplay', () => {
|
|
662
|
+
it('should include harness when requested', () => {
|
|
663
|
+
expect(formatModelDisplay('gpt54', 'codex', { includeHarness: true })).toBe('gpt-5.4 (codex)');
|
|
664
|
+
expect(formatModelDisplay('claude-sonnet-4-5-20250929', 'claude', { includeHarness: true })).toBe('sonnet (claude)');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should allow explicit full model ID display', () => {
|
|
668
|
+
expect(formatModelDisplay('gpt54', 'codex', { fullId: true })).toBe('gpt-5.4');
|
|
669
|
+
expect(formatModelDisplay('sonnet', 'claude', { fullId: true })).toBe('claude-sonnet-4-5-20250929');
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
561
673
|
describe('resolveFullModelId', () => {
|
|
562
674
|
it('should resolve short aliases to full model IDs', () => {
|
|
563
675
|
expect(resolveFullModelId('opus')).toBe('claude-opus-4-6');
|
|
@@ -578,36 +690,65 @@ describe('Config', () => {
|
|
|
578
690
|
});
|
|
579
691
|
});
|
|
580
692
|
|
|
581
|
-
describe('
|
|
582
|
-
it('should
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
expect(config.models.execute).toBe('sonnet');
|
|
587
|
-
expect(config.models.plan).toBe('haiku');
|
|
588
|
-
// Others should remain at defaults
|
|
589
|
-
expect(config.models.nameGeneration).toBe('sonnet');
|
|
590
|
-
expect(config.models.failureAnalysis).toBe('haiku');
|
|
693
|
+
describe('getModelTier', () => {
|
|
694
|
+
it('should return correct tier for short aliases', () => {
|
|
695
|
+
expect(getModelTier('haiku')).toBe(1);
|
|
696
|
+
expect(getModelTier('sonnet')).toBe(2);
|
|
697
|
+
expect(getModelTier('opus')).toBe(3);
|
|
591
698
|
});
|
|
592
699
|
|
|
593
|
-
it('should
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
expect(config.effortMapping.high).toBe('sonnet');
|
|
598
|
-
// Others should remain at defaults
|
|
599
|
-
expect(config.effortMapping.low).toBe('sonnet');
|
|
600
|
-
expect(config.effortMapping.medium).toBe('opus');
|
|
700
|
+
it('should extract tier from full model IDs', () => {
|
|
701
|
+
expect(getModelTier('claude-haiku-4-5-20251001')).toBe(1);
|
|
702
|
+
expect(getModelTier('claude-sonnet-4-5-20250929')).toBe(2);
|
|
703
|
+
expect(getModelTier('claude-opus-4-6')).toBe(3);
|
|
601
704
|
});
|
|
602
705
|
|
|
603
|
-
it('should
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
expect(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
706
|
+
it('should return correct tiers for Codex models', () => {
|
|
707
|
+
expect(getModelTier('codex')).toBe(1);
|
|
708
|
+
expect(getModelTier('gpt-5.3-codex')).toBe(1);
|
|
709
|
+
expect(getModelTier('gpt54')).toBe(2);
|
|
710
|
+
expect(getModelTier('gpt-5.4')).toBe(2);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('should return highest tier for unknown models', () => {
|
|
714
|
+
expect(getModelTier('unknown-model')).toBe(3);
|
|
715
|
+
expect(getModelTier('claude-future-5-0')).toBe(3);
|
|
716
|
+
expect(getModelTier('')).toBe(3);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe('applyModelCeiling', () => {
|
|
721
|
+
it('should return resolved entry when below ceiling', () => {
|
|
722
|
+
const resolved = { model: 'haiku', harness: 'claude' as const };
|
|
723
|
+
const ceiling = { model: 'sonnet', harness: 'claude' as const };
|
|
724
|
+
expect(applyModelCeiling(resolved, ceiling)).toEqual(resolved);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should return ceiling entry when above ceiling', () => {
|
|
728
|
+
const resolved = { model: 'opus', harness: 'claude' as const };
|
|
729
|
+
const ceiling = { model: 'sonnet', harness: 'claude' as const };
|
|
730
|
+
expect(applyModelCeiling(resolved, ceiling)).toEqual(ceiling);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('should return resolved entry when at ceiling', () => {
|
|
734
|
+
const resolved = { model: 'sonnet', harness: 'claude' as const };
|
|
735
|
+
const ceiling = { model: 'sonnet', harness: 'claude' as const };
|
|
736
|
+
expect(applyModelCeiling(resolved, ceiling)).toEqual(resolved);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should work with full model IDs', () => {
|
|
740
|
+
const resolved = { model: 'claude-opus-4-6', harness: 'claude' as const };
|
|
741
|
+
const ceiling = { model: 'sonnet', harness: 'claude' as const };
|
|
742
|
+
expect(applyModelCeiling(resolved, ceiling)).toEqual(ceiling);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
describe('resolveEffortToModel', () => {
|
|
747
|
+
it('should resolve effort levels to default model entries', () => {
|
|
748
|
+
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
749
|
+
expect(config.effortMapping.low).toEqual({ model: 'sonnet', harness: 'claude' });
|
|
750
|
+
expect(config.effortMapping.medium).toEqual({ model: 'opus', harness: 'claude' });
|
|
751
|
+
expect(config.effortMapping.high).toEqual({ model: 'opus', harness: 'claude' });
|
|
611
752
|
});
|
|
612
753
|
});
|
|
613
754
|
|
|
@@ -662,94 +803,37 @@ describe('Config', () => {
|
|
|
662
803
|
});
|
|
663
804
|
});
|
|
664
805
|
|
|
665
|
-
describe('
|
|
666
|
-
it('should
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
expect(getModelTier('claude-opus-4-6')).toBe(3);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
it('should return highest tier for unknown models', () => {
|
|
679
|
-
expect(getModelTier('unknown-model')).toBe(3);
|
|
680
|
-
expect(getModelTier('claude-future-5-0')).toBe(3);
|
|
681
|
-
expect(getModelTier('')).toBe(3);
|
|
682
|
-
});
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
describe('applyModelCeiling', () => {
|
|
686
|
-
it('should return resolved model when below ceiling', () => {
|
|
687
|
-
expect(applyModelCeiling('haiku', 'sonnet')).toBe('haiku');
|
|
688
|
-
expect(applyModelCeiling('haiku', 'opus')).toBe('haiku');
|
|
689
|
-
expect(applyModelCeiling('sonnet', 'opus')).toBe('sonnet');
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it('should return ceiling model when above ceiling', () => {
|
|
693
|
-
expect(applyModelCeiling('opus', 'sonnet')).toBe('sonnet');
|
|
694
|
-
expect(applyModelCeiling('opus', 'haiku')).toBe('haiku');
|
|
695
|
-
expect(applyModelCeiling('sonnet', 'haiku')).toBe('haiku');
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
it('should return resolved model when at ceiling', () => {
|
|
699
|
-
expect(applyModelCeiling('sonnet', 'sonnet')).toBe('sonnet');
|
|
700
|
-
expect(applyModelCeiling('opus', 'opus')).toBe('opus');
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
it('should work with full model IDs', () => {
|
|
704
|
-
expect(applyModelCeiling('claude-opus-4-6', 'sonnet')).toBe('sonnet');
|
|
705
|
-
expect(applyModelCeiling('claude-haiku-4-5-20251001', 'claude-opus-4-6')).toBe('claude-haiku-4-5-20251001');
|
|
706
|
-
});
|
|
707
|
-
});
|
|
806
|
+
describe('new schema - harness-aware resolution', () => {
|
|
807
|
+
it('should allow mixing Claude and Codex entries across scenarios', () => {
|
|
808
|
+
const configPath = path.join(tempDir, 'mixed.json');
|
|
809
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
810
|
+
models: {
|
|
811
|
+
plan: { model: 'opus', harness: 'claude' },
|
|
812
|
+
execute: { model: 'gpt-5.4', harness: 'codex' },
|
|
813
|
+
nameGeneration: { model: 'sonnet', harness: 'claude' },
|
|
814
|
+
},
|
|
815
|
+
}));
|
|
708
816
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const config = resolveConfig(path.join(tempDir, 'nonexistent.json'));
|
|
714
|
-
expect(config.effortMapping.low).toBe('sonnet');
|
|
715
|
-
expect(config.effortMapping.medium).toBe('opus');
|
|
716
|
-
expect(config.effortMapping.high).toBe('opus');
|
|
817
|
+
const config = resolveConfig(configPath);
|
|
818
|
+
expect(config.models.plan.harness).toBe('claude');
|
|
819
|
+
expect(config.models.execute.harness).toBe('codex');
|
|
820
|
+
expect(config.models.nameGeneration.harness).toBe('claude');
|
|
717
821
|
});
|
|
718
|
-
});
|
|
719
822
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
823
|
+
it('should allow mixing harnesses in effortMapping', () => {
|
|
824
|
+
const configPath = path.join(tempDir, 'mixed-effort.json');
|
|
825
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
723
826
|
effortMapping: {
|
|
724
|
-
low: 'sonnet',
|
|
725
|
-
|
|
726
|
-
high: 'opus',
|
|
827
|
+
low: { model: 'sonnet', harness: 'claude' },
|
|
828
|
+
high: { model: 'gpt-5.4', harness: 'codex' },
|
|
727
829
|
},
|
|
728
|
-
}))
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
it('should accept partial effortMapping override', () => {
|
|
732
|
-
expect(() => validateConfig({
|
|
733
|
-
effortMapping: { high: 'sonnet' },
|
|
734
|
-
})).not.toThrow();
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it('should accept full model IDs in effortMapping', () => {
|
|
738
|
-
expect(() => validateConfig({
|
|
739
|
-
effortMapping: { low: 'claude-haiku-4-5-20251001' },
|
|
740
|
-
})).not.toThrow();
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
it('should reject invalid model names in effortMapping', () => {
|
|
744
|
-
expect(() => validateConfig({
|
|
745
|
-
effortMapping: { low: 'gpt-4' },
|
|
746
|
-
})).toThrow('effortMapping.low must be a short alias');
|
|
747
|
-
});
|
|
830
|
+
}));
|
|
748
831
|
|
|
749
|
-
|
|
750
|
-
expect(()
|
|
751
|
-
|
|
752
|
-
|
|
832
|
+
const config = resolveConfig(configPath);
|
|
833
|
+
expect(config.effortMapping.low.harness).toBe('claude');
|
|
834
|
+
expect(config.effortMapping.high.harness).toBe('codex');
|
|
835
|
+
// Medium preserved from defaults
|
|
836
|
+
expect(config.effortMapping.medium.harness).toBe('claude');
|
|
753
837
|
});
|
|
754
838
|
});
|
|
755
839
|
});
|