@golba98/codexa 1.0.2 → 1.0.4
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/README.md +396 -100
- package/bin/codexa.js +62 -144
- package/package.json +14 -8
- package/src/app.tsx +596 -306
- package/src/commands/handler.ts +6 -6
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/persistence.ts +10 -0
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +8 -16
- package/src/config/trustStore.ts +1 -1
- package/src/config/updateCheckCache.ts +19 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +65 -43
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/version/channel.ts +23 -0
- package/src/core/version/updateCheck.ts +193 -0
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +3 -2
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +670 -719
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +158 -118
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +222 -42
- package/src/ui/ModelReasoningPicker.tsx +15 -15
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +9 -9
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +8 -4
- package/src/ui/SettingsPanel.tsx +9 -9
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +11 -11
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +1625 -1472
- package/src/ui/TopHeader.tsx +437 -293
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +33 -33
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +197 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +299 -25
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +103 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +112 -0
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +218 -136
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codex.ts +0 -124
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/updateCheck.ts +0 -172
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
|
@@ -1,897 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import type { BackendProgressUpdate } from "../core/providers/types.js";
|
|
4
|
-
import type { AssistantEvent, RunEvent, TimelineEvent, UserPromptEvent } from "./types.js";
|
|
5
|
-
import { getRunPlanText, isBusy } from "./types.js";
|
|
6
|
-
import { createInitialSessionState, reduceSessionState, type SessionState } from "./appSession.js";
|
|
7
|
-
import { TEST_RUNTIME } from "../test/runtimeTestUtils.js";
|
|
8
|
-
import { isAnimatedBusyState } from "../ui/busyStatusAnimation.js";
|
|
9
|
-
|
|
10
|
-
function makeUserEvent(turnId: number): UserPromptEvent {
|
|
11
|
-
return { id: 1, type: "user", createdAt: 1, prompt: "Do work", turnId };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function makeRunEvent(turnId: number): RunEvent {
|
|
15
|
-
return {
|
|
16
|
-
id: 2,
|
|
17
|
-
type: "run",
|
|
18
|
-
createdAt: 2,
|
|
19
|
-
startedAt: 2,
|
|
20
|
-
durationMs: null,
|
|
21
|
-
backendId: "codex-subprocess",
|
|
22
|
-
backendLabel: "Codexa",
|
|
23
|
-
runtime: TEST_RUNTIME,
|
|
24
|
-
prompt: "Do work",
|
|
25
|
-
progressEntries: [],
|
|
26
|
-
status: "running",
|
|
27
|
-
summary: "Running",
|
|
28
|
-
truncatedOutput: false,
|
|
29
|
-
toolActivities: [],
|
|
30
|
-
activity: [],
|
|
31
|
-
touchedFileCount: 0,
|
|
32
|
-
errorMessage: null,
|
|
33
|
-
turnId,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeAssistantEvent(turnId: number, content: string): AssistantEvent {
|
|
38
|
-
return { id: 3, type: "assistant", createdAt: 3, content, contentChunks: [], turnId };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function makeProgressUpdate(
|
|
42
|
-
id: string,
|
|
43
|
-
text: string,
|
|
44
|
-
source: BackendProgressUpdate["source"] = "reasoning",
|
|
45
|
-
): BackendProgressUpdate {
|
|
46
|
-
return {
|
|
47
|
-
id,
|
|
48
|
-
source,
|
|
49
|
-
text,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function stateWithActiveRun(turnId: number): SessionState {
|
|
54
|
-
const state = createInitialSessionState();
|
|
55
|
-
return {
|
|
56
|
-
...state,
|
|
57
|
-
activeEvents: [
|
|
58
|
-
makeUserEvent(turnId),
|
|
59
|
-
makeRunEvent(turnId),
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
test("SUBMIT_PROMPT_RUN atomically clears composer, records history, appends one turn, and enters thinking", () => {
|
|
65
|
-
const turnId = 50;
|
|
66
|
-
const runId = 2;
|
|
67
|
-
const initial: SessionState = {
|
|
68
|
-
...createInitialSessionState(),
|
|
69
|
-
inputValue: "hello",
|
|
70
|
-
cursor: 5,
|
|
71
|
-
history: ["older"],
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const state = reduceSessionState(initial, {
|
|
75
|
-
type: "SUBMIT_PROMPT_RUN",
|
|
76
|
-
historyValue: "hello",
|
|
77
|
-
turnId,
|
|
78
|
-
runId,
|
|
79
|
-
events: [makeUserEvent(turnId), makeRunEvent(turnId)],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
assert.equal(state.inputValue, "");
|
|
83
|
-
assert.equal(state.cursor, 0);
|
|
84
|
-
assert.deepEqual(state.history, ["hello", "older"]);
|
|
85
|
-
assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run"]);
|
|
86
|
-
assert.equal(state.activeEvents.filter((event) => event.type === "user").length, 1);
|
|
87
|
-
assert.equal(state.activeEvents.filter((event) => event.type === "run").length, 1);
|
|
88
|
-
assert.deepEqual(state.uiState, { kind: "THINKING", turnId });
|
|
89
|
-
assert.deepEqual(state.staticEvents, []);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("busy lifecycle preserves one canonical active turn until finalization", () => {
|
|
93
|
-
const turnId = 51;
|
|
94
|
-
let state = reduceSessionState(createInitialSessionState(), {
|
|
95
|
-
type: "SUBMIT_PROMPT_RUN",
|
|
96
|
-
historyValue: "hello",
|
|
97
|
-
turnId,
|
|
98
|
-
runId: 2,
|
|
99
|
-
events: [makeUserEvent(turnId), makeRunEvent(turnId)],
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
state = reduceSessionState(state, {
|
|
103
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
104
|
-
turnId,
|
|
105
|
-
runId: 2,
|
|
106
|
-
chunk: "Hello",
|
|
107
|
-
eventFactory: () => makeAssistantEvent(turnId, "Hello"),
|
|
108
|
-
});
|
|
109
|
-
state = reduceSessionState(state, {
|
|
110
|
-
type: "RUN_MARK_FINAL_ANSWER_OBSERVED",
|
|
111
|
-
runId: 2,
|
|
112
|
-
turnId,
|
|
113
|
-
response: "Hello",
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run", "assistant"]);
|
|
117
|
-
assert.equal(state.activeEvents.filter((event) => event.type === "user").length, 1);
|
|
118
|
-
assert.equal(state.activeEvents.filter((event) => event.type === "assistant").length, 1);
|
|
119
|
-
assert.deepEqual(state.staticEvents, []);
|
|
120
|
-
|
|
121
|
-
state = reduceSessionState(state, {
|
|
122
|
-
type: "FINALIZE_RUN",
|
|
123
|
-
runId: 2,
|
|
124
|
-
turnId,
|
|
125
|
-
status: "completed",
|
|
126
|
-
response: undefined,
|
|
127
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
assert.deepEqual(state.activeEvents, []);
|
|
131
|
-
assert.deepEqual(state.staticEvents.map((event) => event.type), ["user", "run", "assistant"]);
|
|
132
|
-
assert.equal(state.staticEvents.filter((event) => event.type === "user").length, 1);
|
|
133
|
-
assert.equal(state.staticEvents.filter((event) => event.type === "assistant").length, 1);
|
|
134
|
-
assert.equal(state.uiState.kind, "IDLE");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("CLEAR_TRANSCRIPT removes all rendered transcript event state and preserves prompt history", () => {
|
|
138
|
-
const turnId = 7;
|
|
139
|
-
const shellEvent: TimelineEvent = {
|
|
140
|
-
id: 10,
|
|
141
|
-
type: "shell",
|
|
142
|
-
createdAt: 10,
|
|
143
|
-
command: "echo stale",
|
|
144
|
-
lines: ["stale shell output"],
|
|
145
|
-
stderrLines: [],
|
|
146
|
-
summary: "Executed shell",
|
|
147
|
-
status: "completed",
|
|
148
|
-
exitCode: 0,
|
|
149
|
-
durationMs: 20,
|
|
150
|
-
};
|
|
151
|
-
const state: SessionState = {
|
|
152
|
-
...createInitialSessionState(),
|
|
153
|
-
staticEvents: [
|
|
154
|
-
makeUserEvent(turnId),
|
|
155
|
-
{ ...makeRunEvent(turnId), status: "completed", durationMs: 100 },
|
|
156
|
-
makeAssistantEvent(turnId, "stale assistant"),
|
|
157
|
-
shellEvent,
|
|
158
|
-
{ id: 11, type: "system", createdAt: 11, title: "Mode", content: "Updated" },
|
|
159
|
-
],
|
|
160
|
-
activeEvents: [
|
|
161
|
-
makeUserEvent(turnId + 1),
|
|
162
|
-
makeRunEvent(turnId + 1),
|
|
163
|
-
makeAssistantEvent(turnId + 1, "streaming"),
|
|
164
|
-
],
|
|
165
|
-
uiState: { kind: "RESPONDING", turnId: turnId + 1 },
|
|
166
|
-
inputValue: "/clear",
|
|
167
|
-
cursor: 6,
|
|
168
|
-
history: ["Hello"],
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const cleared = reduceSessionState(state, { type: "CLEAR_TRANSCRIPT" });
|
|
172
|
-
|
|
173
|
-
assert.deepEqual(cleared.staticEvents, []);
|
|
174
|
-
assert.deepEqual(cleared.activeEvents, []);
|
|
175
|
-
assert.deepEqual(cleared.uiState, { kind: "IDLE" });
|
|
176
|
-
assert.equal(cleared.clearCount, state.clearCount + 1);
|
|
177
|
-
assert.equal(cleared.clearEpoch, state.clearEpoch + 1);
|
|
178
|
-
assert.deepEqual(cleared.history, ["Hello"]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("FINALIZE_RUN preserves streamed content when response is undefined", () => {
|
|
182
|
-
const turnId = 1;
|
|
183
|
-
let state = stateWithActiveRun(turnId);
|
|
184
|
-
|
|
185
|
-
// Simulate streaming: append assistant delta
|
|
186
|
-
state = reduceSessionState(state, {
|
|
187
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
188
|
-
turnId,
|
|
189
|
-
runId: 2,
|
|
190
|
-
chunk: "Streamed response text",
|
|
191
|
-
eventFactory: () => makeAssistantEvent(turnId, "Streamed response text"),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Finalize with undefined response — should preserve streamed content
|
|
195
|
-
state = reduceSessionState(state, {
|
|
196
|
-
type: "FINALIZE_RUN",
|
|
197
|
-
runId: 2,
|
|
198
|
-
turnId,
|
|
199
|
-
status: "completed",
|
|
200
|
-
response: undefined,
|
|
201
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const assistantEvent = state.staticEvents.find(
|
|
205
|
-
(e): e is AssistantEvent => e.type === "assistant",
|
|
206
|
-
);
|
|
207
|
-
assert.ok(assistantEvent, "Assistant event should exist in static events");
|
|
208
|
-
assert.equal(assistantEvent.content, "Streamed response text");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("FINALIZE_RUN stores the fixed elapsed duration supplied by the app", () => {
|
|
212
|
-
const turnId = 11;
|
|
213
|
-
const state = reduceSessionState(stateWithActiveRun(turnId), {
|
|
214
|
-
type: "FINALIZE_RUN",
|
|
215
|
-
runId: 2,
|
|
216
|
-
turnId,
|
|
217
|
-
status: "completed",
|
|
218
|
-
durationMs: 9876,
|
|
219
|
-
response: "Done",
|
|
220
|
-
assistantFactory: () => makeAssistantEvent(turnId, "Done"),
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
224
|
-
assert.ok(runEvent);
|
|
225
|
-
assert.equal(runEvent.durationMs, 9876);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("FINALIZE_RUN replaces streamed content when response differs", () => {
|
|
229
|
-
const turnId = 2;
|
|
230
|
-
let state = stateWithActiveRun(turnId);
|
|
231
|
-
|
|
232
|
-
// Simulate streaming
|
|
233
|
-
state = reduceSessionState(state, {
|
|
234
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
235
|
-
turnId,
|
|
236
|
-
runId: 2,
|
|
237
|
-
chunk: "Partial streamed text",
|
|
238
|
-
eventFactory: () => makeAssistantEvent(turnId, "Partial streamed text"),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Finalize with different response — should replace
|
|
242
|
-
state = reduceSessionState(state, {
|
|
243
|
-
type: "FINALIZE_RUN",
|
|
244
|
-
runId: 2,
|
|
245
|
-
turnId,
|
|
246
|
-
status: "completed",
|
|
247
|
-
response: "Full sanitized response with additional content",
|
|
248
|
-
assistantFactory: () => makeAssistantEvent(turnId, "Full sanitized response with additional content"),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const assistantEvent = state.staticEvents.find(
|
|
252
|
-
(e): e is AssistantEvent => e.type === "assistant",
|
|
253
|
-
);
|
|
254
|
-
assert.ok(assistantEvent, "Assistant event should exist in static events");
|
|
255
|
-
assert.equal(assistantEvent.content, "Full sanitized response with additional content");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("RUN_APPEND_ASSISTANT_DELTA transitions UI state to RESPONDING", () => {
|
|
259
|
-
const turnId = 3;
|
|
260
|
-
let state = stateWithActiveRun(turnId);
|
|
261
|
-
state = reduceSessionState(state, {
|
|
262
|
-
type: "UI_ACTION",
|
|
263
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
264
|
-
});
|
|
265
|
-
assert.equal(state.uiState.kind, "THINKING");
|
|
266
|
-
|
|
267
|
-
state = reduceSessionState(state, {
|
|
268
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
269
|
-
turnId,
|
|
270
|
-
runId: 2,
|
|
271
|
-
chunk: "First chunk",
|
|
272
|
-
eventFactory: () => makeAssistantEvent(turnId, "First chunk"),
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
assert.equal(state.uiState.kind, "RESPONDING");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("FINALIZE_RUN after assistant delta transitions UI state to IDLE", () => {
|
|
279
|
-
const turnId = 35;
|
|
280
|
-
let state = stateWithActiveRun(turnId);
|
|
281
|
-
state = reduceSessionState(state, {
|
|
282
|
-
type: "UI_ACTION",
|
|
283
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
284
|
-
});
|
|
285
|
-
state = reduceSessionState(state, {
|
|
286
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
287
|
-
turnId,
|
|
288
|
-
runId: 2,
|
|
289
|
-
chunk: "Done.",
|
|
290
|
-
eventFactory: () => makeAssistantEvent(turnId, "Done."),
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
state = reduceSessionState(state, {
|
|
294
|
-
type: "FINALIZE_RUN",
|
|
295
|
-
runId: 2,
|
|
296
|
-
turnId,
|
|
297
|
-
status: "completed",
|
|
298
|
-
response: undefined,
|
|
299
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
assert.equal(state.uiState.kind, "IDLE");
|
|
303
|
-
assert.equal(isAnimatedBusyState(state.uiState.kind), false);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("RUN_MARK_FINAL_ANSWER_OBSERVED completes visible answer without thinking animation", () => {
|
|
307
|
-
const turnId = 36;
|
|
308
|
-
let state = stateWithActiveRun(turnId);
|
|
309
|
-
state = reduceSessionState(state, {
|
|
310
|
-
type: "UI_ACTION",
|
|
311
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
312
|
-
});
|
|
313
|
-
state = reduceSessionState(state, {
|
|
314
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
315
|
-
turnId,
|
|
316
|
-
runId: 2,
|
|
317
|
-
chunk: "READY",
|
|
318
|
-
eventFactory: () => makeAssistantEvent(turnId, "READY"),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
state = reduceSessionState(state, {
|
|
322
|
-
type: "RUN_MARK_FINAL_ANSWER_OBSERVED",
|
|
323
|
-
runId: 2,
|
|
324
|
-
turnId,
|
|
325
|
-
response: "READY",
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
assert.equal(state.uiState.kind, "ANSWER_VISIBLE");
|
|
329
|
-
assert.equal(isAnimatedBusyState(state.uiState.kind), false);
|
|
330
|
-
assert.equal(isBusy(state.uiState), true);
|
|
331
|
-
const run = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
332
|
-
assert.ok(run);
|
|
333
|
-
assert.equal(run.responseSegments?.[0]?.status, "completed");
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("plan deltas update one plan block and FINALIZE_RUN does not create assistant response", () => {
|
|
337
|
-
const turnId = 37;
|
|
338
|
-
let state = createInitialSessionState();
|
|
339
|
-
const run: RunEvent = {
|
|
340
|
-
...makeRunEvent(turnId),
|
|
341
|
-
plan: {
|
|
342
|
-
id: "plan-2",
|
|
343
|
-
streamSeq: 1,
|
|
344
|
-
chunks: [],
|
|
345
|
-
status: "active",
|
|
346
|
-
startedAt: 2,
|
|
347
|
-
},
|
|
348
|
-
streamItems: [{ streamSeq: 1, kind: "plan", refId: "plan-2" }],
|
|
349
|
-
responseSegments: [],
|
|
350
|
-
lastStreamSeq: 1,
|
|
351
|
-
activeResponseSegmentId: null,
|
|
352
|
-
};
|
|
353
|
-
state = {
|
|
354
|
-
...state,
|
|
355
|
-
activeEvents: [makeUserEvent(turnId), run],
|
|
356
|
-
};
|
|
357
|
-
state = reduceSessionState(state, {
|
|
358
|
-
type: "UI_ACTION",
|
|
359
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
state = reduceSessionState(state, {
|
|
363
|
-
type: "RUN_APPEND_PLAN_DELTA",
|
|
364
|
-
turnId,
|
|
365
|
-
runId: 2,
|
|
366
|
-
chunk: "1. Inspect\n",
|
|
367
|
-
});
|
|
368
|
-
state = reduceSessionState(state, {
|
|
369
|
-
type: "RUN_APPEND_PLAN_DELTA",
|
|
370
|
-
turnId,
|
|
371
|
-
runId: 2,
|
|
372
|
-
chunk: "2. Render panel",
|
|
373
|
-
});
|
|
374
|
-
state = reduceSessionState(state, {
|
|
375
|
-
type: "RUN_UPSERT_TOOL_ACTIVITY",
|
|
376
|
-
runId: 2,
|
|
377
|
-
activity: {
|
|
378
|
-
id: "tool-1",
|
|
379
|
-
command: "Get-Content src/app.tsx",
|
|
380
|
-
status: "completed",
|
|
381
|
-
startedAt: 10,
|
|
382
|
-
completedAt: 20,
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const activeRun = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
387
|
-
assert.ok(activeRun);
|
|
388
|
-
assert.deepEqual(activeRun.streamItems?.map((item) => item.kind), ["plan", "action"]);
|
|
389
|
-
assert.equal(getRunPlanText(activeRun.plan), "1. Inspect\n2. Render panel");
|
|
390
|
-
|
|
391
|
-
state = reduceSessionState(state, {
|
|
392
|
-
type: "FINALIZE_RUN",
|
|
393
|
-
runId: 2,
|
|
394
|
-
turnId,
|
|
395
|
-
status: "completed",
|
|
396
|
-
response: "1. Inspect\n2. Render panel",
|
|
397
|
-
responsePresentation: "plan",
|
|
398
|
-
assistantFactory: () => makeAssistantEvent(turnId, "should not render"),
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
402
|
-
assert.ok(finalizedRun);
|
|
403
|
-
assert.equal(finalizedRun.plan?.status, "completed");
|
|
404
|
-
assert.equal(getRunPlanText(finalizedRun.plan), "1. Inspect\n2. Render panel");
|
|
405
|
-
assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["action", "plan"]);
|
|
406
|
-
assert.deepEqual(finalizedRun.streamItems?.map((item) => item.streamSeq), [2, 3]);
|
|
407
|
-
assert.equal(state.staticEvents.some((event) => event.type === "assistant"), false);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("FINALIZE_RUN with plan presentation creates visible plan from final response without deltas", () => {
|
|
411
|
-
const turnId = 38;
|
|
412
|
-
let state = stateWithActiveRun(turnId);
|
|
413
|
-
state = reduceSessionState(state, {
|
|
414
|
-
type: "RUN_UPSERT_TOOL_ACTIVITY",
|
|
415
|
-
runId: 2,
|
|
416
|
-
activity: {
|
|
417
|
-
id: "tool-1",
|
|
418
|
-
command: "rg --files",
|
|
419
|
-
status: "completed",
|
|
420
|
-
startedAt: 10,
|
|
421
|
-
completedAt: 20,
|
|
422
|
-
},
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
state = reduceSessionState(state, {
|
|
426
|
-
type: "FINALIZE_RUN",
|
|
427
|
-
runId: 2,
|
|
428
|
-
turnId,
|
|
429
|
-
status: "completed",
|
|
430
|
-
response: "1. Inspect files\n2. Update the timeline cache",
|
|
431
|
-
responsePresentation: "plan",
|
|
432
|
-
assistantFactory: () => makeAssistantEvent(turnId, "should not render"),
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
436
|
-
assert.ok(finalizedRun);
|
|
437
|
-
assert.equal(finalizedRun.plan?.status, "completed");
|
|
438
|
-
assert.equal(getRunPlanText(finalizedRun.plan), "1. Inspect files\n2. Update the timeline cache");
|
|
439
|
-
assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["action", "plan"]);
|
|
440
|
-
assert.equal(state.staticEvents.some((event) => event.type === "assistant"), false);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test("FINALIZE_RUN for approved plan execution keeps approved plan and records assistant response", () => {
|
|
444
|
-
const turnId = 39;
|
|
445
|
-
let state = createInitialSessionState();
|
|
446
|
-
const approvedPlan = "1. Inspect files\n2. Render panel";
|
|
447
|
-
state = {
|
|
448
|
-
...state,
|
|
449
|
-
activeEvents: [
|
|
450
|
-
makeUserEvent(turnId),
|
|
451
|
-
{
|
|
452
|
-
...makeRunEvent(turnId),
|
|
453
|
-
plan: {
|
|
454
|
-
id: "plan-2",
|
|
455
|
-
streamSeq: 1,
|
|
456
|
-
chunks: [approvedPlan],
|
|
457
|
-
status: "completed",
|
|
458
|
-
startedAt: 2,
|
|
459
|
-
},
|
|
460
|
-
approvedPlan,
|
|
461
|
-
streamItems: [{ streamSeq: 1, kind: "plan", refId: "plan-2" }],
|
|
462
|
-
responseSegments: [],
|
|
463
|
-
lastStreamSeq: 1,
|
|
464
|
-
activeResponseSegmentId: null,
|
|
465
|
-
},
|
|
466
|
-
],
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
state = reduceSessionState(state, {
|
|
470
|
-
type: "FINALIZE_RUN",
|
|
471
|
-
runId: 2,
|
|
472
|
-
turnId,
|
|
473
|
-
status: "completed",
|
|
474
|
-
response: "Implemented the approved plan.",
|
|
475
|
-
assistantFactory: () => makeAssistantEvent(turnId, "Implemented the approved plan."),
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
479
|
-
const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
|
|
480
|
-
assert.ok(finalizedRun);
|
|
481
|
-
assert.ok(finalizedAssistant);
|
|
482
|
-
assert.equal(getRunPlanText(finalizedRun.plan), approvedPlan);
|
|
483
|
-
assert.deepEqual(finalizedRun.streamItems?.map((item) => item.kind), ["plan", "response"]);
|
|
484
|
-
assert.equal(finalizedAssistant.content, "Implemented the approved plan.");
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
test("FINALIZE_RUN for canceled thinking-only run transitions UI state to IDLE", () => {
|
|
488
|
-
const turnId = 36;
|
|
489
|
-
let state = stateWithActiveRun(turnId);
|
|
490
|
-
state = reduceSessionState(state, {
|
|
491
|
-
type: "UI_ACTION",
|
|
492
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
state = reduceSessionState(state, {
|
|
496
|
-
type: "FINALIZE_RUN",
|
|
497
|
-
runId: 2,
|
|
498
|
-
turnId,
|
|
499
|
-
status: "canceled",
|
|
500
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
assert.equal(state.uiState.kind, "IDLE");
|
|
504
|
-
assert.equal(isAnimatedBusyState(state.uiState.kind), false);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test("FINALIZE_RUN for failed run records error and exits busy animation", () => {
|
|
508
|
-
const turnId = 37;
|
|
509
|
-
let state = stateWithActiveRun(turnId);
|
|
510
|
-
state = reduceSessionState(state, {
|
|
511
|
-
type: "UI_ACTION",
|
|
512
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
state = reduceSessionState(state, {
|
|
516
|
-
type: "FINALIZE_RUN",
|
|
517
|
-
runId: 2,
|
|
518
|
-
turnId,
|
|
519
|
-
status: "failed",
|
|
520
|
-
message: "Provider exploded",
|
|
521
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
525
|
-
assert.ok(runEvent);
|
|
526
|
-
assert.equal(runEvent.status, "failed");
|
|
527
|
-
assert.equal(runEvent.errorMessage, "Provider exploded");
|
|
528
|
-
assert.deepEqual(state.uiState, { kind: "ERROR", turnId, message: "Provider exploded" });
|
|
529
|
-
assert.equal(isAnimatedBusyState(state.uiState.kind), false);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
test("stale RUN_APPEND_ASSISTANT_DELTA does not mutate current UI state", () => {
|
|
533
|
-
const currentTurnId = 38;
|
|
534
|
-
let state = stateWithActiveRun(currentTurnId);
|
|
535
|
-
state = reduceSessionState(state, {
|
|
536
|
-
type: "UI_ACTION",
|
|
537
|
-
action: { type: "PROMPT_RUN_STARTED", turnId: currentTurnId },
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
const afterStaleDelta = reduceSessionState(state, {
|
|
541
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
542
|
-
turnId: currentTurnId - 1,
|
|
543
|
-
runId: 2,
|
|
544
|
-
chunk: "late chunk",
|
|
545
|
-
eventFactory: () => makeAssistantEvent(currentTurnId - 1, "late chunk"),
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
assert.strictEqual(afterStaleDelta, state);
|
|
549
|
-
assert.deepEqual(afterStaleDelta.uiState, { kind: "THINKING", turnId: currentTurnId });
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
test("RUN_APPLY_PROGRESS_UPDATES preserves separate entries and updates by id", () => {
|
|
553
|
-
const turnId = 30;
|
|
554
|
-
let state = stateWithActiveRun(turnId);
|
|
555
|
-
|
|
556
|
-
state = reduceSessionState(state, {
|
|
557
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
558
|
-
runId: 2,
|
|
559
|
-
updates: [
|
|
560
|
-
makeProgressUpdate("progress-1", "Checking files"),
|
|
561
|
-
makeProgressUpdate("progress-2", "Comparing snapshots"),
|
|
562
|
-
],
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
state = reduceSessionState(state, {
|
|
566
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
567
|
-
runId: 2,
|
|
568
|
-
updates: [
|
|
569
|
-
makeProgressUpdate("progress-1", "Checking files\n\nFound two candidates"),
|
|
570
|
-
],
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
574
|
-
assert.ok(runEvent);
|
|
575
|
-
assert.equal(runEvent.progressEntries.length, 2);
|
|
576
|
-
assert.equal(runEvent.progressEntries[0]?.text, "Checking files\n\nFound two candidates");
|
|
577
|
-
assert.equal(runEvent.progressEntries[0]?.blocks.length, 2);
|
|
578
|
-
assert.equal(runEvent.progressEntries[0]?.blocks[0]?.text, "Checking files");
|
|
579
|
-
assert.equal(runEvent.progressEntries[0]?.blocks[0]?.status, "completed");
|
|
580
|
-
assert.equal(runEvent.progressEntries[0]?.blocks[1]?.text, "Found two candidates");
|
|
581
|
-
assert.equal(runEvent.progressEntries[1]?.text, "Comparing snapshots");
|
|
582
|
-
assert.equal(runEvent.progressEntries[1]?.blocks[0]?.text, "Comparing snapshots");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test("RUN_APPLY_PROGRESS_UPDATES keeps completed block identities stable while the active block grows", () => {
|
|
586
|
-
const turnId = 31;
|
|
587
|
-
let state = stateWithActiveRun(turnId);
|
|
588
|
-
|
|
589
|
-
state = reduceSessionState(state, {
|
|
590
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
591
|
-
runId: 2,
|
|
592
|
-
updates: [
|
|
593
|
-
makeProgressUpdate("progress-1", "Inspecting workspace\n\nSwitching to scratch files"),
|
|
594
|
-
],
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
const afterFirstUpdate = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
598
|
-
assert.ok(afterFirstUpdate);
|
|
599
|
-
const completedBlock = afterFirstUpdate.progressEntries[0]?.blocks[0];
|
|
600
|
-
const activeBlock = afterFirstUpdate.progressEntries[0]?.blocks[1];
|
|
601
|
-
|
|
602
|
-
state = reduceSessionState(state, {
|
|
603
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
604
|
-
runId: 2,
|
|
605
|
-
updates: [
|
|
606
|
-
makeProgressUpdate("progress-1", "Inspecting workspace\n\nSwitching to scratch files in repo root"),
|
|
607
|
-
],
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
const afterSecondUpdate = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
611
|
-
assert.ok(afterSecondUpdate);
|
|
612
|
-
assert.strictEqual(afterSecondUpdate.progressEntries[0]?.blocks[0], completedBlock);
|
|
613
|
-
assert.notStrictEqual(afterSecondUpdate.progressEntries[0]?.blocks[1], activeBlock);
|
|
614
|
-
assert.equal(afterSecondUpdate.progressEntries[0]?.blocks[1]?.text, "Switching to scratch files in repo root");
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
test("reducer preserves response action response ordering across dispatched callbacks", () => {
|
|
618
|
-
const turnId = 32;
|
|
619
|
-
let state = stateWithActiveRun(turnId);
|
|
620
|
-
|
|
621
|
-
state = reduceSessionState(state, {
|
|
622
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
623
|
-
turnId,
|
|
624
|
-
runId: 2,
|
|
625
|
-
chunk: "First segment.",
|
|
626
|
-
eventFactory: () => makeAssistantEvent(turnId, "First segment."),
|
|
627
|
-
});
|
|
628
|
-
state = reduceSessionState(state, {
|
|
629
|
-
type: "RUN_UPSERT_TOOL_ACTIVITY",
|
|
630
|
-
runId: 2,
|
|
631
|
-
activity: {
|
|
632
|
-
id: "tool-1",
|
|
633
|
-
command: "Get-Content README.md",
|
|
634
|
-
status: "completed",
|
|
635
|
-
startedAt: 10,
|
|
636
|
-
completedAt: 20,
|
|
637
|
-
},
|
|
638
|
-
});
|
|
639
|
-
state = reduceSessionState(state, {
|
|
640
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
641
|
-
turnId,
|
|
642
|
-
runId: 2,
|
|
643
|
-
chunk: "Second segment.",
|
|
644
|
-
eventFactory: () => makeAssistantEvent(turnId, "Second segment."),
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
648
|
-
assert.ok(runEvent);
|
|
649
|
-
assert.deepEqual(runEvent.streamItems?.map((item) => item.kind), ["response", "action", "response"]);
|
|
650
|
-
assert.equal(runEvent.responseSegments?.[0]?.chunks.join(""), "First segment.");
|
|
651
|
-
assert.equal(runEvent.responseSegments?.[1]?.chunks.join(""), "Second segment.");
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
test("RUN_APPLY_LIVE_UPDATES applies ordered busy updates in one reducer action", () => {
|
|
655
|
-
const turnId = 34;
|
|
656
|
-
let state = stateWithActiveRun(turnId);
|
|
657
|
-
|
|
658
|
-
state = reduceSessionState(state, {
|
|
659
|
-
type: "UI_ACTION",
|
|
660
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
661
|
-
});
|
|
662
|
-
state = reduceSessionState(state, {
|
|
663
|
-
type: "RUN_APPLY_LIVE_UPDATES",
|
|
664
|
-
turnId,
|
|
665
|
-
runId: 2,
|
|
666
|
-
updates: [
|
|
667
|
-
{ type: "progress", update: makeProgressUpdate("think-1", "Inspecting files") },
|
|
668
|
-
{
|
|
669
|
-
type: "tool",
|
|
670
|
-
activity: {
|
|
671
|
-
id: "tool-1",
|
|
672
|
-
command: "Get-Content README.md",
|
|
673
|
-
status: "running",
|
|
674
|
-
startedAt: 10,
|
|
675
|
-
},
|
|
676
|
-
},
|
|
677
|
-
{ type: "assistant", chunk: "First segment." },
|
|
678
|
-
{
|
|
679
|
-
type: "tool",
|
|
680
|
-
activity: {
|
|
681
|
-
id: "tool-1",
|
|
682
|
-
command: "Get-Content README.md",
|
|
683
|
-
status: "completed",
|
|
684
|
-
startedAt: 10,
|
|
685
|
-
completedAt: 20,
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
{ type: "assistant", chunk: "Second segment." },
|
|
689
|
-
],
|
|
690
|
-
assistantEventFactory: (chunk) => ({
|
|
691
|
-
...makeAssistantEvent(turnId, ""),
|
|
692
|
-
contentChunks: [chunk],
|
|
693
|
-
}),
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
const runEvent = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
697
|
-
const assistantEvent = state.activeEvents.find((event): event is AssistantEvent => event.type === "assistant");
|
|
698
|
-
assert.ok(runEvent);
|
|
699
|
-
assert.ok(assistantEvent);
|
|
700
|
-
assert.equal(state.uiState.kind, "RESPONDING");
|
|
701
|
-
assert.deepEqual(
|
|
702
|
-
runEvent.streamItems?.map((item) => item.kind),
|
|
703
|
-
["thinking", "action", "response"],
|
|
704
|
-
);
|
|
705
|
-
assert.equal(assistantEvent.id, 3);
|
|
706
|
-
assert.deepEqual(assistantEvent.contentChunks, ["First segment.", "Second segment."]);
|
|
707
|
-
assert.equal(runEvent.toolActivities[0]?.status, "completed");
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
test("first prompt lifecycle exposes progress before finalization and preserves chronological stream order", () => {
|
|
711
|
-
const turnId = 33;
|
|
712
|
-
const userEvent = makeUserEvent(turnId);
|
|
713
|
-
const runEvent = { ...makeRunEvent(turnId), summary: "Codex is starting..." };
|
|
714
|
-
let state = createInitialSessionState();
|
|
715
|
-
|
|
716
|
-
state = reduceSessionState(state, {
|
|
717
|
-
type: "UI_ACTION",
|
|
718
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
719
|
-
});
|
|
720
|
-
state = reduceSessionState(state, {
|
|
721
|
-
type: "SET_ACTIVE_EVENTS",
|
|
722
|
-
events: [userEvent, runEvent],
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
assert.equal(state.uiState.kind, "THINKING");
|
|
726
|
-
assert.deepEqual(state.activeEvents.map((event) => event.type), ["user", "run"]);
|
|
727
|
-
assert.equal((state.activeEvents[1] as RunEvent).summary, "Codex is starting...");
|
|
728
|
-
|
|
729
|
-
state = reduceSessionState(state, {
|
|
730
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
731
|
-
runId: 2,
|
|
732
|
-
updates: [makeProgressUpdate("think-1", "Codex is checking project structure.")],
|
|
733
|
-
});
|
|
734
|
-
state = reduceSessionState(state, {
|
|
735
|
-
type: "RUN_UPSERT_TOOL_ACTIVITY",
|
|
736
|
-
runId: 2,
|
|
737
|
-
activity: {
|
|
738
|
-
id: "tool-1",
|
|
739
|
-
command: "Get-ChildItem",
|
|
740
|
-
status: "running",
|
|
741
|
-
startedAt: 10,
|
|
742
|
-
},
|
|
743
|
-
});
|
|
744
|
-
state = reduceSessionState(state, {
|
|
745
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
746
|
-
runId: 2,
|
|
747
|
-
updates: [makeProgressUpdate("think-2", "Codex is validating the startup flow.")],
|
|
748
|
-
});
|
|
749
|
-
state = reduceSessionState(state, {
|
|
750
|
-
type: "RUN_APPEND_ASSISTANT_DELTA",
|
|
751
|
-
turnId,
|
|
752
|
-
runId: 2,
|
|
753
|
-
chunk: "Final answer.",
|
|
754
|
-
eventFactory: () => makeAssistantEvent(turnId, "Final answer."),
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
const activeRun = state.activeEvents.find((event): event is RunEvent => event.type === "run");
|
|
758
|
-
assert.ok(activeRun);
|
|
759
|
-
assert.deepEqual(
|
|
760
|
-
activeRun.streamItems?.map((item) => item.kind),
|
|
761
|
-
["thinking", "action", "thinking", "response"],
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
state = reduceSessionState(state, {
|
|
765
|
-
type: "FINALIZE_RUN",
|
|
766
|
-
runId: 2,
|
|
767
|
-
turnId,
|
|
768
|
-
status: "completed",
|
|
769
|
-
response: undefined,
|
|
770
|
-
assistantFactory: () => makeAssistantEvent(turnId, ""),
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
774
|
-
const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
|
|
775
|
-
assert.ok(finalizedRun);
|
|
776
|
-
assert.ok(finalizedAssistant);
|
|
777
|
-
assert.deepEqual(
|
|
778
|
-
finalizedRun.streamItems?.map((item) => item.kind),
|
|
779
|
-
["thinking", "action", "thinking", "response"],
|
|
780
|
-
);
|
|
781
|
-
assert.equal(finalizedAssistant.content, "Final answer.");
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
test("FINALIZE_RUN preserves construction trail and appends final response after actions", () => {
|
|
785
|
-
const turnId = 34;
|
|
786
|
-
const userEvent = makeUserEvent(turnId);
|
|
787
|
-
const runEvent = { ...makeRunEvent(turnId), summary: "Codex is starting..." };
|
|
788
|
-
let state = createInitialSessionState();
|
|
789
|
-
|
|
790
|
-
state = reduceSessionState(state, {
|
|
791
|
-
type: "UI_ACTION",
|
|
792
|
-
action: { type: "PROMPT_RUN_STARTED", turnId },
|
|
793
|
-
});
|
|
794
|
-
state = reduceSessionState(state, {
|
|
795
|
-
type: "SET_ACTIVE_EVENTS",
|
|
796
|
-
events: [userEvent, runEvent],
|
|
797
|
-
});
|
|
798
|
-
state = reduceSessionState(state, {
|
|
799
|
-
type: "RUN_APPLY_PROGRESS_UPDATES",
|
|
800
|
-
runId: 2,
|
|
801
|
-
updates: [makeProgressUpdate("think-1", "Inspecting 5-Date Verification.")],
|
|
802
|
-
});
|
|
803
|
-
state = reduceSessionState(state, {
|
|
804
|
-
type: "RUN_UPSERT_TOOL_ACTIVITY",
|
|
805
|
-
runId: 2,
|
|
806
|
-
activity: {
|
|
807
|
-
id: "tool-1",
|
|
808
|
-
command: "Get-Content 5-Date-Verification.md",
|
|
809
|
-
status: "completed",
|
|
810
|
-
startedAt: 10,
|
|
811
|
-
completedAt: 20,
|
|
812
|
-
summary: "Read 42 lines",
|
|
813
|
-
},
|
|
814
|
-
});
|
|
815
|
-
state = reduceSessionState(state, {
|
|
816
|
-
type: "FINALIZE_RUN",
|
|
817
|
-
runId: 2,
|
|
818
|
-
turnId,
|
|
819
|
-
status: "completed",
|
|
820
|
-
response: "Final answer.",
|
|
821
|
-
assistantFactory: () => makeAssistantEvent(turnId, "Final answer."),
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
const finalizedRun = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
825
|
-
const finalizedAssistant = state.staticEvents.find((event): event is AssistantEvent => event.type === "assistant");
|
|
826
|
-
assert.ok(finalizedRun);
|
|
827
|
-
assert.ok(finalizedAssistant);
|
|
828
|
-
assert.deepEqual(
|
|
829
|
-
finalizedRun.streamItems?.map((item) => item.kind),
|
|
830
|
-
["thinking", "action", "response"],
|
|
831
|
-
);
|
|
832
|
-
assert.equal(finalizedRun.toolActivities[0]?.status, "completed");
|
|
833
|
-
assert.equal(finalizedRun.responseSegments?.[0]?.chunks.join(""), "Final answer.");
|
|
834
|
-
assert.equal(finalizedAssistant.content, "Final answer.");
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
test("finalized runs retain their runtime snapshot after unrelated state changes", () => {
|
|
838
|
-
const turnId = 4;
|
|
839
|
-
let state = stateWithActiveRun(turnId);
|
|
840
|
-
|
|
841
|
-
state = reduceSessionState(state, {
|
|
842
|
-
type: "FINALIZE_RUN",
|
|
843
|
-
runId: 2,
|
|
844
|
-
turnId,
|
|
845
|
-
status: "completed",
|
|
846
|
-
response: "Done",
|
|
847
|
-
assistantFactory: () => makeAssistantEvent(turnId, "Done"),
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
const runEvent = state.staticEvents.find((event): event is RunEvent => event.type === "run");
|
|
851
|
-
assert.ok(runEvent);
|
|
852
|
-
assert.equal(runEvent.runtime.model, TEST_RUNTIME.model);
|
|
853
|
-
assert.equal(runEvent.runtime.policy.sandboxMode, TEST_RUNTIME.policy.sandboxMode);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
// ─── externalCliStatus state machine ─────────────────────────────────────────
|
|
857
|
-
|
|
858
|
-
test("initial session state has externalCliStatus 'idle'", () => {
|
|
859
|
-
const state = createInitialSessionState();
|
|
860
|
-
assert.equal(state.externalCliStatus, "idle");
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
test("SET_EXTERNAL_CLI_STATUS transitions idle → starting", () => {
|
|
864
|
-
const state = createInitialSessionState();
|
|
865
|
-
const next = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "starting" });
|
|
866
|
-
assert.equal(next.externalCliStatus, "starting");
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
test("SET_EXTERNAL_CLI_STATUS transitions starting → ready", () => {
|
|
870
|
-
let state = createInitialSessionState();
|
|
871
|
-
state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "starting" });
|
|
872
|
-
const next = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
|
|
873
|
-
assert.equal(next.externalCliStatus, "ready");
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
test("SET_EXTERNAL_CLI_STATUS is a no-op when status is already the same (returns same object)", () => {
|
|
877
|
-
let state = createInitialSessionState();
|
|
878
|
-
state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
|
|
879
|
-
const again = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
|
|
880
|
-
assert.strictEqual(again, state, "should return the exact same state reference");
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
test("SUBMIT_PROMPT_RUN does not change externalCliStatus when provider is already ready", () => {
|
|
884
|
-
let state = createInitialSessionState();
|
|
885
|
-
state = reduceSessionState(state, { type: "SET_EXTERNAL_CLI_STATUS", status: "ready" });
|
|
886
|
-
|
|
887
|
-
const userEvent = makeUserEvent(99);
|
|
888
|
-
const runEvent = makeRunEvent(99);
|
|
889
|
-
const next = reduceSessionState(state, {
|
|
890
|
-
type: "SUBMIT_PROMPT_RUN",
|
|
891
|
-
turnId: 99,
|
|
892
|
-
runId: 2,
|
|
893
|
-
events: [userEvent, runEvent],
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
assert.equal(next.externalCliStatus, "ready", "externalCliStatus must remain 'ready' across prompts");
|
|
897
|
-
});
|