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
|
@@ -13,6 +13,10 @@ export interface TokenSummaryOptions {
|
|
|
13
13
|
showCacheTokens?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function hasExactCost(cost: number | null): cost is number {
|
|
17
|
+
return cost !== null;
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* Visual symbols for terminal output using dots/symbols style.
|
|
18
22
|
*/
|
|
@@ -27,6 +31,11 @@ export const SYMBOLS = {
|
|
|
27
31
|
|
|
28
32
|
export type TaskStatus = 'running' | 'completed' | 'failed' | 'pending' | 'blocked';
|
|
29
33
|
|
|
34
|
+
export interface ModelDisplayOptions {
|
|
35
|
+
effort?: string;
|
|
36
|
+
fast?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Truncates a string to the specified length, adding ellipsis if needed.
|
|
32
41
|
*/
|
|
@@ -55,12 +64,13 @@ export function formatTaskProgress(
|
|
|
55
64
|
name: string,
|
|
56
65
|
elapsedMs?: number,
|
|
57
66
|
taskId?: string,
|
|
58
|
-
model?: string
|
|
67
|
+
model?: string,
|
|
68
|
+
modelOptions: ModelDisplayOptions = {}
|
|
59
69
|
): string {
|
|
60
70
|
const symbol = SYMBOLS[status];
|
|
61
71
|
const displayName = truncate(name || 'task', 40);
|
|
62
72
|
const idPrefix = taskId ? `${taskId}-` : '';
|
|
63
|
-
const modelSuffix = model
|
|
73
|
+
const modelSuffix = formatModelDisplay(model, modelOptions);
|
|
64
74
|
|
|
65
75
|
// Show elapsed time for running tasks, completed tasks, and failed tasks
|
|
66
76
|
if (elapsedMs !== undefined) {
|
|
@@ -71,6 +81,31 @@ export function formatTaskProgress(
|
|
|
71
81
|
return `${symbol} ${idPrefix}${displayName}${modelSuffix} ${current}/${total}`;
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Formats a model label with optional effort/fast metadata.
|
|
86
|
+
* Examples: "sonnet", "sonnet, low", "sonnet, low, fast"
|
|
87
|
+
*/
|
|
88
|
+
export function formatModelMetadata(model: string, options: ModelDisplayOptions = {}): string {
|
|
89
|
+
const parts = [model];
|
|
90
|
+
if (options.effort) {
|
|
91
|
+
parts.push(options.effort);
|
|
92
|
+
}
|
|
93
|
+
if (options.fast) {
|
|
94
|
+
parts.push('fast');
|
|
95
|
+
}
|
|
96
|
+
return parts.join(', ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Formats model metadata for display surfaces that wrap the label in parentheses.
|
|
101
|
+
*/
|
|
102
|
+
export function formatModelDisplay(model?: string, options: ModelDisplayOptions = {}): string {
|
|
103
|
+
if (!model) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
return ` (${formatModelMetadata(model, options)})`;
|
|
107
|
+
}
|
|
108
|
+
|
|
74
109
|
/**
|
|
75
110
|
* Formats a project header line.
|
|
76
111
|
* @param name - Project name
|
|
@@ -148,7 +183,8 @@ export function formatNumber(n: number): string {
|
|
|
148
183
|
* Formats a cost in USD with 2-4 decimal places.
|
|
149
184
|
* Uses 2 decimals for values >= $0.01, 4 decimals for smaller values.
|
|
150
185
|
*/
|
|
151
|
-
export function formatCost(cost: number): string {
|
|
186
|
+
export function formatCost(cost: number | null): string {
|
|
187
|
+
if (cost === null) return 'unavailable';
|
|
152
188
|
if (cost === 0) return '$0.00';
|
|
153
189
|
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
154
190
|
return `$${cost.toFixed(2)}`;
|
|
@@ -160,7 +196,7 @@ export function formatCost(cost: number): string {
|
|
|
160
196
|
*/
|
|
161
197
|
function formatTokenLine(
|
|
162
198
|
usage: UsageData,
|
|
163
|
-
costValue: number,
|
|
199
|
+
costValue: number | null,
|
|
164
200
|
prefix: string = '',
|
|
165
201
|
indent: string = ' ',
|
|
166
202
|
options: TokenSummaryOptions = {}
|
|
@@ -183,7 +219,9 @@ function formatTokenLine(
|
|
|
183
219
|
}
|
|
184
220
|
}
|
|
185
221
|
|
|
186
|
-
|
|
222
|
+
if (hasExactCost(costValue)) {
|
|
223
|
+
parts.push(`Cost: ${formatCost(costValue)}`);
|
|
224
|
+
}
|
|
187
225
|
|
|
188
226
|
return `${indent}${parts.join(' | ')}`;
|
|
189
227
|
}
|
|
@@ -245,7 +283,9 @@ export function formatTokenTotalSummary(
|
|
|
245
283
|
lines.push(`Cache: ${cacheParts.join(' / ')}`);
|
|
246
284
|
}
|
|
247
285
|
|
|
248
|
-
|
|
286
|
+
if (hasExactCost(cost.totalCost)) {
|
|
287
|
+
lines.push(`Total cost: ${formatCost(cost.totalCost)}`);
|
|
288
|
+
}
|
|
249
289
|
|
|
250
290
|
lines.push('─────────────────────────────────────────');
|
|
251
291
|
return lines.join('\n');
|
|
@@ -2,7 +2,7 @@ import { UsageData } from '../types/config.js';
|
|
|
2
2
|
|
|
3
3
|
/** Cost breakdown for a single task or accumulated total. */
|
|
4
4
|
export interface CostBreakdown {
|
|
5
|
-
totalCost: number;
|
|
5
|
+
totalCost: number | null;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/** Per-task usage snapshot stored by the tracker. */
|
|
@@ -16,12 +16,82 @@ export interface TaskUsageEntry {
|
|
|
16
16
|
attempts: UsageData[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function mergeCostUsd(existing: number | null | undefined, incoming: number | null | undefined): number | null {
|
|
20
|
+
if (existing === null || incoming === null || existing === undefined || incoming === undefined) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return existing + incoming;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge usage data into an accumulated snapshot.
|
|
28
|
+
* Handles undefined input for first-event initialization.
|
|
29
|
+
*/
|
|
30
|
+
export function mergeUsageData(existing: UsageData | undefined, incoming: UsageData | undefined): UsageData | undefined {
|
|
31
|
+
if (!incoming) {
|
|
32
|
+
return existing;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!existing) {
|
|
36
|
+
return {
|
|
37
|
+
inputTokens: incoming.inputTokens ?? 0,
|
|
38
|
+
outputTokens: incoming.outputTokens ?? 0,
|
|
39
|
+
cacheReadInputTokens: incoming.cacheReadInputTokens ?? 0,
|
|
40
|
+
cacheCreationInputTokens: incoming.cacheCreationInputTokens ?? 0,
|
|
41
|
+
modelUsage: Object.fromEntries(
|
|
42
|
+
Object.entries(incoming.modelUsage ?? {}).map(([modelId, usage]) => [
|
|
43
|
+
modelId,
|
|
44
|
+
{
|
|
45
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
46
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
47
|
+
cacheReadInputTokens: usage.cacheReadInputTokens ?? 0,
|
|
48
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
|
|
49
|
+
costUsd: usage.costUsd ?? null,
|
|
50
|
+
},
|
|
51
|
+
]),
|
|
52
|
+
),
|
|
53
|
+
totalCostUsd: incoming.totalCostUsd ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const merged: UsageData = {
|
|
58
|
+
inputTokens: (existing.inputTokens ?? 0) + (incoming.inputTokens ?? 0),
|
|
59
|
+
outputTokens: (existing.outputTokens ?? 0) + (incoming.outputTokens ?? 0),
|
|
60
|
+
cacheReadInputTokens: (existing.cacheReadInputTokens ?? 0) + (incoming.cacheReadInputTokens ?? 0),
|
|
61
|
+
cacheCreationInputTokens: (existing.cacheCreationInputTokens ?? 0) + (incoming.cacheCreationInputTokens ?? 0),
|
|
62
|
+
modelUsage: {},
|
|
63
|
+
totalCostUsd: mergeCostUsd(existing.totalCostUsd, incoming.totalCostUsd),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const allModelIds = new Set([
|
|
67
|
+
...Object.keys(existing.modelUsage ?? {}),
|
|
68
|
+
...Object.keys(incoming.modelUsage ?? {}),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
for (const modelId of allModelIds) {
|
|
72
|
+
const existingModel = existing.modelUsage?.[modelId];
|
|
73
|
+
const incomingModel = incoming.modelUsage?.[modelId];
|
|
74
|
+
merged.modelUsage[modelId] = {
|
|
75
|
+
inputTokens: (existingModel?.inputTokens ?? 0) + (incomingModel?.inputTokens ?? 0),
|
|
76
|
+
outputTokens: (existingModel?.outputTokens ?? 0) + (incomingModel?.outputTokens ?? 0),
|
|
77
|
+
cacheReadInputTokens: (existingModel?.cacheReadInputTokens ?? 0) + (incomingModel?.cacheReadInputTokens ?? 0),
|
|
78
|
+
cacheCreationInputTokens: (existingModel?.cacheCreationInputTokens ?? 0) + (incomingModel?.cacheCreationInputTokens ?? 0),
|
|
79
|
+
costUsd: mergeCostUsd(existingModel?.costUsd, incomingModel?.costUsd),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
19
86
|
/**
|
|
20
87
|
* Sum multiple CostBreakdown objects into a single total.
|
|
21
88
|
*/
|
|
22
89
|
export function sumCostBreakdowns(costs: CostBreakdown[]): CostBreakdown {
|
|
23
90
|
let totalCost = 0;
|
|
24
91
|
for (const cost of costs) {
|
|
92
|
+
if (cost.totalCost === null) {
|
|
93
|
+
return { totalCost: null };
|
|
94
|
+
}
|
|
25
95
|
totalCost += cost.totalCost;
|
|
26
96
|
}
|
|
27
97
|
return { totalCost };
|
|
@@ -32,7 +102,12 @@ export function sumCostBreakdowns(costs: CostBreakdown[]): CostBreakdown {
|
|
|
32
102
|
* Sums all token fields and merges modelUsage maps.
|
|
33
103
|
*/
|
|
34
104
|
export function accumulateUsage(attempts: UsageData[]): UsageData {
|
|
35
|
-
|
|
105
|
+
let result: UsageData | undefined;
|
|
106
|
+
for (const attempt of attempts) {
|
|
107
|
+
result = mergeUsageData(result, attempt);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result ?? {
|
|
36
111
|
inputTokens: 0,
|
|
37
112
|
outputTokens: 0,
|
|
38
113
|
cacheReadInputTokens: 0,
|
|
@@ -40,32 +115,6 @@ export function accumulateUsage(attempts: UsageData[]): UsageData {
|
|
|
40
115
|
modelUsage: {},
|
|
41
116
|
totalCostUsd: 0,
|
|
42
117
|
};
|
|
43
|
-
|
|
44
|
-
for (const attempt of attempts) {
|
|
45
|
-
result.inputTokens += attempt.inputTokens;
|
|
46
|
-
result.outputTokens += attempt.outputTokens;
|
|
47
|
-
result.cacheReadInputTokens += attempt.cacheReadInputTokens;
|
|
48
|
-
result.cacheCreationInputTokens += attempt.cacheCreationInputTokens;
|
|
49
|
-
|
|
50
|
-
// Merge per-model usage
|
|
51
|
-
for (const [modelId, modelUsage] of Object.entries(attempt.modelUsage)) {
|
|
52
|
-
const existing = result.modelUsage[modelId];
|
|
53
|
-
if (existing) {
|
|
54
|
-
existing.inputTokens += modelUsage.inputTokens;
|
|
55
|
-
existing.outputTokens += modelUsage.outputTokens;
|
|
56
|
-
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
57
|
-
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
58
|
-
existing.costUsd += modelUsage.costUsd;
|
|
59
|
-
} else {
|
|
60
|
-
result.modelUsage[modelId] = { ...modelUsage };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Sum totalCostUsd across attempts
|
|
65
|
-
result.totalCostUsd += attempt.totalCostUsd;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return result;
|
|
69
118
|
}
|
|
70
119
|
|
|
71
120
|
/**
|
|
@@ -85,8 +134,7 @@ export class TokenTracker {
|
|
|
85
134
|
*/
|
|
86
135
|
addTask(taskId: string, attempts: UsageData[]): TaskUsageEntry {
|
|
87
136
|
const usage = accumulateUsage(attempts);
|
|
88
|
-
|
|
89
|
-
const totalCost = attempts.reduce((sum, attempt) => sum + attempt.totalCostUsd, 0);
|
|
137
|
+
const totalCost = usage.totalCostUsd;
|
|
90
138
|
const cost: CostBreakdown = { totalCost };
|
|
91
139
|
const entry: TaskUsageEntry = { taskId, usage, cost, attempts };
|
|
92
140
|
this.entries.push(entry);
|
|
@@ -104,43 +152,34 @@ export class TokenTracker {
|
|
|
104
152
|
* Get accumulated totals across all tasks.
|
|
105
153
|
*/
|
|
106
154
|
getTotals(): { usage: UsageData; cost: CostBreakdown } {
|
|
107
|
-
|
|
108
|
-
inputTokens: 0,
|
|
109
|
-
outputTokens: 0,
|
|
110
|
-
cacheReadInputTokens: 0,
|
|
111
|
-
cacheCreationInputTokens: 0,
|
|
112
|
-
modelUsage: {},
|
|
113
|
-
totalCostUsd: 0,
|
|
114
|
-
};
|
|
155
|
+
let totalUsage: UsageData | undefined;
|
|
115
156
|
const totalCost: CostBreakdown = {
|
|
116
157
|
totalCost: 0,
|
|
117
158
|
};
|
|
118
159
|
|
|
119
160
|
for (const entry of this.entries) {
|
|
120
|
-
totalUsage
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Merge per-model usage
|
|
127
|
-
for (const [modelId, modelUsage] of Object.entries(entry.usage.modelUsage)) {
|
|
128
|
-
const existing = totalUsage.modelUsage[modelId];
|
|
129
|
-
if (existing) {
|
|
130
|
-
existing.inputTokens += modelUsage.inputTokens;
|
|
131
|
-
existing.outputTokens += modelUsage.outputTokens;
|
|
132
|
-
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
133
|
-
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
134
|
-
existing.costUsd += modelUsage.costUsd;
|
|
161
|
+
totalUsage = mergeUsageData(totalUsage, entry.usage);
|
|
162
|
+
|
|
163
|
+
if (totalCost.totalCost !== null) {
|
|
164
|
+
if (entry.cost.totalCost === null) {
|
|
165
|
+
totalCost.totalCost = null;
|
|
135
166
|
} else {
|
|
136
|
-
|
|
167
|
+
totalCost.totalCost += entry.cost.totalCost;
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
|
-
|
|
140
|
-
totalCost.totalCost += entry.cost.totalCost;
|
|
141
170
|
}
|
|
142
171
|
|
|
143
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
usage: totalUsage ?? {
|
|
174
|
+
inputTokens: 0,
|
|
175
|
+
outputTokens: 0,
|
|
176
|
+
cacheReadInputTokens: 0,
|
|
177
|
+
cacheCreationInputTokens: 0,
|
|
178
|
+
modelUsage: {},
|
|
179
|
+
totalCostUsd: 0,
|
|
180
|
+
},
|
|
181
|
+
cost: totalCost,
|
|
182
|
+
};
|
|
144
183
|
}
|
|
145
184
|
|
|
146
185
|
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -2,9 +2,8 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
|
-
import type { ClaudeModelName
|
|
6
|
-
import {
|
|
7
|
-
import { getModel, isValidModelName } from './config.js';
|
|
5
|
+
import type { ClaudeModelName } from '../types/config.js';
|
|
6
|
+
import { isValidModelName } from './config.js';
|
|
8
7
|
|
|
9
8
|
export interface ValidationResult {
|
|
10
9
|
valid: boolean;
|
|
@@ -19,12 +18,12 @@ export function validateEnvironment(): ValidationResult {
|
|
|
19
18
|
errors: [],
|
|
20
19
|
};
|
|
21
20
|
|
|
22
|
-
// Check CLI
|
|
21
|
+
// Check that at least one supported CLI harness is installed
|
|
23
22
|
try {
|
|
24
|
-
execSync('which claude', { encoding: 'utf-8', stdio: 'pipe' });
|
|
23
|
+
execSync('which claude || which codex', { encoding: 'utf-8', stdio: 'pipe' });
|
|
25
24
|
} catch {
|
|
26
25
|
result.valid = false;
|
|
27
|
-
result.errors.push('CLI
|
|
26
|
+
result.errors.push('CLI harness not found. Please install Claude CLI or Codex CLI first.');
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
// Check for git repo (warning only)
|
|
@@ -101,27 +100,3 @@ export function validateModelName(model: string): ClaudeModelName | null {
|
|
|
101
100
|
}
|
|
102
101
|
return null;
|
|
103
102
|
}
|
|
104
|
-
|
|
105
|
-
export function resolveModelOption(model?: string, sonnet?: boolean, scenario: ModelScenario = 'execute'): ClaudeModelName {
|
|
106
|
-
// Check for conflicting flags
|
|
107
|
-
if (model && sonnet) {
|
|
108
|
-
throw new Error('Cannot specify both --model and --sonnet flags');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// --sonnet shorthand
|
|
112
|
-
if (sonnet) {
|
|
113
|
-
return 'sonnet';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// --model flag
|
|
117
|
-
if (model) {
|
|
118
|
-
const validated = validateModelName(model);
|
|
119
|
-
if (!validated) {
|
|
120
|
-
throw new Error(`Invalid model name: "${model}". Valid options: ${VALID_MODEL_ALIASES.join(', ')} or a full model ID (e.g., claude-sonnet-4-5-20250929), or harness-prefixed (e.g., codex/gpt-5.4)`);
|
|
121
|
-
}
|
|
122
|
-
return validated;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Default from config
|
|
126
|
-
return getModel(scenario);
|
|
127
|
-
}
|
|
@@ -65,7 +65,7 @@ describe('Amend Prompt', () => {
|
|
|
65
65
|
expect(systemPrompt).not.toContain('--worktree');
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('should show raf do
|
|
68
|
+
it('should show raf do without --worktree when worktreeMode is true', () => {
|
|
69
69
|
const params: AmendPromptParams = {
|
|
70
70
|
...baseParams,
|
|
71
71
|
worktreeMode: true,
|
|
@@ -73,7 +73,8 @@ describe('Amend Prompt', () => {
|
|
|
73
73
|
|
|
74
74
|
const { systemPrompt } = getAmendPrompt(params);
|
|
75
75
|
|
|
76
|
-
expect(systemPrompt).toContain('raf do <project>
|
|
76
|
+
expect(systemPrompt).toContain('raf do <project>');
|
|
77
|
+
expect(systemPrompt).not.toContain('--worktree');
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
it('should include new task description in user message', () => {
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const suiteHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-claude-home-'));
|
|
8
|
+
let mockHomeDir = suiteHomeDir;
|
|
9
|
+
|
|
10
|
+
jest.unstable_mockModule('node:os', () => ({
|
|
11
|
+
homedir: () => mockHomeDir,
|
|
12
|
+
tmpdir: () => os.tmpdir(),
|
|
13
|
+
}));
|
|
3
14
|
|
|
4
15
|
// Create mock pty spawn before importing ClaudeRunner
|
|
5
16
|
const mockPtySpawn = jest.fn();
|
|
@@ -16,6 +27,7 @@ jest.unstable_mockModule('node:child_process', () => ({
|
|
|
16
27
|
|
|
17
28
|
// Import after mocking
|
|
18
29
|
const { ClaudeRunner } = await import('../../src/core/claude-runner.js');
|
|
30
|
+
const { getModel, resetConfigCache } = await import('../../src/utils/config.js');
|
|
19
31
|
|
|
20
32
|
describe('ClaudeRunner - runInteractive', () => {
|
|
21
33
|
// Save original stdin/stdout for restoration
|
|
@@ -24,15 +36,22 @@ describe('ClaudeRunner - runInteractive', () => {
|
|
|
24
36
|
|
|
25
37
|
beforeEach(() => {
|
|
26
38
|
jest.clearAllMocks();
|
|
39
|
+
fs.rmSync(path.join(mockHomeDir, '.raf'), { recursive: true, force: true });
|
|
40
|
+
resetConfigCache();
|
|
27
41
|
mockExecSync.mockReturnValue('/usr/local/bin/claude\n');
|
|
28
42
|
});
|
|
29
43
|
|
|
30
44
|
afterEach(() => {
|
|
45
|
+
resetConfigCache();
|
|
31
46
|
// Restore stdin/stdout
|
|
32
47
|
Object.defineProperty(process, 'stdin', { value: originalStdin });
|
|
33
48
|
Object.defineProperty(process, 'stdout', { value: originalStdout });
|
|
34
49
|
});
|
|
35
50
|
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
fs.rmSync(suiteHomeDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
36
55
|
/**
|
|
37
56
|
* Creates a mock PTY process for testing.
|
|
38
57
|
*/
|
|
@@ -129,7 +148,7 @@ describe('ClaudeRunner - runInteractive', () => {
|
|
|
129
148
|
await runPromise;
|
|
130
149
|
});
|
|
131
150
|
|
|
132
|
-
it('should use
|
|
151
|
+
it('should use the configured execute model by default', async () => {
|
|
133
152
|
const mockProc = createMockPtyProcess();
|
|
134
153
|
const mockStdin = createMockStdin();
|
|
135
154
|
const mockStdout = createMockStdout();
|
|
@@ -144,9 +163,8 @@ describe('ClaudeRunner - runInteractive', () => {
|
|
|
144
163
|
|
|
145
164
|
const spawnArgs = mockPtySpawn.mock.calls[0][1] as string[];
|
|
146
165
|
expect(spawnArgs).toContain('--model');
|
|
147
|
-
// Default model comes from config, could be short alias or full model ID
|
|
148
166
|
const modelArgIndex = spawnArgs.indexOf('--model');
|
|
149
|
-
expect(spawnArgs[modelArgIndex + 1]).
|
|
167
|
+
expect(spawnArgs[modelArgIndex + 1]).toBe(getModel('execute').model);
|
|
150
168
|
|
|
151
169
|
mockProc._exitCallback({ exitCode: 0 });
|
|
152
170
|
await runPromise;
|
|
@@ -762,6 +762,45 @@ describe('ClaudeRunner', () => {
|
|
|
762
762
|
expect(result.usageData!.outputTokens).toBe(800);
|
|
763
763
|
});
|
|
764
764
|
|
|
765
|
+
it('should accumulate usageData across multiple result events', async () => {
|
|
766
|
+
const mockProc = createMockProcess();
|
|
767
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
768
|
+
|
|
769
|
+
const runner = new ClaudeRunner();
|
|
770
|
+
const runPromise = runner.run('test prompt', { timeout: 60 });
|
|
771
|
+
|
|
772
|
+
const firstResultEvent = JSON.stringify({
|
|
773
|
+
type: 'result',
|
|
774
|
+
usage: { input_tokens: 1000, output_tokens: 500, cache_read_input_tokens: 100, cache_creation_input_tokens: 50 },
|
|
775
|
+
modelUsage: { 'claude-opus-4-6': { inputTokens: 1000, outputTokens: 500, cacheReadInputTokens: 100, cacheCreationInputTokens: 50 } },
|
|
776
|
+
total_cost_usd: 2.5,
|
|
777
|
+
});
|
|
778
|
+
const secondResultEvent = JSON.stringify({
|
|
779
|
+
type: 'result',
|
|
780
|
+
usage: { input_tokens: 600, output_tokens: 300, cache_read_input_tokens: 40, cache_creation_input_tokens: 20 },
|
|
781
|
+
modelUsage: { 'claude-opus-4-6': { inputTokens: 600, outputTokens: 300, cacheReadInputTokens: 40, cacheCreationInputTokens: 20 } },
|
|
782
|
+
total_cost_usd: 1.5,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
mockProc.stdout.emit('data', Buffer.from(firstResultEvent + '\n' + secondResultEvent + '\n'));
|
|
786
|
+
mockProc.emit('close', 0);
|
|
787
|
+
|
|
788
|
+
const result = await runPromise;
|
|
789
|
+
expect(result.usageData).toBeDefined();
|
|
790
|
+
expect(result.usageData!.inputTokens).toBe(1600);
|
|
791
|
+
expect(result.usageData!.outputTokens).toBe(800);
|
|
792
|
+
expect(result.usageData!.cacheReadInputTokens).toBe(140);
|
|
793
|
+
expect(result.usageData!.cacheCreationInputTokens).toBe(70);
|
|
794
|
+
expect(result.usageData!.totalCostUsd).toBe(4);
|
|
795
|
+
expect(result.usageData!.modelUsage['claude-opus-4-6']).toEqual({
|
|
796
|
+
inputTokens: 1600,
|
|
797
|
+
outputTokens: 800,
|
|
798
|
+
cacheReadInputTokens: 140,
|
|
799
|
+
cacheCreationInputTokens: 70,
|
|
800
|
+
costUsd: 0,
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
765
804
|
it('should return undefined usageData when no result event', async () => {
|
|
766
805
|
const mockProc = createMockProcess();
|
|
767
806
|
mockSpawn.mockReturnValue(mockProc);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
const mockSpawn = jest.fn();
|
|
5
|
+
const mockExecSync = jest.fn();
|
|
6
|
+
|
|
7
|
+
const mockExistsSync = jest.fn();
|
|
8
|
+
const mockStatSync = jest.fn();
|
|
9
|
+
const mockReadFileSync = jest.fn();
|
|
10
|
+
|
|
11
|
+
const mockGetHeadCommitHash = jest.fn();
|
|
12
|
+
const mockGetHeadCommitMessage = jest.fn();
|
|
13
|
+
const mockIsFileCommittedInHead = jest.fn();
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule('node:child_process', () => ({
|
|
16
|
+
spawn: mockSpawn,
|
|
17
|
+
execSync: mockExecSync,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.unstable_mockModule('node-pty', () => ({
|
|
21
|
+
spawn: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.unstable_mockModule('node:fs', () => ({
|
|
25
|
+
default: {
|
|
26
|
+
existsSync: mockExistsSync,
|
|
27
|
+
statSync: mockStatSync,
|
|
28
|
+
readFileSync: mockReadFileSync,
|
|
29
|
+
},
|
|
30
|
+
existsSync: mockExistsSync,
|
|
31
|
+
statSync: mockStatSync,
|
|
32
|
+
readFileSync: mockReadFileSync,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.unstable_mockModule('../../src/core/git.js', () => ({
|
|
36
|
+
getHeadCommitHash: mockGetHeadCommitHash,
|
|
37
|
+
getHeadCommitMessage: mockGetHeadCommitMessage,
|
|
38
|
+
isFileCommittedInHead: mockIsFileCommittedInHead,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { CodexRunner } = await import('../../src/core/codex-runner.js');
|
|
42
|
+
|
|
43
|
+
function createMockProcess() {
|
|
44
|
+
const stdout = new EventEmitter();
|
|
45
|
+
const stderr = new EventEmitter();
|
|
46
|
+
const proc = new EventEmitter() as any;
|
|
47
|
+
proc.stdout = stdout;
|
|
48
|
+
proc.stderr = stderr;
|
|
49
|
+
proc.kill = jest.fn().mockImplementation(() => {
|
|
50
|
+
setImmediate(() => proc.emit('close', 1));
|
|
51
|
+
});
|
|
52
|
+
return proc;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('CodexRunner', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
mockExecSync.mockReturnValue('/usr/local/bin/codex\n');
|
|
59
|
+
mockExistsSync.mockReturnValue(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns usageData from run() when turn.completed includes usage', async () => {
|
|
63
|
+
const mockProc = createMockProcess();
|
|
64
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
65
|
+
|
|
66
|
+
const runner = new CodexRunner({ model: 'gpt-5.4' });
|
|
67
|
+
const runPromise = runner.run('test prompt');
|
|
68
|
+
|
|
69
|
+
mockProc.stdout.emit('data', Buffer.from(`${JSON.stringify({
|
|
70
|
+
type: 'turn.completed',
|
|
71
|
+
model: 'gpt-5.4',
|
|
72
|
+
usage: {
|
|
73
|
+
input_tokens: 1000,
|
|
74
|
+
output_tokens: 250,
|
|
75
|
+
},
|
|
76
|
+
})}\n`));
|
|
77
|
+
mockProc.emit('close', 0);
|
|
78
|
+
|
|
79
|
+
const result = await runPromise;
|
|
80
|
+
expect(result.usageData).toEqual({
|
|
81
|
+
inputTokens: 1000,
|
|
82
|
+
outputTokens: 250,
|
|
83
|
+
cacheReadInputTokens: 0,
|
|
84
|
+
cacheCreationInputTokens: 0,
|
|
85
|
+
modelUsage: {
|
|
86
|
+
'gpt-5.4': {
|
|
87
|
+
inputTokens: 1000,
|
|
88
|
+
outputTokens: 250,
|
|
89
|
+
cacheReadInputTokens: 0,
|
|
90
|
+
cacheCreationInputTokens: 0,
|
|
91
|
+
costUsd: null,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
totalCostUsd: null,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('accumulates usageData across multiple turn.completed events', async () => {
|
|
99
|
+
const mockProc = createMockProcess();
|
|
100
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
101
|
+
|
|
102
|
+
const runner = new CodexRunner({ model: 'gpt-5.4' });
|
|
103
|
+
const runPromise = runner.run('test prompt');
|
|
104
|
+
|
|
105
|
+
const firstTurn = JSON.stringify({
|
|
106
|
+
type: 'turn.completed',
|
|
107
|
+
model: 'gpt-5.4',
|
|
108
|
+
usage: {
|
|
109
|
+
input_tokens: 1000,
|
|
110
|
+
output_tokens: 250,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const secondTurn = JSON.stringify({
|
|
114
|
+
type: 'turn.completed',
|
|
115
|
+
model: 'gpt-5.4',
|
|
116
|
+
usage: {
|
|
117
|
+
input_tokens: 500,
|
|
118
|
+
output_tokens: 150,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
mockProc.stdout.emit('data', Buffer.from(firstTurn + '\n' + secondTurn + '\n'));
|
|
123
|
+
mockProc.emit('close', 0);
|
|
124
|
+
|
|
125
|
+
const result = await runPromise;
|
|
126
|
+
expect(result.usageData).toEqual({
|
|
127
|
+
inputTokens: 1500,
|
|
128
|
+
outputTokens: 400,
|
|
129
|
+
cacheReadInputTokens: 0,
|
|
130
|
+
cacheCreationInputTokens: 0,
|
|
131
|
+
modelUsage: {
|
|
132
|
+
'gpt-5.4': {
|
|
133
|
+
inputTokens: 1500,
|
|
134
|
+
outputTokens: 400,
|
|
135
|
+
cacheReadInputTokens: 0,
|
|
136
|
+
cacheCreationInputTokens: 0,
|
|
137
|
+
costUsd: null,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
totalCostUsd: null,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns undefined usageData when no turn.completed usage event is present', async () => {
|
|
145
|
+
const mockProc = createMockProcess();
|
|
146
|
+
mockSpawn.mockReturnValue(mockProc);
|
|
147
|
+
|
|
148
|
+
const runner = new CodexRunner({ model: 'gpt-5.4' });
|
|
149
|
+
const runPromise = runner.run('test prompt');
|
|
150
|
+
|
|
151
|
+
mockProc.stdout.emit('data', Buffer.from(`${JSON.stringify({
|
|
152
|
+
type: 'item.completed',
|
|
153
|
+
item: {
|
|
154
|
+
type: 'agent_message',
|
|
155
|
+
text: 'Done.',
|
|
156
|
+
},
|
|
157
|
+
})}\n`));
|
|
158
|
+
mockProc.emit('close', 0);
|
|
159
|
+
|
|
160
|
+
const result = await runPromise;
|
|
161
|
+
expect(result.usageData).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|