@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,205 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { resolveRuntimeConfig, normalizeRuntimeConfig } from "../config/runtimeConfig.js";
4
- import { prepareCodexExecLaunch } from "./codexLaunch.js";
5
- import type { CodexCliCapabilities } from "./models/codexCapabilities.js";
6
-
7
- test("prepares a shared launch plan with resolved executable strategy and source metadata", async () => {
8
- const capabilities: CodexCliCapabilities = {
9
- askForApproval: false,
10
- sandbox: true,
11
- config: true,
12
- fullAuto: true,
13
- };
14
-
15
- const result = await prepareCodexExecLaunch(
16
- {
17
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
18
- model: "gpt-5.4",
19
- reasoningLevel: "medium",
20
- policy: {
21
- approvalPolicy: "on-request",
22
- sandboxMode: "workspace-write",
23
- },
24
- })),
25
- cwd: "C:/repo",
26
- structuredOutput: false,
27
- probeCapabilities: true,
28
- },
29
- "file:///C:/project/src/core/providers/codexSubprocess.ts",
30
- {
31
- resolveExecutable: async () => "C:/tools/codex.cmd",
32
- getCapabilities: async () => capabilities,
33
- },
34
- );
35
-
36
- assert.deepEqual(result, {
37
- ok: true,
38
- strategy: "config-overrides",
39
- executable: "C:/tools/codex.cmd",
40
- capabilities,
41
- responsibleModulePath: "C:\\project\\src\\core\\providers\\codexSubprocess.ts",
42
- responsibleModuleKind: "src",
43
- launchContext: {
44
- launchKind: process.env.CODEXA_LAUNCH_KIND,
45
- packageRoot: process.env.CODEXA_PACKAGE_ROOT,
46
- launcherScript: process.env.CODEXA_LAUNCHER_SCRIPT,
47
- },
48
- args: [
49
- "exec",
50
- "--skip-git-repo-check",
51
- "--cd",
52
- "C:/repo",
53
- "--model",
54
- "gpt-5.4",
55
- "--config",
56
- "model_reasoning_effort=medium",
57
- "-c",
58
- "approval_policy=on-request",
59
- "--sandbox",
60
- "workspace-write",
61
- "-",
62
- ],
63
- });
64
- });
65
-
66
- test("launch diagnostics are injectable and silent by default", async () => {
67
- const previousDebug = process.env.CODEXA_DEBUG_CODEX_LAUNCH;
68
- process.env.CODEXA_DEBUG_CODEX_LAUNCH = "1";
69
- const capabilities: CodexCliCapabilities = {
70
- askForApproval: false,
71
- sandbox: true,
72
- config: true,
73
- fullAuto: true,
74
- };
75
-
76
- try {
77
- const silent = await prepareCodexExecLaunch(
78
- {
79
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
80
- model: "gpt-5.4",
81
- reasoningLevel: "medium",
82
- policy: {
83
- approvalPolicy: "on-request",
84
- sandboxMode: "workspace-write",
85
- },
86
- })),
87
- cwd: "C:/repo",
88
- structuredOutput: true,
89
- },
90
- "file:///C:/repo/src/core/providers/codexSubprocess.ts",
91
- {
92
- resolveExecutable: async () => "C:/tools/codex.cmd",
93
- getCapabilities: async () => capabilities,
94
- },
95
- );
96
- assert.equal(silent.ok, true);
97
-
98
- const diagnostics: string[] = [];
99
- await prepareCodexExecLaunch(
100
- {
101
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
102
- model: "gpt-5.4",
103
- reasoningLevel: "medium",
104
- policy: {
105
- approvalPolicy: "on-request",
106
- sandboxMode: "workspace-write",
107
- },
108
- })),
109
- cwd: "C:/repo",
110
- structuredOutput: true,
111
- },
112
- "file:///C:/repo/src/core/providers/codexSubprocess.ts",
113
- {
114
- resolveExecutable: async () => "C:/tools/codex.cmd",
115
- getCapabilities: async () => capabilities,
116
- diagnosticsLogger: (message) => diagnostics.push(message),
117
- },
118
- );
119
-
120
- assert.equal(diagnostics.length, 1);
121
- assert.match(diagnostics[0]!, /\[codexa\] codex launch debug/);
122
- } finally {
123
- if (previousDebug == null) {
124
- delete process.env.CODEXA_DEBUG_CODEX_LAUNCH;
125
- } else {
126
- process.env.CODEXA_DEBUG_CODEX_LAUNCH = previousDebug;
127
- }
128
- }
129
- });
130
-
131
- test("uses modern Codex capabilities by default without probing help output", async () => {
132
- let capabilityProbeCount = 0;
133
-
134
- const result = await prepareCodexExecLaunch(
135
- {
136
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
137
- model: "gpt-5.4-mini",
138
- reasoningLevel: "medium",
139
- policy: {
140
- approvalPolicy: "never",
141
- sandboxMode: "danger-full-access",
142
- },
143
- })),
144
- cwd: "C:/repo",
145
- structuredOutput: true,
146
- },
147
- "file:///C:/repo/src/core/providers/codexSubprocess.ts",
148
- {
149
- resolveExecutable: async () => "codex.cmd",
150
- getCapabilities: async () => {
151
- capabilityProbeCount += 1;
152
- throw new Error("capability probe should not run");
153
- },
154
- },
155
- );
156
-
157
- assert.equal(result.ok, true);
158
- assert.equal(capabilityProbeCount, 0);
159
- });
160
-
161
- test("can explicitly probe capabilities for compatibility fallback", async () => {
162
- let capabilityProbeCount = 0;
163
- const capabilities: CodexCliCapabilities = {
164
- askForApproval: false,
165
- sandbox: false,
166
- config: true,
167
- fullAuto: false,
168
- };
169
-
170
- const result = await prepareCodexExecLaunch(
171
- {
172
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
173
- model: "gpt-5.4-mini",
174
- reasoningLevel: "medium",
175
- policy: {
176
- approvalPolicy: "on-request",
177
- sandboxMode: "workspace-write",
178
- },
179
- })),
180
- cwd: "C:/repo",
181
- structuredOutput: false,
182
- probeCapabilities: true,
183
- },
184
- "file:///C:/repo/src/core/providers/codexSubprocess.ts",
185
- {
186
- resolveExecutable: async () => "codex.cmd",
187
- getCapabilities: async () => {
188
- capabilityProbeCount += 1;
189
- return capabilities;
190
- },
191
- },
192
- );
193
-
194
- assert.equal(result.ok, true);
195
- assert.equal(capabilityProbeCount, 1);
196
- if (result.ok) {
197
- assert.deepEqual(result.args.slice(-5), [
198
- "-c",
199
- "approval_policy=on-request",
200
- "-c",
201
- "sandbox_mode=workspace-write",
202
- "-",
203
- ]);
204
- }
205
- });
@@ -1,252 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- buildCodexPrompt,
5
- buildPlanExecutionPrompt,
6
- buildPlanningPrompt,
7
- detectHollowResponse,
8
- isClearlySafeGeneratedCleanupRequest,
9
- promptHasWriteIntent,
10
- resolveExecutionMode,
11
- } from "./codexPrompt.js";
12
-
13
- const readOnlyPolicy = {
14
- approvalPolicy: "untrusted",
15
- sandboxMode: "read-only",
16
- } as const;
17
-
18
- const writePolicy = {
19
- approvalPolicy: "on-request",
20
- sandboxMode: "workspace-write",
21
- } as const;
22
-
23
- test("detects write intent for build requests", () => {
24
- assert.equal(promptHasWriteIntent("Create a weather app script with tests"), true);
25
- });
26
-
27
- test("does not force write mode for general questions", () => {
28
- assert.equal(promptHasWriteIntent("Explain how array sorting works"), false);
29
- });
30
-
31
- test("detects write intent for generated cleanup requests", () => {
32
- assert.equal(promptHasWriteIntent("Delete only clearly safe generated files and folders"), true);
33
- });
34
-
35
- test("detects narrow generated cleanup requests", () => {
36
- assert.equal(isClearlySafeGeneratedCleanupRequest("Delete only clearly safe generated files and folders"), true);
37
- assert.equal(isClearlySafeGeneratedCleanupRequest("Prune build artifacts and caches"), true);
38
- });
39
-
40
- test("does not fast-path broad destructive cleanup requests", () => {
41
- assert.equal(isClearlySafeGeneratedCleanupRequest("delete everything"), false);
42
- assert.equal(isClearlySafeGeneratedCleanupRequest("remove all files"), false);
43
- assert.equal(isClearlySafeGeneratedCleanupRequest("wipe the workspace"), false);
44
- });
45
-
46
- test("auto-upgrades suggest mode for editing prompts", () => {
47
- assert.deepEqual(resolveExecutionMode("suggest", "Create a new CLI script and tests"), {
48
- mode: "auto-edit",
49
- autoUpgraded: true,
50
- });
51
- });
52
-
53
- test("auto-upgrades suggest mode for generated cleanup prompts", () => {
54
- assert.deepEqual(resolveExecutionMode("suggest", "Delete only clearly safe generated files and folders"), {
55
- mode: "auto-edit",
56
- autoUpgraded: true,
57
- });
58
- });
59
-
60
- test("keeps explicit full-auto mode unchanged", () => {
61
- assert.deepEqual(resolveExecutionMode("full-auto", "Create a new CLI script and tests"), {
62
- mode: "full-auto",
63
- autoUpgraded: false,
64
- });
65
- });
66
-
67
- test("read-only runtime policy overrides write-capable modes", () => {
68
- const prompt = buildCodexPrompt("Create a weather app script with tests", "auto-edit", readOnlyPolicy);
69
- assert.match(prompt, /runtime permissions are read-only/i);
70
- assert.doesNotMatch(prompt, /write access/i);
71
- assert.doesNotMatch(prompt, /create or update files directly/i);
72
- });
73
-
74
- test("suggest mode stays advisory even with write-capable permissions", () => {
75
- const prompt = buildCodexPrompt("Explain this code", "suggest", writePolicy);
76
- assert.match(prompt, /permissions allow workspace edits/i);
77
- assert.match(prompt, /still in suggest mode/i);
78
- assert.match(prompt, /without making file changes/i);
79
- });
80
-
81
- test("plan mode injects plan-first instructions without changing suggest semantics", () => {
82
- const prompt = buildCodexPrompt("Explain this code", "suggest", {
83
- ...writePolicy,
84
- planMode: true,
85
- });
86
- assert.match(prompt, /Planning mode is enabled for this session/i);
87
- assert.match(prompt, /Start by giving a concise, repo-aware plan/i);
88
- assert.match(prompt, /continue the task normally under the current mode and runtime permissions/i);
89
- assert.match(prompt, /still in suggest mode/i);
90
- assert.match(prompt, /without making file changes/i);
91
- });
92
-
93
- test("builds a write-enabled codex prompt for auto-edit when permissions allow it", () => {
94
- const prompt = buildCodexPrompt("Create a weather app script with tests", "auto-edit", writePolicy);
95
- assert.match(prompt, /write access/i);
96
- assert.match(prompt, /create or update files directly/i);
97
- assert.match(prompt, /best-effort continuation/i);
98
- assert.match(prompt, /make the most reasonable assumption/i);
99
- assert.match(prompt, /\[QUESTION\]:/i);
100
- assert.match(prompt, /do not reply with generic readiness/i);
101
- assert.match(prompt, /Task:/i);
102
- });
103
-
104
- test("codex prompts include terminal-native final answer guidance", () => {
105
- const prompt = buildCodexPrompt("Explain this repo", "suggest", writePolicy);
106
-
107
- assert.match(prompt, /Final answer style for this terminal UI/i);
108
- assert.match(prompt, /short relative paths such as src\/App\.tsx/i);
109
- assert.match(prompt, /Do not include absolute local paths like C:\/Users/i);
110
- assert.match(prompt, /avoid web-style local Markdown links/i);
111
- assert.match(prompt, /Answer simple questions simply/i);
112
- });
113
-
114
- test("injects project instructions before the task", () => {
115
- const prompt = buildCodexPrompt("Explain this repo", "suggest", writePolicy, {
116
- projectInstructions: {
117
- path: "/workspace/AGENTS.md",
118
- content: "Prefer small, focused changes.",
119
- },
120
- });
121
-
122
- assert.match(prompt, /Project instructions:/i);
123
- assert.match(prompt, /Loaded from: \/workspace\/AGENTS\.md/i);
124
- assert.match(prompt, /Prefer small, focused changes\./i);
125
- assert.ok(prompt.indexOf("Project instructions:") < prompt.indexOf("Task:"));
126
- });
127
-
128
- test("adds fast generated cleanup safety instructions for write-enabled cleanup prompts", () => {
129
- const prompt = buildCodexPrompt("Delete only clearly safe generated files and folders", "auto-edit", writePolicy);
130
- assert.match(prompt, /Fast generated-file cleanup guidance/i);
131
- assert.match(prompt, /shallow workspace inspection/i);
132
- assert.match(prompt, /generated artifacts/i);
133
- assert.match(prompt, /Attempt each safe cleanup target once/i);
134
- assert.match(prompt, /stop immediately and report the blocked path and cause/i);
135
- assert.match(prompt, /Do not retry, force-delete/i);
136
- assert.match(prompt, /Do not do branch, bootstrap, package install, or repo setup/i);
137
- });
138
-
139
- test("plan mode keeps write-enabled prompts actionable", () => {
140
- const prompt = buildCodexPrompt("Create a weather app script with tests", "auto-edit", {
141
- ...writePolicy,
142
- planMode: true,
143
- });
144
- assert.match(prompt, /Planning mode is enabled for this session/i);
145
- assert.match(prompt, /continue the task normally under the current mode and runtime permissions/i);
146
- assert.match(prompt, /write access/i);
147
- assert.match(prompt, /create or update files directly/i);
148
- });
149
-
150
- test("plan mode does not override read-only runtime guidance", () => {
151
- const prompt = buildCodexPrompt("Create a weather app script with tests", "full-auto", {
152
- ...readOnlyPolicy,
153
- planMode: true,
154
- });
155
- assert.match(prompt, /Planning mode is enabled for this session/i);
156
- assert.match(prompt, /runtime permissions are read-only/i);
157
- assert.doesNotMatch(prompt, /write access/i);
158
- });
159
-
160
- test("builds a planning prompt that stays plan-only and includes constraints", () => {
161
- const prompt = buildPlanningPrompt({
162
- task: "Delete everything in hello.py and replace it with a starter hello_world.py script.",
163
- constraints: ["Keep the change scoped to the current workspace."],
164
- });
165
-
166
- assert.match(prompt, /plan-only turn/i);
167
- assert.match(prompt, /Do not implement the task/i);
168
- assert.match(prompt, /Files, Steps, Assumptions, Risks/i);
169
- assert.match(prompt, /Active constraints:/i);
170
- assert.match(prompt, /Task:/i);
171
- });
172
-
173
- test("builds an approved-plan execution prompt that tells codexa to implement now", () => {
174
- const prompt = buildPlanExecutionPrompt({
175
- task: "Delete everything in hello.py and replace it with a starter hello_world.py script.",
176
- approvedPlan: "## Files\n- hello.py\n- hello_world.py",
177
- constraints: ["Keep it minimal."],
178
- });
179
-
180
- assert.match(prompt, /approved the following plan/i);
181
- assert.match(prompt, /implement it now/i);
182
- assert.match(prompt, /Do the work in the workspace instead of re-planning/i);
183
- assert.match(prompt, /Approved plan:/i);
184
- assert.match(prompt, /Additional constraints:/i);
185
- });
186
-
187
- // --- detectHollowResponse tests ---
188
-
189
- test("detects greetings as hollow with kind=greeting", () => {
190
- for (const greeting of ["Hello.", "Hi!", "Hey", "Sure.", "Okay", "Sounds good"]) {
191
- const result = detectHollowResponse("create a file", greeting);
192
- assert.equal(result.isHollow, true, `Expected "${greeting}" to be hollow`);
193
- assert.equal(result.kind, "greeting");
194
- }
195
- });
196
-
197
- test("detects filler acknowledgments as hollow with kind=filler", () => {
198
- for (const filler of ["Thanks.", "Thank you!", "No problem", "Will do", "Noted"]) {
199
- const result = detectHollowResponse("create a file", filler);
200
- assert.equal(result.isHollow, true, `Expected "${filler}" to be hollow`);
201
- assert.equal(result.kind, "filler");
202
- }
203
- });
204
-
205
- test("detects clarification questions as hollow with kind=clarification", () => {
206
- for (const question of ["Can you clarify?", "Could you specify the path?", "What do you mean?"]) {
207
- const result = detectHollowResponse("create a file", question);
208
- assert.equal(result.isHollow, true, `Expected "${question}" to be hollow`);
209
- assert.equal(result.kind, "clarification");
210
- }
211
- });
212
-
213
- test("detects empty/whitespace as hollow with kind=filler", () => {
214
- assert.equal(detectHollowResponse("create a file", "").isHollow, true);
215
- assert.equal(detectHollowResponse("create a file", " ").isHollow, true);
216
- assert.equal(detectHollowResponse("create a file", "").kind, "filler");
217
- });
218
-
219
- test("detects short no-action responses for write-intent prompts", () => {
220
- const result = detectHollowResponse("create a new script file", "Let me think about that.");
221
- assert.equal(result.isHollow, true);
222
- assert.equal(result.kind, "short-no-action");
223
- });
224
-
225
- test("does not flag valid short responses with action confirmation", () => {
226
- const result = detectHollowResponse("create a file", "Done, created the file.");
227
- assert.equal(result.isHollow, false);
228
- assert.equal(result.kind, "none");
229
- });
230
-
231
- test("does not flag longer task completions", () => {
232
- const longResponse = "I've created the file at src/utils/helper.ts with the requested utility functions. The module exports three helpers for string manipulation.";
233
- const result = detectHollowResponse("create a utility file", longResponse);
234
- assert.equal(result.isHollow, false);
235
- });
236
-
237
- test("does not flag responses containing code blocks", () => {
238
- const result = detectHollowResponse("create a script", "Here:\n```js\nconsole.log('hi');\n```");
239
- assert.equal(result.isHollow, false);
240
- });
241
-
242
- test("does not flag greetings when prompt has no write intent", () => {
243
- for (const prompt of ["Hello", "Hi there", "Hey", "What's up"]) {
244
- const result = detectHollowResponse(prompt, "Hello!");
245
- assert.equal(result.isHollow, false, `Prompt "${prompt}" should not trigger hollow detection`);
246
- }
247
- });
248
-
249
- test("does not flag filler when prompt is conversational", () => {
250
- const result = detectHollowResponse("thanks for that", "You're welcome!");
251
- assert.equal(result.isHollow, false);
252
- });
@@ -1,212 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { afterEach } from "node:test";
4
- import type { ChildProcess } from "node:child_process";
5
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { tmpdir } from "node:os";
8
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
9
- import {
10
- resolveCodexExecutable,
11
- resetCodexExecutableCacheForTests,
12
- spawnCodexProcess,
13
- } from "./codexExecutable.js";
14
-
15
- function commandResult(overrides: Partial<CommandResult> = {}): CommandResult {
16
- return {
17
- status: "completed",
18
- exitCode: 0,
19
- signal: null,
20
- stdout: "",
21
- stderr: "",
22
- startedAt: 0,
23
- endedAt: 0,
24
- durationMs: 0,
25
- userMessage: "Command completed.",
26
- ...overrides,
27
- };
28
- }
29
-
30
- function mockRunCommand(result: CommandResult, onCall?: (spec: Parameters<typeof runCommand>[0]) => void): typeof runCommand {
31
- return ((spec) => {
32
- onCall?.(spec);
33
- return {
34
- child: null as unknown as ChildProcess,
35
- result: Promise.resolve(result),
36
- cancel: () => undefined,
37
- };
38
- }) as typeof runCommand;
39
- }
40
-
41
- async function withEnv<T>(env: Partial<NodeJS.ProcessEnv>, callback: () => Promise<T>): Promise<T> {
42
- const originalCodexExe = process.env.CODEX_EXECUTABLE;
43
- try {
44
- if ("CODEX_EXECUTABLE" in env) process.env.CODEX_EXECUTABLE = env.CODEX_EXECUTABLE;
45
- else delete process.env.CODEX_EXECUTABLE;
46
- resetCodexExecutableCacheForTests();
47
- return await callback();
48
- } finally {
49
- if (originalCodexExe === undefined) delete process.env.CODEX_EXECUTABLE;
50
- else process.env.CODEX_EXECUTABLE = originalCodexExe;
51
- resetCodexExecutableCacheForTests();
52
- }
53
- }
54
-
55
- afterEach(() => {
56
- resetCodexExecutableCacheForTests();
57
- });
58
-
59
- test("Codex resolver: configuredPath wins and bypasses env and PATH", async () => {
60
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
61
- let whereCalled = false;
62
- const resolved = await resolveCodexExecutable({
63
- configuredPath: process.execPath,
64
- runCommandImpl: mockRunCommand(commandResult(), () => { whereCalled = true; }),
65
- });
66
-
67
- assert.equal(resolved, process.execPath);
68
- assert.equal(whereCalled, false, "where.exe should not be called when configuredPath is set");
69
- });
70
- });
71
-
72
- test("Codex resolver: CODEX_EXECUTABLE env var used when no configuredPath", async () => {
73
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
74
- let whereCalled = false;
75
- const resolved = await resolveCodexExecutable({
76
- runCommandImpl: mockRunCommand(commandResult(), () => { whereCalled = true; }),
77
- });
78
-
79
- assert.equal(resolved, "env-codex.cmd");
80
- assert.equal(whereCalled, false, "where.exe should not be called when CODEX_EXECUTABLE is set");
81
- });
82
- });
83
-
84
- test("Codex resolver: rejects unsafe CODEX_EXECUTABLE values", async () => {
85
- await withEnv({ CODEX_EXECUTABLE: "codex.cmd & calc" }, async () => {
86
- await assert.rejects(
87
- () => resolveCodexExecutable({
88
- runCommandImpl: mockRunCommand(commandResult()),
89
- }),
90
- /shell metacharacters|single executable name/i,
91
- );
92
- });
93
- });
94
-
95
- test("Codex resolver: accepts environment executable paths with spaces", async () => {
96
- const tempRoot = join(tmpdir(), `codexa codex resolver ${Date.now()}`);
97
- const codexPath = join(tempRoot, "codex cli.exe");
98
- mkdirSync(tempRoot, { recursive: true });
99
- writeFileSync(codexPath, "");
100
- try {
101
- await withEnv({ CODEX_EXECUTABLE: `"${codexPath}"` }, async () => {
102
- const resolved = await resolveCodexExecutable({
103
- runCommandImpl: mockRunCommand(commandResult()),
104
- });
105
- assert.equal(resolved, codexPath);
106
- });
107
- } finally {
108
- rmSync(tempRoot, { recursive: true, force: true });
109
- }
110
- });
111
-
112
- test("Codex resolver: Windows where.exe PATH lookup used when no env or config", async () => {
113
- if (process.platform !== "win32") return;
114
-
115
- const resolvedPath = join(tmpdir(), "codex.cmd");
116
- writeFileSync(resolvedPath, "");
117
- await withEnv({}, async () => {
118
- const calls: Array<Parameters<typeof runCommand>[0]> = [];
119
- const resolved = await resolveCodexExecutable({
120
- runCommandImpl: mockRunCommand(
121
- commandResult({ stdout: `${resolvedPath}\n` }),
122
- (spec) => calls.push(spec),
123
- ),
124
- });
125
-
126
- assert.equal(resolved, resolvedPath);
127
- const whereCall = calls.find((c) => c.executable === "where.exe");
128
- assert.ok(whereCall, "where.exe should have been called");
129
- });
130
- });
131
-
132
- test("Codex resolver: bare fallback returned when nothing found (non-Windows)", async () => {
133
- if (process.platform === "win32") return;
134
-
135
- await withEnv({}, async () => {
136
- const resolved = await resolveCodexExecutable({
137
- runCommandImpl: mockRunCommand(commandResult({ exitCode: 1, status: "failed" })),
138
- });
139
-
140
- assert.equal(resolved, "codex", "Should fall back to bare codex name");
141
- });
142
- });
143
-
144
- test("Codex resolver: Windows bare fallback when where.exe fails", async () => {
145
- if (process.platform !== "win32") return;
146
-
147
- await withEnv({}, async () => {
148
- const resolved = await resolveCodexExecutable({
149
- runCommandImpl: mockRunCommand(commandResult({ exitCode: 1, status: "failed" })),
150
- });
151
-
152
- assert.ok(["codex.cmd", "codex.exe", "codex"].includes(resolved), `Unexpected fallback: ${resolved}`);
153
- });
154
- });
155
-
156
- test("Codex resolver: cache is populated after first resolution", async () => {
157
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
158
- const first = await resolveCodexExecutable();
159
- const second = await resolveCodexExecutable();
160
-
161
- assert.equal(first, second);
162
- assert.equal(first, "env-codex.cmd");
163
- });
164
- });
165
-
166
- test("Codex resolver: configuredPath bypasses cache", async () => {
167
- await withEnv({ CODEX_EXECUTABLE: "env-codex.cmd" }, async () => {
168
- const cached = await resolveCodexExecutable();
169
- assert.equal(cached, "env-codex.cmd");
170
-
171
- const withOverride = await resolveCodexExecutable({
172
- configuredPath: process.execPath,
173
- });
174
- assert.equal(withOverride, process.execPath);
175
-
176
- const afterOverride = await resolveCodexExecutable();
177
- assert.equal(afterOverride, "env-codex.cmd", "Cache should not be polluted by configuredPath call");
178
- });
179
- });
180
-
181
- test("Codex resolver: resetCodexExecutableCacheForTests clears state", async () => {
182
- await withEnv({ CODEX_EXECUTABLE: "first.cmd" }, async () => {
183
- const first = await resolveCodexExecutable();
184
- assert.equal(first, "first.cmd");
185
- });
186
-
187
- resetCodexExecutableCacheForTests();
188
-
189
- await withEnv({ CODEX_EXECUTABLE: "second.cmd" }, async () => {
190
- const second = await resolveCodexExecutable();
191
- assert.equal(second, "second.cmd");
192
- });
193
- });
194
-
195
- test("spawnCodexProcess wraps .cmd in cmd.exe on Windows", () => {
196
- if (process.platform !== "win32") return;
197
-
198
- const codexPath = "C:\\Users\\Example\\AppData\\Roaming\\npm\\codex.cmd";
199
- const proc = spawnCodexProcess(codexPath, ["exec", "--help"], { stdio: ["ignore", "pipe", "pipe"] });
200
- proc.kill();
201
-
202
- assert.ok(proc, "Process should have been spawned");
203
- });
204
-
205
- test("spawnCodexProcess uses executable directly on non-Windows", () => {
206
- if (process.platform === "win32") return;
207
-
208
- const proc = spawnCodexProcess("echo", ["hello"], { stdio: ["ignore", "pipe", "pipe"] });
209
- proc.kill();
210
-
211
- assert.ok(proc, "Process should have been spawned");
212
- });