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,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SYMBOLS,
|
|
3
|
+
formatModelMetadata,
|
|
3
4
|
formatTaskProgress,
|
|
4
5
|
formatProjectHeader,
|
|
5
6
|
formatSummary,
|
|
@@ -15,6 +16,28 @@ import type { UsageData } from '../../src/types/config.js';
|
|
|
15
16
|
import type { CostBreakdown, TaskUsageEntry } from '../../src/utils/token-tracker.js';
|
|
16
17
|
|
|
17
18
|
describe('Terminal Symbols', () => {
|
|
19
|
+
describe('formatModelMetadata', () => {
|
|
20
|
+
it('should format model only when no metadata is present', () => {
|
|
21
|
+
expect(formatModelMetadata('sonnet')).toBe('sonnet');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should append effort when present', () => {
|
|
25
|
+
expect(formatModelMetadata('sonnet', { effort: 'low' })).toBe('sonnet, low');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should append fast when enabled', () => {
|
|
29
|
+
expect(formatModelMetadata('sonnet', { fast: true })).toBe('sonnet, fast');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should append effort and fast in order', () => {
|
|
33
|
+
expect(formatModelMetadata('sonnet', { effort: 'low', fast: true })).toBe('sonnet, low, fast');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should omit falsy fast values', () => {
|
|
37
|
+
expect(formatModelMetadata('sonnet', { effort: 'low', fast: false })).toBe('sonnet, low');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
18
41
|
describe('SYMBOLS', () => {
|
|
19
42
|
it('should have all required symbols', () => {
|
|
20
43
|
expect(SYMBOLS.running).toBe('●');
|
|
@@ -115,6 +138,11 @@ describe('Terminal Symbols', () => {
|
|
|
115
138
|
expect(result).toBe('● auth-login (sonnet) 1m 23s');
|
|
116
139
|
});
|
|
117
140
|
|
|
141
|
+
it('should show model metadata in parentheses for running task with time', () => {
|
|
142
|
+
const result = formatTaskProgress(1, 5, 'running', 'auth-login', 83000, undefined, 'sonnet', { effort: 'low', fast: true });
|
|
143
|
+
expect(result).toBe('● auth-login (sonnet, low, fast) 1m 23s');
|
|
144
|
+
});
|
|
145
|
+
|
|
118
146
|
it('should show model name in parentheses for running task without time', () => {
|
|
119
147
|
const result = formatTaskProgress(1, 5, 'running', 'auth-login', undefined, undefined, 'opus');
|
|
120
148
|
expect(result).toBe('● auth-login (opus) 1/5');
|
|
@@ -125,6 +153,11 @@ describe('Terminal Symbols', () => {
|
|
|
125
153
|
expect(result).toBe('✓ setup-db (haiku) 2m 34s');
|
|
126
154
|
});
|
|
127
155
|
|
|
156
|
+
it('should omit effort when unavailable and omit falsy fast', () => {
|
|
157
|
+
const result = formatTaskProgress(3, 5, 'completed', 'setup-db', 154000, undefined, 'haiku', { fast: false });
|
|
158
|
+
expect(result).toBe('✓ setup-db (haiku) 2m 34s');
|
|
159
|
+
});
|
|
160
|
+
|
|
128
161
|
it('should show model name in parentheses for completed task without time', () => {
|
|
129
162
|
const result = formatTaskProgress(3, 5, 'completed', 'setup-db', undefined, undefined, 'sonnet');
|
|
130
163
|
expect(result).toBe('✓ setup-db (sonnet) 3/5');
|
|
@@ -145,6 +178,11 @@ describe('Terminal Symbols', () => {
|
|
|
145
178
|
expect(result).toBe('● 001-auth-login (sonnet) 1m 23s');
|
|
146
179
|
});
|
|
147
180
|
|
|
181
|
+
it('should show normalized canonical model names in task progress', () => {
|
|
182
|
+
const result = formatTaskProgress(1, 5, 'running', 'auth-login', 83000, '001', 'gpt-5.4');
|
|
183
|
+
expect(result).toBe('● 001-auth-login (gpt-5.4) 1m 23s');
|
|
184
|
+
});
|
|
185
|
+
|
|
148
186
|
it('should show model name for blocked task without time', () => {
|
|
149
187
|
const result = formatTaskProgress(2, 5, 'blocked', 'depends-on-failed', undefined, undefined, 'sonnet');
|
|
150
188
|
expect(result).toBe('⊘ depends-on-failed (sonnet) 2/5');
|
|
@@ -164,6 +202,14 @@ describe('Terminal Symbols', () => {
|
|
|
164
202
|
const result = formatTaskProgress(2, 5, 'completed', 'setup-db', 154000, '002', 'opus');
|
|
165
203
|
expect(result).toBe('✓ 002-setup-db (opus) 2m 34s');
|
|
166
204
|
});
|
|
205
|
+
|
|
206
|
+
it('should keep truncation stable with model metadata', () => {
|
|
207
|
+
const longName = 'this-is-a-very-long-task-name-that-should-be-truncated-for-display';
|
|
208
|
+
const result = formatTaskProgress(1, 1, 'running', longName, 1000, '001', 'sonnet', { effort: 'low', fast: true });
|
|
209
|
+
|
|
210
|
+
expect(result).toContain('001-this-is-a-very-long-task-name-that-shou…');
|
|
211
|
+
expect(result).toContain('(sonnet, low, fast) 1s');
|
|
212
|
+
});
|
|
167
213
|
});
|
|
168
214
|
|
|
169
215
|
describe('formatProjectHeader', () => {
|
|
@@ -317,6 +363,10 @@ describe('Terminal Symbols', () => {
|
|
|
317
363
|
expect(formatCost(0)).toBe('$0.00');
|
|
318
364
|
});
|
|
319
365
|
|
|
366
|
+
it('should format unavailable cost', () => {
|
|
367
|
+
expect(formatCost(null)).toBe('unavailable');
|
|
368
|
+
});
|
|
369
|
+
|
|
320
370
|
it('should format normal costs with 2 decimals', () => {
|
|
321
371
|
expect(formatCost(1.23)).toBe('$1.23');
|
|
322
372
|
});
|
|
@@ -341,10 +391,11 @@ describe('Terminal Symbols', () => {
|
|
|
341
391
|
cacheReadInputTokens: 0,
|
|
342
392
|
cacheCreationInputTokens: 0,
|
|
343
393
|
modelUsage: {},
|
|
394
|
+
totalCostUsd: 0,
|
|
344
395
|
...overrides,
|
|
345
396
|
});
|
|
346
397
|
|
|
347
|
-
const makeCost = (total: number): CostBreakdown => ({
|
|
398
|
+
const makeCost = (total: number | null): CostBreakdown => ({
|
|
348
399
|
totalCost: total,
|
|
349
400
|
});
|
|
350
401
|
|
|
@@ -391,6 +442,12 @@ describe('Terminal Symbols', () => {
|
|
|
391
442
|
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(0.42), []));
|
|
392
443
|
expect(result).toBe(' Tokens: 5,234 in / 1,023 out | Cost: $0.42');
|
|
393
444
|
});
|
|
445
|
+
|
|
446
|
+
it('should omit cost when exact cost is unknown', () => {
|
|
447
|
+
const usage = makeUsage({ totalCostUsd: null });
|
|
448
|
+
const result = formatTaskTokenSummary(makeEntry(usage, makeCost(null)));
|
|
449
|
+
expect(result).toBe(' Tokens: 5,234 in / 1,023 out');
|
|
450
|
+
});
|
|
394
451
|
});
|
|
395
452
|
|
|
396
453
|
describe('multi-attempt tasks', () => {
|
|
@@ -460,6 +517,34 @@ describe('Terminal Symbols', () => {
|
|
|
460
517
|
expect(lines[2]).toContain('Attempt 3');
|
|
461
518
|
expect(lines[3]).toContain('Total');
|
|
462
519
|
});
|
|
520
|
+
|
|
521
|
+
it('should omit cost for attempts and totals with unknown exact cost', () => {
|
|
522
|
+
const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, totalCostUsd: null });
|
|
523
|
+
const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400, totalCostUsd: null });
|
|
524
|
+
const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600, totalCostUsd: null });
|
|
525
|
+
const entry = makeEntry(totalUsage, makeCost(null), [attempt1, attempt2]);
|
|
526
|
+
|
|
527
|
+
const result = formatTaskTokenSummary(entry);
|
|
528
|
+
const lines = result.split('\n');
|
|
529
|
+
|
|
530
|
+
expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out');
|
|
531
|
+
expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out');
|
|
532
|
+
expect(lines[2]).toBe(' Total: 3,000 in / 600 out');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should preserve exact zero cost instead of omitting it', () => {
|
|
536
|
+
const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, totalCostUsd: 0 });
|
|
537
|
+
const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400, totalCostUsd: null });
|
|
538
|
+
const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600, totalCostUsd: 0 });
|
|
539
|
+
const entry = makeEntry(totalUsage, makeCost(0), [attempt1, attempt2]);
|
|
540
|
+
|
|
541
|
+
const result = formatTaskTokenSummary(entry);
|
|
542
|
+
const lines = result.split('\n');
|
|
543
|
+
|
|
544
|
+
expect(lines[0]).toBe(' Attempt 1: 1,000 in / 200 out | Cost: $0.00');
|
|
545
|
+
expect(lines[1]).toBe(' Attempt 2: 2,000 in / 400 out');
|
|
546
|
+
expect(lines[2]).toBe(' Total: 3,000 in / 600 out | Cost: $0.00');
|
|
547
|
+
});
|
|
463
548
|
});
|
|
464
549
|
});
|
|
465
550
|
|
|
@@ -474,7 +559,7 @@ describe('Terminal Symbols', () => {
|
|
|
474
559
|
...overrides,
|
|
475
560
|
});
|
|
476
561
|
|
|
477
|
-
const makeCost = (total: number): CostBreakdown => ({
|
|
562
|
+
const makeCost = (total: number | null): CostBreakdown => ({
|
|
478
563
|
totalCost: total,
|
|
479
564
|
});
|
|
480
565
|
|
|
@@ -528,6 +613,11 @@ describe('Terminal Symbols', () => {
|
|
|
528
613
|
);
|
|
529
614
|
expect(result).not.toContain('Cache:');
|
|
530
615
|
});
|
|
616
|
+
|
|
617
|
+
it('should omit total cost when exact cost is unknown', () => {
|
|
618
|
+
const result = formatTokenTotalSummary(makeUsage(), makeCost(null));
|
|
619
|
+
expect(result).not.toContain('Total cost:');
|
|
620
|
+
});
|
|
531
621
|
});
|
|
532
622
|
|
|
533
623
|
describe('formatTaskTokenSummary with options', () => {
|
|
@@ -537,10 +627,11 @@ describe('Terminal Symbols', () => {
|
|
|
537
627
|
cacheReadInputTokens: 0,
|
|
538
628
|
cacheCreationInputTokens: 0,
|
|
539
629
|
modelUsage: {},
|
|
630
|
+
totalCostUsd: 0,
|
|
540
631
|
...overrides,
|
|
541
632
|
});
|
|
542
633
|
|
|
543
|
-
const makeCost = (total: number): CostBreakdown => ({
|
|
634
|
+
const makeCost = (total: number | null): CostBreakdown => ({
|
|
544
635
|
totalCost: total,
|
|
545
636
|
});
|
|
546
637
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TokenTracker, CostBreakdown, accumulateUsage, sumCostBreakdowns } from '../../src/utils/token-tracker.js';
|
|
1
|
+
import { TokenTracker, CostBreakdown, accumulateUsage, mergeUsageData, sumCostBreakdowns } from '../../src/utils/token-tracker.js';
|
|
2
2
|
import { UsageData } from '../../src/types/config.js';
|
|
3
3
|
|
|
4
4
|
function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
|
|
@@ -85,6 +85,26 @@ describe('TokenTracker', () => {
|
|
|
85
85
|
expect(entry.cost.totalCost).toBe(25.0);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('should mark total cost unavailable when any attempt has unknown exact cost', () => {
|
|
89
|
+
const tracker = new TokenTracker();
|
|
90
|
+
const attempt1 = makeUsage({
|
|
91
|
+
inputTokens: 100,
|
|
92
|
+
outputTokens: 50,
|
|
93
|
+
totalCostUsd: null,
|
|
94
|
+
});
|
|
95
|
+
const attempt2 = makeUsage({
|
|
96
|
+
inputTokens: 200,
|
|
97
|
+
outputTokens: 75,
|
|
98
|
+
totalCostUsd: 0.5,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const entry = tracker.addTask('01', [attempt1, attempt2]);
|
|
102
|
+
expect(entry.usage.inputTokens).toBe(300);
|
|
103
|
+
expect(entry.usage.outputTokens).toBe(125);
|
|
104
|
+
expect(entry.cost.totalCost).toBeNull();
|
|
105
|
+
expect(entry.usage.totalCostUsd).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
88
108
|
it('should store attempts array in entry', () => {
|
|
89
109
|
const tracker = new TokenTracker();
|
|
90
110
|
const usage = makeUsage({ inputTokens: 100, totalCostUsd: 0.01 });
|
|
@@ -129,6 +149,27 @@ describe('TokenTracker', () => {
|
|
|
129
149
|
expect(totals.usage.outputTokens).toBe(750_000);
|
|
130
150
|
});
|
|
131
151
|
|
|
152
|
+
it('should keep grand total cost unavailable when any task cost is unknown', () => {
|
|
153
|
+
const tracker = new TokenTracker();
|
|
154
|
+
|
|
155
|
+
tracker.addTask('01', [makeUsage({
|
|
156
|
+
inputTokens: 100,
|
|
157
|
+
outputTokens: 10,
|
|
158
|
+
totalCostUsd: null,
|
|
159
|
+
})]);
|
|
160
|
+
|
|
161
|
+
tracker.addTask('02', [makeUsage({
|
|
162
|
+
inputTokens: 200,
|
|
163
|
+
outputTokens: 20,
|
|
164
|
+
totalCostUsd: 1.25,
|
|
165
|
+
})]);
|
|
166
|
+
|
|
167
|
+
const totals = tracker.getTotals();
|
|
168
|
+
expect(totals.usage.inputTokens).toBe(300);
|
|
169
|
+
expect(totals.cost.totalCost).toBeNull();
|
|
170
|
+
expect(totals.usage.totalCostUsd).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
132
173
|
it('should return empty totals when no tasks added', () => {
|
|
133
174
|
const tracker = new TokenTracker();
|
|
134
175
|
const totals = tracker.getTotals();
|
|
@@ -330,6 +371,21 @@ describe('TokenTracker', () => {
|
|
|
330
371
|
expect(result.totalCostUsd).toBe(1.5);
|
|
331
372
|
});
|
|
332
373
|
|
|
374
|
+
it('should keep accumulated cost unavailable when any attempt has unknown cost', () => {
|
|
375
|
+
const attempt1 = makeUsage({
|
|
376
|
+
inputTokens: 100,
|
|
377
|
+
totalCostUsd: null,
|
|
378
|
+
});
|
|
379
|
+
const attempt2 = makeUsage({
|
|
380
|
+
inputTokens: 200,
|
|
381
|
+
totalCostUsd: 1.0,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const result = accumulateUsage([attempt1, attempt2]);
|
|
385
|
+
expect(result.inputTokens).toBe(300);
|
|
386
|
+
expect(result.totalCostUsd).toBeNull();
|
|
387
|
+
});
|
|
388
|
+
|
|
333
389
|
it('should merge modelUsage for same model across attempts', () => {
|
|
334
390
|
const attempt1 = makeUsage({
|
|
335
391
|
modelUsage: {
|
|
@@ -440,6 +496,121 @@ describe('TokenTracker', () => {
|
|
|
440
496
|
});
|
|
441
497
|
});
|
|
442
498
|
|
|
499
|
+
describe('mergeUsageData', () => {
|
|
500
|
+
it('should initialize usage when existing is undefined', () => {
|
|
501
|
+
const incoming = makeUsage({
|
|
502
|
+
inputTokens: 10,
|
|
503
|
+
outputTokens: 5,
|
|
504
|
+
totalCostUsd: 0.25,
|
|
505
|
+
modelUsage: {
|
|
506
|
+
'claude-opus-4-6': {
|
|
507
|
+
inputTokens: 10,
|
|
508
|
+
outputTokens: 5,
|
|
509
|
+
cacheReadInputTokens: 0,
|
|
510
|
+
cacheCreationInputTokens: 0,
|
|
511
|
+
costUsd: 0.25,
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const result = mergeUsageData(undefined, incoming);
|
|
517
|
+
expect(result).toEqual(incoming);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should return existing usage when incoming is undefined', () => {
|
|
521
|
+
const existing = makeUsage({
|
|
522
|
+
inputTokens: 10,
|
|
523
|
+
outputTokens: 5,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const result = mergeUsageData(existing, undefined);
|
|
527
|
+
expect(result).toEqual(existing);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('should sum usage fields and model usage across both inputs', () => {
|
|
531
|
+
const existing = makeUsage({
|
|
532
|
+
inputTokens: 100,
|
|
533
|
+
outputTokens: 40,
|
|
534
|
+
cacheReadInputTokens: 10,
|
|
535
|
+
cacheCreationInputTokens: 5,
|
|
536
|
+
totalCostUsd: 1.0,
|
|
537
|
+
modelUsage: {
|
|
538
|
+
'claude-opus-4-6': {
|
|
539
|
+
inputTokens: 100,
|
|
540
|
+
outputTokens: 40,
|
|
541
|
+
cacheReadInputTokens: 10,
|
|
542
|
+
cacheCreationInputTokens: 5,
|
|
543
|
+
costUsd: 1.0,
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
const incoming = makeUsage({
|
|
548
|
+
inputTokens: 50,
|
|
549
|
+
outputTokens: 20,
|
|
550
|
+
cacheReadInputTokens: 4,
|
|
551
|
+
cacheCreationInputTokens: 2,
|
|
552
|
+
totalCostUsd: 0.5,
|
|
553
|
+
modelUsage: {
|
|
554
|
+
'claude-opus-4-6': {
|
|
555
|
+
inputTokens: 50,
|
|
556
|
+
outputTokens: 20,
|
|
557
|
+
cacheReadInputTokens: 4,
|
|
558
|
+
cacheCreationInputTokens: 2,
|
|
559
|
+
costUsd: 0.5,
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const result = mergeUsageData(existing, incoming);
|
|
565
|
+
expect(result).toEqual({
|
|
566
|
+
inputTokens: 150,
|
|
567
|
+
outputTokens: 60,
|
|
568
|
+
cacheReadInputTokens: 14,
|
|
569
|
+
cacheCreationInputTokens: 7,
|
|
570
|
+
totalCostUsd: 1.5,
|
|
571
|
+
modelUsage: {
|
|
572
|
+
'claude-opus-4-6': {
|
|
573
|
+
inputTokens: 150,
|
|
574
|
+
outputTokens: 60,
|
|
575
|
+
cacheReadInputTokens: 14,
|
|
576
|
+
cacheCreationInputTokens: 7,
|
|
577
|
+
costUsd: 1.5,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should treat missing numeric fields as zero while merging', () => {
|
|
584
|
+
const existing = makeUsage({
|
|
585
|
+
inputTokens: 20,
|
|
586
|
+
outputTokens: 10,
|
|
587
|
+
totalCostUsd: null,
|
|
588
|
+
});
|
|
589
|
+
const incoming = {
|
|
590
|
+
inputTokens: 5,
|
|
591
|
+
modelUsage: {
|
|
592
|
+
codex: {
|
|
593
|
+
inputTokens: 5,
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
} as unknown as UsageData;
|
|
597
|
+
|
|
598
|
+
const result = mergeUsageData(existing, incoming);
|
|
599
|
+
expect(result!.inputTokens).toBe(25);
|
|
600
|
+
expect(result!.outputTokens).toBe(10);
|
|
601
|
+
expect(result!.cacheReadInputTokens).toBe(0);
|
|
602
|
+
expect(result!.cacheCreationInputTokens).toBe(0);
|
|
603
|
+
expect(result!.totalCostUsd).toBeNull();
|
|
604
|
+
expect(result!.modelUsage.codex).toEqual({
|
|
605
|
+
inputTokens: 5,
|
|
606
|
+
outputTokens: 0,
|
|
607
|
+
cacheReadInputTokens: 0,
|
|
608
|
+
cacheCreationInputTokens: 0,
|
|
609
|
+
costUsd: null,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
443
614
|
describe('sumCostBreakdowns', () => {
|
|
444
615
|
it('should return zero breakdown for empty array', () => {
|
|
445
616
|
const result = sumCostBreakdowns([]);
|
|
@@ -464,5 +635,13 @@ describe('TokenTracker', () => {
|
|
|
464
635
|
const result = sumCostBreakdowns([cost1, cost2]);
|
|
465
636
|
expect(result.totalCost).toBe(49.5);
|
|
466
637
|
});
|
|
638
|
+
|
|
639
|
+
it('should keep total cost unavailable when any breakdown is unknown', () => {
|
|
640
|
+
const result = sumCostBreakdowns([
|
|
641
|
+
{ totalCost: 33 },
|
|
642
|
+
{ totalCost: null },
|
|
643
|
+
]);
|
|
644
|
+
expect(result.totalCost).toBeNull();
|
|
645
|
+
});
|
|
467
646
|
});
|
|
468
647
|
});
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
validateProjectName,
|
|
3
3
|
sanitizeProjectName,
|
|
4
4
|
validateModelName,
|
|
5
|
-
resolveModelOption,
|
|
6
5
|
} from '../../src/utils/validation.js';
|
|
7
6
|
|
|
8
7
|
describe('Validation', () => {
|
|
@@ -67,6 +66,11 @@ describe('Validation', () => {
|
|
|
67
66
|
expect(validateModelName('OPUS')).toBe('opus');
|
|
68
67
|
});
|
|
69
68
|
|
|
69
|
+
it('should accept Codex model names', () => {
|
|
70
|
+
expect(validateModelName('gpt-5.4')).toBe('gpt-5.4');
|
|
71
|
+
expect(validateModelName('gpt54')).toBe('gpt54');
|
|
72
|
+
});
|
|
73
|
+
|
|
70
74
|
it('should reject invalid model names', () => {
|
|
71
75
|
expect(validateModelName('gpt4')).toBeNull();
|
|
72
76
|
expect(validateModelName('claude')).toBeNull();
|
|
@@ -75,46 +79,10 @@ describe('Validation', () => {
|
|
|
75
79
|
});
|
|
76
80
|
});
|
|
77
81
|
|
|
78
|
-
describe('resolveModelOption', () => {
|
|
79
|
-
it('should
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(result).toMatch(/^(opus|sonnet|haiku|claude-(opus|sonnet|haiku)-.+)$/);
|
|
83
|
-
expect(resolveModelOption(undefined, undefined)).toBe(result);
|
|
84
|
-
expect(resolveModelOption(undefined, false)).toBe(result);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should use --model flag when provided', () => {
|
|
88
|
-
expect(resolveModelOption('sonnet')).toBe('sonnet');
|
|
89
|
-
expect(resolveModelOption('haiku')).toBe('haiku');
|
|
90
|
-
expect(resolveModelOption('opus')).toBe('opus');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should normalize model name to lowercase', () => {
|
|
94
|
-
expect(resolveModelOption('SONNET')).toBe('sonnet');
|
|
95
|
-
expect(resolveModelOption('Haiku')).toBe('haiku');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should use --sonnet shorthand', () => {
|
|
99
|
-
expect(resolveModelOption(undefined, true)).toBe('sonnet');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should throw error for conflicting flags', () => {
|
|
103
|
-
expect(() => resolveModelOption('haiku', true)).toThrow(
|
|
104
|
-
'Cannot specify both --model and --sonnet flags'
|
|
105
|
-
);
|
|
106
|
-
expect(() => resolveModelOption('opus', true)).toThrow(
|
|
107
|
-
'Cannot specify both --model and --sonnet flags'
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should throw error for invalid model name', () => {
|
|
112
|
-
expect(() => resolveModelOption('gpt4')).toThrow(
|
|
113
|
-
'Invalid model name: "gpt4". Valid options: sonnet, haiku, opus'
|
|
114
|
-
);
|
|
115
|
-
expect(() => resolveModelOption('invalid')).toThrow(
|
|
116
|
-
'Invalid model name: "invalid". Valid options: sonnet, haiku, opus'
|
|
117
|
-
);
|
|
82
|
+
describe('resolveModelOption removed', () => {
|
|
83
|
+
it('should not be exported from validation module', async () => {
|
|
84
|
+
const validationModule = await import('../../src/utils/validation.js');
|
|
85
|
+
expect('resolveModelOption' in validationModule).toBe(false);
|
|
118
86
|
});
|
|
119
87
|
});
|
|
120
88
|
});
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
|
-
import { Command } from 'commander';
|
|
5
|
-
import { createPlanCommand } from '../../src/commands/plan.js';
|
|
6
|
-
import { createDoCommand } from '../../src/commands/do.js';
|
|
7
|
-
import { getWorktreeDefault, resetConfigCache, saveConfig } from '../../src/utils/config.js';
|
|
8
|
-
|
|
9
|
-
describe('Worktree Flag Override', () => {
|
|
10
|
-
let tempDir: string;
|
|
11
|
-
let originalHome: string | undefined;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-worktree-flag-test-'));
|
|
15
|
-
originalHome = process.env.HOME;
|
|
16
|
-
process.env.HOME = tempDir;
|
|
17
|
-
resetConfigCache();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
process.env.HOME = originalHome;
|
|
22
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
23
|
-
resetConfigCache();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('Commander.js --no-worktree flag parsing', () => {
|
|
27
|
-
it('should parse --worktree as true', () => {
|
|
28
|
-
const planCommand = createPlanCommand();
|
|
29
|
-
// Use parseOptions instead of parse to avoid running the action
|
|
30
|
-
planCommand.parseOptions(['--worktree']);
|
|
31
|
-
const opts = planCommand.opts();
|
|
32
|
-
expect(opts.worktree).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should parse --no-worktree as false', () => {
|
|
36
|
-
const planCommand = createPlanCommand();
|
|
37
|
-
planCommand.parseOptions(['--no-worktree']);
|
|
38
|
-
const opts = planCommand.opts();
|
|
39
|
-
expect(opts.worktree).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should parse omitted flag as undefined', () => {
|
|
43
|
-
const planCommand = createPlanCommand();
|
|
44
|
-
planCommand.parseOptions([]);
|
|
45
|
-
const opts = planCommand.opts();
|
|
46
|
-
expect(opts.worktree).toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should parse --worktree for do command as true', () => {
|
|
50
|
-
const doCommand = createDoCommand();
|
|
51
|
-
doCommand.parseOptions(['--worktree']);
|
|
52
|
-
const opts = doCommand.opts();
|
|
53
|
-
expect(opts.worktree).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should parse --no-worktree for do command as false', () => {
|
|
57
|
-
const doCommand = createDoCommand();
|
|
58
|
-
doCommand.parseOptions(['--no-worktree']);
|
|
59
|
-
const opts = doCommand.opts();
|
|
60
|
-
expect(opts.worktree).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should parse omitted flag for do command as undefined', () => {
|
|
64
|
-
const doCommand = createDoCommand();
|
|
65
|
-
doCommand.parseOptions([]);
|
|
66
|
-
const opts = doCommand.opts();
|
|
67
|
-
expect(opts.worktree).toBeUndefined();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('Config resolution with --no-worktree flag', () => {
|
|
72
|
-
it('should resolve to true when --worktree flag is passed (regardless of config)', () => {
|
|
73
|
-
// Simulate: options.worktree = true (from --worktree flag)
|
|
74
|
-
const options = { worktree: true };
|
|
75
|
-
// With nullish coalescing, explicit true takes precedence
|
|
76
|
-
const resolved = options.worktree ?? getWorktreeDefault();
|
|
77
|
-
expect(resolved).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should resolve to false when --no-worktree flag is passed (regardless of config)', () => {
|
|
81
|
-
// Simulate: options.worktree = false (from --no-worktree flag)
|
|
82
|
-
const options = { worktree: false };
|
|
83
|
-
// With nullish coalescing, explicit false takes precedence
|
|
84
|
-
const resolved = options.worktree ?? getWorktreeDefault();
|
|
85
|
-
expect(resolved).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should resolve to config default when no flag is passed', () => {
|
|
89
|
-
// Simulate: options.worktree = undefined (no flag passed)
|
|
90
|
-
const options = { worktree: undefined };
|
|
91
|
-
// With nullish coalescing, undefined falls back to getWorktreeDefault()
|
|
92
|
-
const resolved = options.worktree ?? getWorktreeDefault();
|
|
93
|
-
// We can't assert a specific value here since it depends on the user's actual config
|
|
94
|
-
expect(typeof resolved).toBe('boolean');
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('Tri-state behavior verification', () => {
|
|
99
|
-
it('should correctly handle all three states (true/false/undefined) in plan command', () => {
|
|
100
|
-
// State 1: --worktree (explicit true)
|
|
101
|
-
const planCmd1 = createPlanCommand();
|
|
102
|
-
planCmd1.parseOptions(['--worktree']);
|
|
103
|
-
const opts1 = planCmd1.opts();
|
|
104
|
-
expect(opts1.worktree).toBe(true);
|
|
105
|
-
const resolved1 = opts1.worktree ?? getWorktreeDefault();
|
|
106
|
-
expect(resolved1).toBe(true);
|
|
107
|
-
|
|
108
|
-
// State 2: --no-worktree (explicit false)
|
|
109
|
-
const planCmd2 = createPlanCommand();
|
|
110
|
-
planCmd2.parseOptions(['--no-worktree']);
|
|
111
|
-
const opts2 = planCmd2.opts();
|
|
112
|
-
expect(opts2.worktree).toBe(false);
|
|
113
|
-
const resolved2 = opts2.worktree ?? getWorktreeDefault();
|
|
114
|
-
expect(resolved2).toBe(false);
|
|
115
|
-
|
|
116
|
-
// State 3: omitted (undefined, falls back to config)
|
|
117
|
-
const planCmd3 = createPlanCommand();
|
|
118
|
-
planCmd3.parseOptions([]);
|
|
119
|
-
const opts3 = planCmd3.opts();
|
|
120
|
-
expect(opts3.worktree).toBeUndefined();
|
|
121
|
-
const resolved3 = opts3.worktree ?? getWorktreeDefault();
|
|
122
|
-
// Should be a boolean (actual value depends on config)
|
|
123
|
-
expect(typeof resolved3).toBe('boolean');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should correctly handle all three states (true/false/undefined) in do command', () => {
|
|
127
|
-
// State 1: --worktree (explicit true)
|
|
128
|
-
const doCmd1 = createDoCommand();
|
|
129
|
-
doCmd1.parseOptions(['--worktree']);
|
|
130
|
-
const opts1 = doCmd1.opts();
|
|
131
|
-
expect(opts1.worktree).toBe(true);
|
|
132
|
-
const resolved1 = opts1.worktree ?? getWorktreeDefault();
|
|
133
|
-
expect(resolved1).toBe(true);
|
|
134
|
-
|
|
135
|
-
// State 2: --no-worktree (explicit false)
|
|
136
|
-
const doCmd2 = createDoCommand();
|
|
137
|
-
doCmd2.parseOptions(['--no-worktree']);
|
|
138
|
-
const opts2 = doCmd2.opts();
|
|
139
|
-
expect(opts2.worktree).toBe(false);
|
|
140
|
-
const resolved2 = opts2.worktree ?? getWorktreeDefault();
|
|
141
|
-
expect(resolved2).toBe(false);
|
|
142
|
-
|
|
143
|
-
// State 3: omitted (undefined, falls back to config)
|
|
144
|
-
const doCmd3 = createDoCommand();
|
|
145
|
-
doCmd3.parseOptions([]);
|
|
146
|
-
const opts3 = doCmd3.opts();
|
|
147
|
-
expect(opts3.worktree).toBeUndefined();
|
|
148
|
-
const resolved3 = opts3.worktree ?? getWorktreeDefault();
|
|
149
|
-
// Should be a boolean (actual value depends on config)
|
|
150
|
-
expect(typeof resolved3).toBe('boolean');
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe('Override semantics', () => {
|
|
155
|
-
it('--no-worktree should override config default (explicit false takes precedence)', () => {
|
|
156
|
-
const planCommand = createPlanCommand();
|
|
157
|
-
planCommand.parseOptions(['--no-worktree']);
|
|
158
|
-
const opts = planCommand.opts();
|
|
159
|
-
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
160
|
-
|
|
161
|
-
expect(opts.worktree).toBe(false); // Flag sets explicit false
|
|
162
|
-
expect(resolved).toBe(false); // Final result is false (flag takes precedence)
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('--worktree should override config default (explicit true takes precedence)', () => {
|
|
166
|
-
const doCommand = createDoCommand();
|
|
167
|
-
doCommand.parseOptions(['--worktree']);
|
|
168
|
-
const opts = doCommand.opts();
|
|
169
|
-
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
170
|
-
|
|
171
|
-
expect(opts.worktree).toBe(true); // Flag sets explicit true
|
|
172
|
-
expect(resolved).toBe(true); // Final result is true (flag takes precedence)
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('omitting flag should fall back to config default', () => {
|
|
176
|
-
const planCommand = createPlanCommand();
|
|
177
|
-
planCommand.parseOptions([]);
|
|
178
|
-
const opts = planCommand.opts();
|
|
179
|
-
const resolved = opts.worktree ?? getWorktreeDefault();
|
|
180
|
-
|
|
181
|
-
expect(opts.worktree).toBeUndefined(); // Flag not set
|
|
182
|
-
expect(typeof resolved).toBe('boolean'); // Falls back to config (which is a boolean)
|
|
183
|
-
expect(resolved).toBe(getWorktreeDefault()); // Final result matches config
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|