@golba98/codexa 1.0.1 → 1.0.3

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.
Files changed (234) hide show
  1. package/README.md +391 -94
  2. package/package.json +14 -8
  3. package/src/app.tsx +196 -63
  4. package/src/commands/handler.ts +9 -4
  5. package/src/config/buildInfo.ts +3 -0
  6. package/src/config/launchArgs.ts +10 -0
  7. package/src/config/layeredConfig.ts +0 -0
  8. package/src/config/persistence.ts +51 -0
  9. package/src/config/runtimeConfig.ts +4 -0
  10. package/src/config/settings.ts +6 -13
  11. package/src/config/toml-serialize.ts +0 -0
  12. package/src/config/trustStore.ts +0 -0
  13. package/src/config/updateCheckCache.ts +63 -0
  14. package/src/core/attachments.ts +0 -0
  15. package/src/core/auth/codexAuth.ts +0 -0
  16. package/src/core/channel.ts +23 -0
  17. package/src/core/cleanupFastFail.ts +0 -0
  18. package/src/core/clipboard.ts +0 -0
  19. package/src/core/codex.ts +0 -0
  20. package/src/core/codexExecArgs.ts +0 -0
  21. package/src/core/codexLaunch.ts +0 -0
  22. package/src/core/codexPrompt.ts +0 -0
  23. package/src/core/executables/claudeExecutable.ts +0 -0
  24. package/src/core/executables/codexExecutable.ts +1 -0
  25. package/src/core/executables/executableResolver.ts +61 -43
  26. package/src/core/executables/geminiExecutable.ts +0 -0
  27. package/src/core/githubDiagnostics.ts +0 -0
  28. package/src/core/hollowResponseFormat.ts +0 -0
  29. package/src/core/inputDebug.ts +0 -0
  30. package/src/core/launchContext.ts +33 -33
  31. package/src/core/models/codexCapabilities.ts +0 -0
  32. package/src/core/models/codexModelCapabilities.ts +0 -0
  33. package/src/core/models/modelSpecs.ts +0 -0
  34. package/src/core/perf/profiler.ts +0 -0
  35. package/src/core/perf/renderDebug.ts +0 -0
  36. package/src/core/planStorage.ts +0 -0
  37. package/src/core/process/CommandRunner.ts +0 -0
  38. package/src/core/process/processValidation.ts +9 -5
  39. package/src/core/projectInstructions.ts +0 -0
  40. package/src/core/providerLauncher/launcher.ts +59 -42
  41. package/src/core/providerLauncher/registry.ts +0 -0
  42. package/src/core/providerLauncher/types.ts +0 -0
  43. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  44. package/src/core/providerRuntime/anthropic.ts +0 -0
  45. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  46. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  47. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  48. package/src/core/providerRuntime/gemini.ts +0 -0
  49. package/src/core/providerRuntime/lmstudio.ts +0 -0
  50. package/src/core/providerRuntime/local.ts +0 -0
  51. package/src/core/providerRuntime/models.ts +0 -0
  52. package/src/core/providerRuntime/reasoning.ts +0 -0
  53. package/src/core/providerRuntime/registry.ts +0 -0
  54. package/src/core/providerRuntime/types.ts +0 -0
  55. package/src/core/providers/codexJsonStream.ts +0 -0
  56. package/src/core/providers/codexSubprocess.ts +0 -0
  57. package/src/core/providers/codexTranscript.ts +0 -0
  58. package/src/core/providers/openaiNative.ts +0 -0
  59. package/src/core/providers/registry.ts +0 -0
  60. package/src/core/providers/types.ts +0 -0
  61. package/src/core/terminal/startupClear.ts +20 -0
  62. package/src/core/terminal/terminalCapabilities.ts +0 -0
  63. package/src/core/terminal/terminalControl.ts +0 -0
  64. package/src/core/terminal/terminalSanitize.ts +0 -0
  65. package/src/core/terminal/terminalSelection.ts +0 -0
  66. package/src/core/terminal/terminalTitle.ts +0 -0
  67. package/src/core/updateCheck.ts +188 -0
  68. package/src/core/workspaceActivity.ts +0 -0
  69. package/src/core/workspaceGuard.ts +0 -0
  70. package/src/core/workspaceRoot.ts +0 -0
  71. package/src/exec.ts +0 -0
  72. package/src/headless/execArgs.ts +1 -0
  73. package/src/headless/execRunner.ts +0 -0
  74. package/src/index.tsx +13 -9
  75. package/src/session/appSession.ts +0 -0
  76. package/src/session/chatLifecycle.ts +0 -0
  77. package/src/session/liveRenderScheduler.ts +0 -0
  78. package/src/session/planFlow.ts +0 -0
  79. package/src/session/planTranscript.ts +0 -0
  80. package/src/session/promptRunSchedule.ts +0 -0
  81. package/src/session/types.ts +2 -1
  82. package/src/test/runtimeTestUtils.ts +0 -0
  83. package/src/types/react-dom.d.ts +0 -0
  84. package/src/ui/ActionRequiredBlock.tsx +5 -5
  85. package/src/ui/ActivityBars.tsx +3 -3
  86. package/src/ui/ActivityIndicator.tsx +6 -6
  87. package/src/ui/AgentBlock.tsx +6 -6
  88. package/src/ui/AnimatedStatusText.tsx +1 -1
  89. package/src/ui/AppShell.tsx +13 -5
  90. package/src/ui/AttachmentImportPanel.tsx +8 -8
  91. package/src/ui/AuthPanel.tsx +20 -20
  92. package/src/ui/BackendPicker.tsx +0 -0
  93. package/src/ui/BottomComposer.tsx +117 -78
  94. package/src/ui/DashCard.tsx +3 -3
  95. package/src/ui/Markdown.tsx +17 -17
  96. package/src/ui/ModePicker.tsx +0 -0
  97. package/src/ui/ModelPicker.tsx +0 -0
  98. package/src/ui/ModelPickerScreen.tsx +10 -10
  99. package/src/ui/ModelReasoningPicker.tsx +14 -14
  100. package/src/ui/Panel.tsx +3 -3
  101. package/src/ui/PermissionsPanel.tsx +0 -0
  102. package/src/ui/PlanActionPicker.tsx +6 -6
  103. package/src/ui/PlanReviewPanel.tsx +8 -8
  104. package/src/ui/ProviderPicker.tsx +18 -18
  105. package/src/ui/ReasoningPicker.tsx +0 -0
  106. package/src/ui/RunFooter.tsx +3 -3
  107. package/src/ui/SelectionPanel.tsx +4 -4
  108. package/src/ui/SettingsPanel.tsx +8 -8
  109. package/src/ui/Spinner.tsx +1 -1
  110. package/src/ui/StaticIntroItem.tsx +0 -0
  111. package/src/ui/StaticTranscriptItem.tsx +0 -0
  112. package/src/ui/TextEntryPanel.tsx +10 -10
  113. package/src/ui/ThemePicker.tsx +0 -0
  114. package/src/ui/ThinkingBlock.tsx +8 -8
  115. package/src/ui/Timeline.tsx +21 -17
  116. package/src/ui/TopHeader.tsx +421 -257
  117. package/src/ui/TurnGroup.tsx +31 -31
  118. package/src/ui/UpdateAvailableCard.tsx +41 -0
  119. package/src/ui/UpdatePromptPanel.tsx +203 -0
  120. package/src/ui/busyStatusAnimation.ts +0 -0
  121. package/src/ui/commandNormalize.ts +0 -0
  122. package/src/ui/diffRenderer.ts +0 -0
  123. package/src/ui/focus.ts +3 -0
  124. package/src/ui/inputBuffer.ts +0 -0
  125. package/src/ui/layout.ts +1 -1
  126. package/src/ui/logoVariants.ts +107 -0
  127. package/src/ui/modeDisplay.ts +12 -12
  128. package/src/ui/outputPipeline.ts +0 -0
  129. package/src/ui/progressEntries.ts +0 -0
  130. package/src/ui/runActivityView.ts +0 -0
  131. package/src/ui/runtimeDisplay.ts +100 -0
  132. package/src/ui/slashCommands.ts +0 -0
  133. package/src/ui/terminalAnswerFormat.ts +0 -0
  134. package/src/ui/textLayout.ts +0 -0
  135. package/src/ui/theme.tsx +274 -395
  136. package/src/ui/themeFlow.ts +0 -0
  137. package/src/ui/timelineMeasure.ts +29 -19
  138. package/src/ui/useThrottledValue.ts +0 -0
  139. package/scripts/audit-codexa-capabilities.mjs +0 -466
  140. package/scripts/smoke-terminal-bench.mjs +0 -35
  141. package/src/appRenderStability.test.ts +0 -131
  142. package/src/commands/handler.test.ts +0 -643
  143. package/src/config/launchArgs.test.ts +0 -158
  144. package/src/config/layeredConfig.test.ts +0 -143
  145. package/src/config/persistence.test.ts +0 -110
  146. package/src/config/runtimeConfig.test.ts +0 -218
  147. package/src/config/settings.test.ts +0 -155
  148. package/src/config/trustStore.test.ts +0 -29
  149. package/src/core/attachments.test.ts +0 -155
  150. package/src/core/auth/codexAuth.test.ts +0 -68
  151. package/src/core/cleanupFastFail.test.ts +0 -76
  152. package/src/core/codexExecArgs.test.ts +0 -195
  153. package/src/core/codexLaunch.test.ts +0 -205
  154. package/src/core/codexPrompt.test.ts +0 -252
  155. package/src/core/executables/codexExecutable.test.ts +0 -212
  156. package/src/core/executables/executableResolver.test.ts +0 -129
  157. package/src/core/executables/geminiExecutable.test.ts +0 -116
  158. package/src/core/executables/pathSanityScan.test.ts +0 -47
  159. package/src/core/githubDiagnostics.test.ts +0 -92
  160. package/src/core/hollowResponseFormat.test.ts +0 -58
  161. package/src/core/launchContext.test.ts +0 -157
  162. package/src/core/models/codexCapabilities.test.ts +0 -45
  163. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  164. package/src/core/models/modelSpecs.test.ts +0 -283
  165. package/src/core/perf/renderDebug.test.ts +0 -230
  166. package/src/core/planStorage.test.ts +0 -143
  167. package/src/core/process/CommandRunner.test.ts +0 -105
  168. package/src/core/projectInstructions.test.ts +0 -50
  169. package/src/core/providerLauncher/launcher.test.ts +0 -238
  170. package/src/core/providerLauncher/registry.test.ts +0 -324
  171. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  172. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  173. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  174. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  175. package/src/core/providerRuntime/gemini.test.ts +0 -437
  176. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  177. package/src/core/providerRuntime/local.test.ts +0 -787
  178. package/src/core/providerRuntime/registry.test.ts +0 -233
  179. package/src/core/providers/codexJsonStream.test.ts +0 -148
  180. package/src/core/providers/codexSubprocess.test.ts +0 -68
  181. package/src/core/providers/codexTranscript.test.ts +0 -284
  182. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  183. package/src/core/terminal/terminalControl.test.ts +0 -75
  184. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  185. package/src/core/terminal/terminalSelection.test.ts +0 -42
  186. package/src/core/terminal/terminalTitle.test.ts +0 -328
  187. package/src/core/workspaceActivity.test.ts +0 -163
  188. package/src/core/workspaceGuard.test.ts +0 -151
  189. package/src/core/workspaceRoot.test.ts +0 -23
  190. package/src/exec.test.ts +0 -13
  191. package/src/headless/execArgs.test.ts +0 -147
  192. package/src/headless/execRunner.test.ts +0 -434
  193. package/src/index.test.tsx +0 -618
  194. package/src/session/appSession.test.ts +0 -897
  195. package/src/session/chatLifecycle.test.ts +0 -64
  196. package/src/session/liveRenderScheduler.test.ts +0 -201
  197. package/src/session/planFlow.test.ts +0 -103
  198. package/src/session/planTranscript.test.ts +0 -65
  199. package/src/session/promptRunSchedule.test.ts +0 -36
  200. package/src/ui/ActivityIndicator.test.tsx +0 -58
  201. package/src/ui/AgentBlock.test.ts +0 -6
  202. package/src/ui/AnimatedStatusText.test.ts +0 -16
  203. package/src/ui/AppShell.test.tsx +0 -1739
  204. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  205. package/src/ui/BottomComposer.test.ts +0 -674
  206. package/src/ui/CodexLogo.tsx +0 -55
  207. package/src/ui/Markdown.test.ts +0 -157
  208. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  209. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  210. package/src/ui/ModelPickerState.test.tsx +0 -151
  211. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  212. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  213. package/src/ui/PromptCardBorder.test.tsx +0 -161
  214. package/src/ui/ProviderPicker.test.tsx +0 -289
  215. package/src/ui/ProviderShortcut.test.tsx +0 -143
  216. package/src/ui/SettingsPanel.test.tsx +0 -233
  217. package/src/ui/Timeline.test.ts +0 -2067
  218. package/src/ui/TimelineNavigation.test.tsx +0 -201
  219. package/src/ui/TopHeader.test.tsx +0 -239
  220. package/src/ui/TurnGroup.test.tsx +0 -365
  221. package/src/ui/busyStatusAnimation.test.ts +0 -30
  222. package/src/ui/commandNormalize.test.ts +0 -142
  223. package/src/ui/diffRenderer.test.ts +0 -102
  224. package/src/ui/focusFlow.test.tsx +0 -1098
  225. package/src/ui/inputBuffer.test.ts +0 -151
  226. package/src/ui/layout.test.ts +0 -145
  227. package/src/ui/modeDisplay.test.ts +0 -42
  228. package/src/ui/runActivityView.test.ts +0 -89
  229. package/src/ui/runLifecycleView.test.tsx +0 -237
  230. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  231. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  232. package/src/ui/textLayout.test.ts +0 -18
  233. package/src/ui/themeFlow.test.ts +0 -53
  234. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -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
- });