rafcode 3.2.1 → 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/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 +47 -57
- 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 +57 -44
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +36 -153
- 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/pull-request.js +3 -3
- 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/parsers/codex-stream-renderer.d.ts +7 -0
- package/dist/parsers/codex-stream-renderer.d.ts.map +1 -1
- package/dist/parsers/codex-stream-renderer.js +37 -4
- package/dist/parsers/codex-stream-renderer.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +29 -101
- 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 +21 -120
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +33 -31
- 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/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 +1 -1
- package/src/commands/config.ts +60 -63
- package/src/commands/do.ts +63 -51
- package/src/commands/plan.ts +34 -165
- 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/pull-request.ts +3 -3
- 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/parsers/codex-stream-renderer.ts +47 -4
- package/src/prompts/amend.ts +29 -101
- package/src/prompts/config-docs.md +206 -62
- package/src/prompts/execution.ts +17 -34
- package/src/prompts/planning.ts +21 -120
- package/src/types/config.ts +47 -58
- package/src/utils/config.ts +248 -115
- package/src/utils/name-generator.ts +29 -13
- 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,403 +1,315 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
3
|
import { Command } from 'commander';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
import { jest } from '@jest/globals';
|
|
5
|
+
|
|
6
|
+
const mockRunInteractive = jest.fn<() => Promise<number>>();
|
|
7
|
+
const mockCreateRunner = jest.fn(() => ({
|
|
8
|
+
runInteractive: mockRunInteractive,
|
|
9
|
+
}));
|
|
10
|
+
const mockShutdownHandler = {
|
|
11
|
+
init: jest.fn(),
|
|
12
|
+
registerClaudeRunner: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
const mockLogger = {
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
success: jest.fn(),
|
|
17
|
+
warn: jest.fn(),
|
|
18
|
+
error: jest.fn(),
|
|
19
|
+
newline: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let confirmAnswer = 'y';
|
|
23
|
+
const suiteHomeDir = fs.mkdtempSync(path.join('/tmp', 'raf-config-home-'));
|
|
24
|
+
let mockHomeDir = suiteHomeDir;
|
|
25
|
+
|
|
26
|
+
jest.unstable_mockModule('node:os', () => ({
|
|
27
|
+
homedir: () => mockHomeDir,
|
|
28
|
+
tmpdir: () => '/tmp',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.unstable_mockModule('../../src/core/runner-factory.js', () => ({
|
|
32
|
+
createRunner: mockCreateRunner,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.unstable_mockModule('../../src/core/shutdown-handler.js', () => ({
|
|
36
|
+
shutdownHandler: mockShutdownHandler,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.unstable_mockModule('../../src/utils/logger.js', () => ({
|
|
40
|
+
logger: mockLogger,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.unstable_mockModule('node:readline', () => ({
|
|
44
|
+
createInterface: jest.fn(() => ({
|
|
45
|
+
question: (_message: string, callback: (answer: string) => void) => callback(confirmAnswer),
|
|
46
|
+
close: jest.fn(),
|
|
47
|
+
})),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const { createConfigCommand } = await import('../../src/commands/config.js');
|
|
51
|
+
const { resetConfigCache, resolveConfig, validateConfig, ConfigValidationError } = await import('../../src/utils/config.js');
|
|
52
|
+
const { DEFAULT_CONFIG } = await import('../../src/types/config.js');
|
|
14
53
|
|
|
15
54
|
describe('Config Command', () => {
|
|
16
55
|
let tempDir: string;
|
|
17
56
|
|
|
57
|
+
function configPath(): string {
|
|
58
|
+
return path.join(tempDir, '.raf', 'raf.config.json');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function parseConfigCommand(args: string[]): Promise<void> {
|
|
62
|
+
const command = createConfigCommand();
|
|
63
|
+
command.exitOverride();
|
|
64
|
+
await command.parseAsync(args, { from: 'user' });
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
beforeEach(() => {
|
|
19
|
-
tempDir =
|
|
68
|
+
tempDir = suiteHomeDir;
|
|
69
|
+
mockHomeDir = suiteHomeDir;
|
|
70
|
+
fs.rmSync(path.join(tempDir, '.raf'), { recursive: true, force: true });
|
|
71
|
+
confirmAnswer = 'y';
|
|
20
72
|
resetConfigCache();
|
|
73
|
+
mockRunInteractive.mockReset().mockResolvedValue(0);
|
|
74
|
+
mockCreateRunner.mockClear();
|
|
75
|
+
mockShutdownHandler.init.mockClear();
|
|
76
|
+
mockShutdownHandler.registerClaudeRunner.mockClear();
|
|
77
|
+
mockLogger.info.mockClear();
|
|
78
|
+
mockLogger.success.mockClear();
|
|
79
|
+
mockLogger.warn.mockClear();
|
|
80
|
+
mockLogger.error.mockClear();
|
|
81
|
+
mockLogger.newline.mockClear();
|
|
21
82
|
});
|
|
22
83
|
|
|
23
84
|
afterEach(() => {
|
|
24
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
85
|
+
fs.rmSync(path.join(tempDir, '.raf'), { recursive: true, force: true });
|
|
25
86
|
resetConfigCache();
|
|
26
87
|
});
|
|
27
88
|
|
|
89
|
+
afterAll(() => {
|
|
90
|
+
fs.rmSync(suiteHomeDir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
28
93
|
describe('Command setup', () => {
|
|
29
94
|
it('should create a command named "config"', () => {
|
|
30
95
|
const cmd = createConfigCommand();
|
|
31
96
|
expect(cmd.name()).toBe('config');
|
|
32
97
|
});
|
|
33
98
|
|
|
34
|
-
it('should
|
|
35
|
-
const cmd = createConfigCommand();
|
|
36
|
-
expect(cmd.description()).toBeTruthy();
|
|
37
|
-
expect(cmd.description()).toContain('config');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should accept a variadic prompt argument', () => {
|
|
41
|
-
const cmd = createConfigCommand();
|
|
42
|
-
const args = cmd.registeredArguments;
|
|
43
|
-
expect(args.length).toBe(1);
|
|
44
|
-
expect(args[0]!.variadic).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should have a --reset option', () => {
|
|
99
|
+
it('should expose get, set, reset, wizard, and preset subcommands', () => {
|
|
48
100
|
const cmd = createConfigCommand();
|
|
49
|
-
|
|
50
|
-
expect(resetOption).toBeDefined();
|
|
101
|
+
expect(cmd.commands.map((subcommand) => subcommand.name())).toEqual(['get', 'set', 'reset', 'wizard', 'preset']);
|
|
51
102
|
});
|
|
52
103
|
|
|
53
|
-
it('should
|
|
104
|
+
it('should not keep the old root-level flags or prompt argument', () => {
|
|
54
105
|
const cmd = createConfigCommand();
|
|
55
|
-
|
|
56
|
-
expect(
|
|
106
|
+
expect(cmd.options).toHaveLength(0);
|
|
107
|
+
expect(cmd.registeredArguments).toHaveLength(0);
|
|
57
108
|
});
|
|
58
109
|
|
|
59
|
-
it('should
|
|
110
|
+
it('should define wizard with a variadic prompt argument', () => {
|
|
60
111
|
const cmd = createConfigCommand();
|
|
61
|
-
const
|
|
62
|
-
expect(
|
|
112
|
+
const wizard = cmd.commands.find((subcommand) => subcommand.name() === 'wizard');
|
|
113
|
+
expect(wizard).toBeDefined();
|
|
114
|
+
expect(wizard!.registeredArguments).toHaveLength(1);
|
|
115
|
+
expect(wizard!.registeredArguments[0]!.variadic).toBe(true);
|
|
63
116
|
});
|
|
64
117
|
|
|
65
118
|
it('should register in a parent program', () => {
|
|
66
119
|
const program = new Command();
|
|
67
120
|
program.addCommand(createConfigCommand());
|
|
68
|
-
const configCmd = program.commands.find((
|
|
121
|
+
const configCmd = program.commands.find((command) => command.name() === 'config');
|
|
69
122
|
expect(configCmd).toBeDefined();
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('Post-session validation logic', () => {
|
|
74
|
-
it('should accept valid config with model override', () => {
|
|
75
|
-
const config = { models: { execute: 'sonnet' } };
|
|
76
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should accept valid config with effortMapping override', () => {
|
|
80
|
-
const config = { effortMapping: { low: 'sonnet', medium: 'opus' } };
|
|
81
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should accept valid config with timeout', () => {
|
|
85
|
-
const config = { timeout: 120 };
|
|
86
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should reject config with unknown keys', () => {
|
|
90
|
-
const config = { unknownKey: true };
|
|
91
|
-
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should reject config with invalid model name', () => {
|
|
95
|
-
const config = { models: { execute: 'gpt-4' } };
|
|
96
|
-
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should reject config with invalid effortMapping model', () => {
|
|
100
|
-
const config = { effortMapping: { low: 'gpt-4' } };
|
|
101
|
-
expect(() => validateConfig(config)).toThrow(ConfigValidationError);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should reject non-object config', () => {
|
|
105
|
-
expect(() => validateConfig('string')).toThrow(ConfigValidationError);
|
|
106
|
-
expect(() => validateConfig(null)).toThrow(ConfigValidationError);
|
|
107
|
-
expect(() => validateConfig([])).toThrow(ConfigValidationError);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should accept an empty config (all defaults)', () => {
|
|
111
|
-
expect(() => validateConfig({})).not.toThrow();
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('Reset flow - file operations', () => {
|
|
116
|
-
it('should be able to delete config file', () => {
|
|
117
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
118
|
-
fs.writeFileSync(configPath, JSON.stringify({ timeout: 90 }, null, 2));
|
|
119
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
120
|
-
|
|
121
|
-
fs.unlinkSync(configPath);
|
|
122
|
-
expect(fs.existsSync(configPath)).toBe(false);
|
|
123
|
+
expect(program.commands.find((command) => command.name() === 'preset')).toBeUndefined();
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
it('should
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
it('should nest preset save/load/list/delete under config', () => {
|
|
127
|
+
const cmd = createConfigCommand();
|
|
128
|
+
const preset = cmd.commands.find((subcommand) => subcommand.name() === 'preset');
|
|
129
|
+
expect(preset).toBeDefined();
|
|
130
|
+
expect(preset!.commands.map((subcommand) => subcommand.name())).toEqual(['save', 'load', 'list', 'delete']);
|
|
129
131
|
});
|
|
130
132
|
});
|
|
131
133
|
|
|
132
|
-
describe('
|
|
133
|
-
it('
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
describe('config get', () => {
|
|
135
|
+
it('prints the resolved config when no key is provided', async () => {
|
|
136
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
137
|
+
fs.writeFileSync(configPath(), JSON.stringify({ timeout: 120 }, null, 2));
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
139
|
-
const parsed = JSON.parse(content);
|
|
139
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
expect(parsed.timeout).toBe(90);
|
|
143
|
-
expect(() => validateConfig(parsed)).not.toThrow();
|
|
144
|
-
});
|
|
141
|
+
await parseConfigCommand(['get']);
|
|
145
142
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
143
|
+
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
|
144
|
+
const printed = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
145
|
+
expect(printed.timeout).toBe(120);
|
|
146
|
+
expect(printed.models.execute).toEqual(DEFAULT_CONFIG.models.execute);
|
|
149
147
|
|
|
150
|
-
|
|
151
|
-
expect(() => JSON.parse(content)).toThrow(SyntaxError);
|
|
148
|
+
consoleSpy.mockRestore();
|
|
152
149
|
});
|
|
153
150
|
|
|
154
|
-
it('
|
|
155
|
-
|
|
156
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
157
|
-
|
|
158
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
159
|
-
const parsed = JSON.parse(content);
|
|
160
|
-
expect(() => validateConfig(parsed)).toThrow(ConfigValidationError);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
151
|
+
it('prints a resolved dot-notation value', async () => {
|
|
152
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
153
|
+
fs.writeFileSync(configPath(), JSON.stringify({ models: { plan: { model: 'haiku', harness: 'claude' } } }, null, 2));
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
it('should indicate no config when file does not exist', () => {
|
|
166
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
167
|
-
const exists = fs.existsSync(configPath);
|
|
168
|
-
const state = exists
|
|
169
|
-
? fs.readFileSync(configPath, 'utf-8')
|
|
170
|
-
: 'No config file exists yet.';
|
|
171
|
-
expect(state).toContain('No config file');
|
|
172
|
-
});
|
|
155
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
173
156
|
|
|
174
|
-
|
|
175
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
176
|
-
const config = { timeout: 120, worktree: true };
|
|
177
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
157
|
+
await parseConfigCommand(['get', 'models.plan.model']);
|
|
178
158
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
expect(content).toContain('"worktree": true');
|
|
159
|
+
expect(consoleSpy).toHaveBeenCalledWith('haiku');
|
|
160
|
+
consoleSpy.mockRestore();
|
|
182
161
|
});
|
|
183
162
|
});
|
|
184
163
|
|
|
185
|
-
describe('
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
it('should throw on invalid JSON when resolving config', () => {
|
|
190
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
191
|
-
fs.writeFileSync(configPath, '{ invalid json }}}');
|
|
192
|
-
|
|
193
|
-
expect(() => resolveConfig(configPath)).toThrow(SyntaxError);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should throw on schema validation failure when resolving config', () => {
|
|
197
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
198
|
-
fs.writeFileSync(configPath, JSON.stringify({ unknownKey: true }));
|
|
199
|
-
|
|
200
|
-
expect(() => resolveConfig(configPath)).toThrow(ConfigValidationError);
|
|
201
|
-
});
|
|
164
|
+
describe('config set', () => {
|
|
165
|
+
it('writes values with the existing parsing and validation behavior', async () => {
|
|
166
|
+
await parseConfigCommand(['set', 'timeout', '45']);
|
|
202
167
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
expect(
|
|
206
|
-
// effortMapping defaults used for per-task model resolution
|
|
207
|
-
expect(DEFAULT_CONFIG.effortMapping.medium).toBe('opus');
|
|
168
|
+
const saved = JSON.parse(fs.readFileSync(configPath(), 'utf-8'));
|
|
169
|
+
expect(saved).toEqual({ timeout: 45 });
|
|
170
|
+
expect(resolveConfig(configPath()).timeout).toBe(45);
|
|
208
171
|
});
|
|
209
172
|
|
|
210
|
-
it('
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
214
|
-
const invalidContent = '{ "broken": true, }'; // trailing comma = invalid
|
|
215
|
-
fs.writeFileSync(configPath, invalidContent);
|
|
173
|
+
it('prunes the config file when a value is reset to its default', async () => {
|
|
174
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
175
|
+
fs.writeFileSync(configPath(), JSON.stringify({ timeout: 45 }, null, 2));
|
|
216
176
|
|
|
217
|
-
|
|
218
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
219
|
-
expect(content).toBe(invalidContent);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should be able to read raw file contents even when config fails schema validation', () => {
|
|
223
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
224
|
-
const invalidContent = JSON.stringify({ badKey: 'value' }, null, 2);
|
|
225
|
-
fs.writeFileSync(configPath, invalidContent);
|
|
177
|
+
await parseConfigCommand(['set', 'timeout', String(DEFAULT_CONFIG.timeout)]);
|
|
226
178
|
|
|
227
|
-
|
|
228
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
229
|
-
expect(JSON.parse(content)).toEqual({ badKey: 'value' });
|
|
179
|
+
expect(fs.existsSync(configPath())).toBe(false);
|
|
230
180
|
});
|
|
231
181
|
|
|
232
|
-
it('
|
|
233
|
-
|
|
234
|
-
// so subsequent operations don't fail
|
|
235
|
-
const configPath = path.join(tempDir, 'valid.json');
|
|
236
|
-
fs.writeFileSync(configPath, JSON.stringify({ timeout: 99 }));
|
|
237
|
-
|
|
238
|
-
// Load the config
|
|
239
|
-
const config1 = resolveConfig(configPath);
|
|
240
|
-
expect(config1.timeout).toBe(99);
|
|
241
|
-
|
|
242
|
-
// Write different content
|
|
243
|
-
fs.writeFileSync(configPath, JSON.stringify({ timeout: 120 }));
|
|
182
|
+
it('warns when fast mode is set on a codex entry', async () => {
|
|
183
|
+
await parseConfigCommand(['set', 'models.execute', '{"model":"gpt-5.4","harness":"codex","fast":true}']);
|
|
244
184
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// After reset, we should get new value
|
|
250
|
-
const config2 = resolveConfig(configPath);
|
|
251
|
-
expect(config2.timeout).toBe(120);
|
|
185
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
186
|
+
expect.stringContaining('models.execute.fast is enabled but ignored because Codex does not support fast mode')
|
|
187
|
+
);
|
|
252
188
|
});
|
|
253
189
|
});
|
|
254
190
|
|
|
255
|
-
describe('
|
|
256
|
-
it('
|
|
257
|
-
|
|
258
|
-
fs.writeFileSync(configPath, JSON.stringify({ timeout:
|
|
191
|
+
describe('config reset', () => {
|
|
192
|
+
it('deletes the config file after confirmation', async () => {
|
|
193
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
194
|
+
fs.writeFileSync(configPath(), JSON.stringify({ timeout: 45 }, null, 2));
|
|
259
195
|
|
|
260
|
-
|
|
261
|
-
expect(config.timeout).toBe(120);
|
|
262
|
-
expect(config.models.execute).toBe(DEFAULT_CONFIG.models.execute);
|
|
196
|
+
await parseConfigCommand(['reset']);
|
|
263
197
|
|
|
264
|
-
|
|
265
|
-
expect(config).toHaveProperty('models');
|
|
266
|
-
expect(config).toHaveProperty('effortMapping');
|
|
267
|
-
expect(config).toHaveProperty('timeout');
|
|
198
|
+
expect(fs.existsSync(configPath())).toBe(false);
|
|
268
199
|
});
|
|
269
200
|
|
|
270
|
-
it('
|
|
271
|
-
|
|
272
|
-
fs.
|
|
273
|
-
|
|
274
|
-
const config = resolveConfig(configPath);
|
|
275
|
-
expect(config.models.plan).toBe('sonnet');
|
|
276
|
-
});
|
|
201
|
+
it('keeps the config file when confirmation is declined', async () => {
|
|
202
|
+
confirmAnswer = 'n';
|
|
203
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
204
|
+
fs.writeFileSync(configPath(), JSON.stringify({ timeout: 45 }, null, 2));
|
|
277
205
|
|
|
278
|
-
|
|
279
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
280
|
-
fs.writeFileSync(configPath, JSON.stringify({ display: { showCacheTokens: false } }, null, 2));
|
|
206
|
+
await parseConfigCommand(['reset']);
|
|
281
207
|
|
|
282
|
-
|
|
283
|
-
expect(config.display.showCacheTokens).toBe(false);
|
|
208
|
+
expect(fs.existsSync(configPath())).toBe(true);
|
|
284
209
|
});
|
|
285
210
|
});
|
|
286
211
|
|
|
287
|
-
describe('
|
|
288
|
-
it('
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Start with empty config
|
|
292
|
-
expect(fs.existsSync(configPath)).toBe(false);
|
|
212
|
+
describe('config wizard', () => {
|
|
213
|
+
it('launches the interactive session only from the wizard subcommand', async () => {
|
|
214
|
+
await parseConfigCommand(['wizard', 'show', 'my', 'config']);
|
|
293
215
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
current[key] = {};
|
|
301
|
-
current = current[key] as Record<string, unknown>;
|
|
302
|
-
}
|
|
303
|
-
current[keys[keys.length - 1]!] = 'sonnet';
|
|
304
|
-
|
|
305
|
-
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
306
|
-
|
|
307
|
-
const config = resolveConfig(configPath);
|
|
308
|
-
expect(config.models.plan).toBe('sonnet');
|
|
216
|
+
expect(mockCreateRunner).toHaveBeenCalledWith(expect.objectContaining(DEFAULT_CONFIG.models.config));
|
|
217
|
+
expect(mockRunInteractive).toHaveBeenCalledWith(
|
|
218
|
+
expect.any(String),
|
|
219
|
+
'show my config',
|
|
220
|
+
{ dangerouslySkipPermissions: true }
|
|
221
|
+
);
|
|
309
222
|
});
|
|
310
223
|
|
|
311
|
-
it('
|
|
312
|
-
|
|
224
|
+
it('preserves broken-config recovery behavior and updated guidance', async () => {
|
|
225
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
226
|
+
fs.writeFileSync(configPath(), '{ "timeout": 45, }');
|
|
313
227
|
|
|
314
|
-
|
|
315
|
-
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
228
|
+
await parseConfigCommand(['wizard']);
|
|
316
229
|
|
|
317
|
-
|
|
318
|
-
expect(
|
|
230
|
+
expect(mockCreateRunner).toHaveBeenCalledWith(expect.objectContaining(DEFAULT_CONFIG.models.config));
|
|
231
|
+
expect(mockRunInteractive).toHaveBeenCalledWith(
|
|
232
|
+
expect.stringContaining('{ "timeout": 45, }'),
|
|
233
|
+
'Show me my current config and help me make changes.',
|
|
234
|
+
{ dangerouslySkipPermissions: true }
|
|
235
|
+
);
|
|
236
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('raf config reset'));
|
|
319
237
|
});
|
|
320
238
|
|
|
321
|
-
it('
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
const userConfig = { autoCommit: false };
|
|
325
|
-
fs.writeFileSync(configPath, JSON.stringify(userConfig, null, 2));
|
|
326
|
-
|
|
327
|
-
const config = resolveConfig(configPath);
|
|
328
|
-
expect(config.autoCommit).toBe(false);
|
|
329
|
-
});
|
|
239
|
+
it('does not launch the interactive session from bare config', async () => {
|
|
240
|
+
const stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
330
241
|
|
|
331
|
-
|
|
332
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
242
|
+
await parseConfigCommand([]);
|
|
333
243
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
expect(
|
|
244
|
+
expect(mockRunInteractive).not.toHaveBeenCalled();
|
|
245
|
+
expect(stdoutSpy).toHaveBeenCalled();
|
|
246
|
+
const helpOutput = stdoutSpy.mock.calls.map(([chunk]) => String(chunk)).join('');
|
|
247
|
+
expect(helpOutput).toContain('wizard');
|
|
248
|
+
expect(helpOutput).toContain('get');
|
|
249
|
+
expect(helpOutput).toContain('preset');
|
|
338
250
|
|
|
339
|
-
|
|
340
|
-
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
341
|
-
config = resolveConfig(configPath);
|
|
342
|
-
expect(config.models.plan).toBe(DEFAULT_CONFIG.models.plan);
|
|
251
|
+
stdoutSpy.mockRestore();
|
|
343
252
|
});
|
|
253
|
+
});
|
|
344
254
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
255
|
+
describe('config preset', () => {
|
|
256
|
+
it('saves, lists, loads, and deletes presets with the nested command', async () => {
|
|
257
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
configPath(),
|
|
260
|
+
JSON.stringify({ timeout: 45, models: { execute: { model: 'sonnet', harness: 'claude' } } }, null, 2)
|
|
261
|
+
);
|
|
351
262
|
|
|
352
|
-
|
|
353
|
-
fs.writeFileSync(configPath, JSON.stringify({}, null, 2));
|
|
263
|
+
await parseConfigCommand(['preset', 'save', 'team-default']);
|
|
354
264
|
|
|
355
|
-
const
|
|
356
|
-
|
|
265
|
+
const presetPath = path.join(tempDir, '.raf', 'presets', 'team-default.json');
|
|
266
|
+
expect(fs.existsSync(presetPath)).toBe(true);
|
|
267
|
+
expect(JSON.parse(fs.readFileSync(presetPath, 'utf-8'))).toEqual(
|
|
268
|
+
JSON.parse(fs.readFileSync(configPath(), 'utf-8'))
|
|
269
|
+
);
|
|
357
270
|
|
|
358
|
-
|
|
359
|
-
expect(
|
|
360
|
-
});
|
|
271
|
+
await parseConfigCommand(['preset', 'list']);
|
|
272
|
+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('team-default'));
|
|
361
273
|
|
|
362
|
-
|
|
363
|
-
const configPath = path.join(tempDir, 'raf.config.json');
|
|
274
|
+
fs.writeFileSync(configPath(), JSON.stringify({ timeout: 5 }, null, 2));
|
|
364
275
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
276
|
+
await parseConfigCommand(['preset', 'load', 'team-default']);
|
|
277
|
+
expect(JSON.parse(fs.readFileSync(configPath(), 'utf-8'))).toEqual(
|
|
278
|
+
JSON.parse(fs.readFileSync(presetPath, 'utf-8'))
|
|
279
|
+
);
|
|
369
280
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
fs.writeFileSync(configPath, JSON.stringify(invalidConfig, null, 2));
|
|
373
|
-
expect(() => validateConfig(invalidConfig)).toThrow(ConfigValidationError);
|
|
281
|
+
await parseConfigCommand(['preset', 'delete', 'team-default']);
|
|
282
|
+
expect(fs.existsSync(presetPath)).toBe(false);
|
|
374
283
|
});
|
|
375
284
|
|
|
376
|
-
it('
|
|
377
|
-
|
|
285
|
+
it('uses updated guidance in preset runtime messages', async () => {
|
|
286
|
+
await parseConfigCommand(['preset', 'list']);
|
|
287
|
+
expect(mockLogger.info).toHaveBeenCalledWith('No presets saved. Use `raf config preset save <name>` to create one.');
|
|
378
288
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
289
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
290
|
+
throw new Error('process.exit');
|
|
291
|
+
}) as typeof process.exit);
|
|
382
292
|
|
|
383
|
-
|
|
384
|
-
|
|
293
|
+
await expect(parseConfigCommand(['preset', 'load', 'missing'])).rejects.toThrow('process.exit');
|
|
294
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
295
|
+
'Preset "missing" not found. Run `raf config preset list` to see available presets.'
|
|
296
|
+
);
|
|
385
297
|
|
|
386
|
-
|
|
387
|
-
// For this test, we just verify the file operations work
|
|
388
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
389
|
-
expect(JSON.parse(content)).toEqual({});
|
|
298
|
+
exitSpy.mockRestore();
|
|
390
299
|
});
|
|
300
|
+
});
|
|
391
301
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
302
|
+
describe('Validation helpers used by config flows', () => {
|
|
303
|
+
it('accepts valid partial configs', () => {
|
|
304
|
+
expect(() => validateConfig({ models: { execute: { model: 'sonnet', harness: 'claude' } } })).not.toThrow();
|
|
305
|
+
expect(() => validateConfig({ effortMapping: { low: { model: 'sonnet', harness: 'claude' } } })).not.toThrow();
|
|
306
|
+
expect(() => validateConfig({ timeout: 120 })).not.toThrow();
|
|
307
|
+
});
|
|
398
308
|
|
|
399
|
-
|
|
400
|
-
expect(
|
|
309
|
+
it('rejects invalid configs', () => {
|
|
310
|
+
expect(() => validateConfig({ unknownKey: true })).toThrow(ConfigValidationError);
|
|
311
|
+
expect(() => validateConfig({ models: { execute: { model: 'gpt-4', harness: 'codex' } } })).toThrow(ConfigValidationError);
|
|
312
|
+
expect(() => validateConfig('string')).toThrow(ConfigValidationError);
|
|
401
313
|
});
|
|
402
314
|
});
|
|
403
315
|
});
|