@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.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,437 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import type { ChildProcess } from "node:child_process";
4
- import { writeFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { tmpdir } from "node:os";
7
- import { runCommand, type CommandResult } from "../process/CommandRunner.js";
8
- import {
9
- buildGeminiCommand,
10
- buildGeminiCliPromptArgs,
11
- buildGeminiCliValidationArgs,
12
- classifyGeminiProbeFailure,
13
- hasGeminiApiKey,
14
- isGeminiRouteConfigured,
15
- resetGeminiRouteValidationCacheForTests,
16
- runGeminiDiagnostics,
17
- runGeminiCliWithRunner,
18
- validateGeminiRoute,
19
- } from "./gemini.js";
20
- import { resetGeminiExecutableCacheForTests } from "../executables/geminiExecutable.js";
21
- import { normalizeRuntimeConfig, resolveRuntimeConfig } from "../../config/runtimeConfig.js";
22
- import type { ProviderChatRequest } from "./types.js";
23
-
24
- // A real temp file used wherever tests need a configured absolute exe path that passes the existence check.
25
- const FAKE_GEMINI_EXE = join(tmpdir(), "codexa-test-gemini.cmd");
26
- writeFileSync(FAKE_GEMINI_EXE, "");
27
-
28
- function commandResult(overrides: Partial<CommandResult>): CommandResult {
29
- return {
30
- status: "completed",
31
- exitCode: 0,
32
- signal: null,
33
- stdout: "",
34
- stderr: "",
35
- startedAt: 0,
36
- endedAt: 0,
37
- durationMs: 0,
38
- userMessage: "Command completed.",
39
- ...overrides,
40
- };
41
- }
42
-
43
- async function withGeminiEnv<T>(
44
- env: Partial<NodeJS.ProcessEnv>,
45
- callback: () => T | Promise<T>,
46
- ): Promise<T> {
47
- const originalGemini = process.env.GEMINI_API_KEY;
48
- const originalGoogle = process.env.GOOGLE_API_KEY;
49
- const originalExe = process.env.GEMINI_EXECUTABLE;
50
- const originalCliPath = process.env.GEMINI_CLI_PATH;
51
-
52
- try {
53
- if ("GEMINI_API_KEY" in env) process.env.GEMINI_API_KEY = env.GEMINI_API_KEY;
54
- else delete process.env.GEMINI_API_KEY;
55
- if ("GOOGLE_API_KEY" in env) process.env.GOOGLE_API_KEY = env.GOOGLE_API_KEY;
56
- else delete process.env.GOOGLE_API_KEY;
57
- if ("GEMINI_EXECUTABLE" in env) process.env.GEMINI_EXECUTABLE = env.GEMINI_EXECUTABLE;
58
- else delete process.env.GEMINI_EXECUTABLE;
59
- if ("GEMINI_CLI_PATH" in env) process.env.GEMINI_CLI_PATH = env.GEMINI_CLI_PATH;
60
- else delete process.env.GEMINI_CLI_PATH;
61
- resetGeminiExecutableCacheForTests();
62
- return await callback();
63
- } finally {
64
- if (originalGemini === undefined) delete process.env.GEMINI_API_KEY;
65
- else process.env.GEMINI_API_KEY = originalGemini;
66
- if (originalGoogle === undefined) delete process.env.GOOGLE_API_KEY;
67
- else process.env.GOOGLE_API_KEY = originalGoogle;
68
- if (originalExe === undefined) delete process.env.GEMINI_EXECUTABLE;
69
- else process.env.GEMINI_EXECUTABLE = originalExe;
70
- if (originalCliPath === undefined) delete process.env.GEMINI_CLI_PATH;
71
- else process.env.GEMINI_CLI_PATH = originalCliPath;
72
- resetGeminiRouteValidationCacheForTests();
73
- resetGeminiExecutableCacheForTests();
74
- }
75
- }
76
-
77
- function mockRunCommand(result: CommandResult, onCall?: (spec: Parameters<typeof runCommand>[0]) => void): typeof runCommand {
78
- return ((spec) => {
79
- onCall?.(spec);
80
- return {
81
- child: null as unknown as ChildProcess,
82
- result: Promise.resolve(result),
83
- cancel: () => undefined,
84
- };
85
- }) as typeof runCommand;
86
- }
87
-
88
- function buildRequest(overrides: Partial<ProviderChatRequest> = {}): ProviderChatRequest {
89
- return {
90
- prompt: "hello",
91
- route: {
92
- providerId: "google",
93
- modelId: "gemini-3-flash-preview",
94
- backendKind: "gemini-cli-auth",
95
- },
96
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
97
- model: "gemini-3-flash-preview",
98
- geminiCommandPath: FAKE_GEMINI_EXE,
99
- })),
100
- workspaceRoot: process.cwd(),
101
- ...overrides,
102
- };
103
- }
104
-
105
- test("Gemini route validation returns command-not-found diagnostic with PS commands when executable missing", async () => {
106
- await withGeminiEnv({}, async () => {
107
- const validation = await validateGeminiRoute({
108
- cwd: process.cwd(),
109
- modelId: "gemini-3-flash-preview",
110
- runCommandImpl: mockRunCommand(commandResult({
111
- status: "spawn_error",
112
- exitCode: null,
113
- errorCode: "ENOENT",
114
- userMessage: "`gemini` is not installed or not available on PATH.",
115
- })),
116
- });
117
-
118
- assert.equal(validation.status, "not-configured");
119
- assert.equal(validation.backendKind, "unavailable");
120
- assert.match(validation.message ?? "", /Gemini CLI was not found|Gemini CLI was not found as a real executable/);
121
- assert.match(validation.message ?? "", /GEMINI_EXECUTABLE/);
122
- assert.equal(isGeminiRouteConfigured(), false);
123
- });
124
- });
125
-
126
- test("Gemini readiness uses resolved executable, Gemini 3 Flash Preview, and combined READY output", async () => {
127
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
128
- const calls: Array<Parameters<typeof runCommand>[0]> = [];
129
- const validation = await validateGeminiRoute({
130
- cwd: process.cwd(),
131
- modelId: "gemini-3-flash-preview",
132
- runCommandImpl: mockRunCommand(commandResult({
133
- stderr: "READY\n",
134
- }), (spec) => calls.push(spec)),
135
- });
136
-
137
- const probe = calls.find((call) => call.args.includes("-p"));
138
- assert.equal(validation.status, "ready");
139
- assert.equal(probe?.executable, FAKE_GEMINI_EXE);
140
- assert.deepEqual(probe?.args, ["--model", "gemini-3-flash-preview", "-p", "Respond with READY only."]);
141
- assert.equal("shell" in (probe ?? {}), false);
142
- assert.equal(probe?.args.includes("--reasoning"), false);
143
- assert.equal(validation.diagnostics?.resolvedCommand, FAKE_GEMINI_EXE);
144
- assert.equal(validation.diagnostics?.lastProbeCommandArgs, JSON.stringify(["--model", "gemini-3-flash-preview", "-p", "Respond with READY only."]));
145
- assert.equal(validation.diagnostics?.readyTokenObserved, true);
146
- });
147
- });
148
-
149
- test("Gemini command builders use verified model IDs and no reasoning argv", () => {
150
- assert.deepEqual(buildGeminiCliValidationArgs(), ["--model", "gemini-3-flash-preview", "-p", "Respond with READY only."]);
151
- for (const modelId of [
152
- "gemini-3.1-pro-preview",
153
- "gemini-3-flash-preview",
154
- "gemini-3.1-flash-lite-preview",
155
- "gemini-2.5-pro",
156
- "gemini-2.5-flash",
157
- "gemini-2.5-flash-lite",
158
- ]) {
159
- assert.deepEqual(buildGeminiCliPromptArgs("hello", modelId), ["--model", modelId, "-p", "hello"]);
160
- }
161
- assert.deepEqual(buildGeminiCliPromptArgs("hello", "gemini-3-flash", true), ["--model", "gemini-3-flash-preview", "-p", "hello"]);
162
-
163
- for (const args of [
164
- buildGeminiCliValidationArgs(),
165
- buildGeminiCliPromptArgs("hello", "gemini-3-flash-preview"),
166
- buildGeminiCliPromptArgs("hello", "gemini-2.5-pro", resolveRuntimeConfig(normalizeRuntimeConfig({ mode: "full-auto" }))),
167
- ]) {
168
- assert.equal(args.includes("--reasoning"), false);
169
- assert.equal(args.includes("--approval-mode"), false);
170
- assert.equal(args.includes("--output-format"), false);
171
- }
172
- });
173
-
174
- test("Gemini command builder returns exact readiness and prompt specs", async () => {
175
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
176
- const readiness = await buildGeminiCommand({
177
- cwd: process.cwd(),
178
- mode: "readiness",
179
- runCommandImpl: mockRunCommand(commandResult({ stdout: "READY\n" })),
180
- });
181
- assert.equal(readiness.file, FAKE_GEMINI_EXE);
182
- assert.deepEqual(readiness.args, ["--model", "gemini-3-flash-preview", "-p", "Respond with READY only."]);
183
- assert.equal(readiness.mode, "readiness");
184
- assert.equal(readiness.model, "gemini-3-flash-preview");
185
- assert.equal(readiness.includesPolicy, false);
186
-
187
- const prompt = await buildGeminiCommand({
188
- cwd: process.cwd(),
189
- mode: "prompt",
190
- prompt: "hello",
191
- model: "gemini-3-flash",
192
- reasoning: "high",
193
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({ mode: "full-auto" })),
194
- runCommandImpl: mockRunCommand(commandResult({ stdout: "READY\n" })),
195
- });
196
- assert.equal(prompt.file, FAKE_GEMINI_EXE);
197
- assert.deepEqual(prompt.args, ["--model", "gemini-3-flash-preview", "-p", "hello"]);
198
- assert.equal(prompt.mode, "prompt");
199
- assert.equal(prompt.model, "gemini-3-flash-preview");
200
- assert.equal(prompt.reasoning, "high");
201
- assert.equal(prompt.args.includes("--reasoning"), false);
202
- assert.equal(prompt.args.includes("--approval-mode"), false);
203
- assert.equal(prompt.args.includes("--output-format"), false);
204
- assert.equal(prompt.includesPolicy, false);
205
- });
206
- });
207
-
208
- test("Gemini prompt execution appends plain stdout as assistant text", async () => {
209
- await withGeminiEnv({}, async () => {
210
- const calls: Array<Parameters<typeof runCommand>[0]> = [];
211
- const text = await runGeminiCliWithRunner(
212
- buildRequest({
213
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
214
- model: "gemini-3-flash-preview",
215
- mode: "full-auto",
216
- geminiCommandPath: FAKE_GEMINI_EXE,
217
- })),
218
- }),
219
- mockRunCommand(commandResult({ stdout: "done\n" }), (spec) => {
220
- if (spec.args.includes("-p")) calls.push(spec);
221
- }),
222
- );
223
-
224
- assert.equal(text, "done");
225
- assert.deepEqual(calls[0]?.args, ["--model", "gemini-3-flash-preview", "-p", "hello"]);
226
- assert.equal("shell" in (calls[0] ?? {}), false);
227
- });
228
- });
229
-
230
- test("Gemini prompt execution detects policy file error and provides diagnostics", async () => {
231
- await withGeminiEnv({}, async () => {
232
- await assert.rejects(
233
- () => runGeminiCliWithRunner(
234
- buildRequest(),
235
- mockRunCommand(commandResult({
236
- status: "completed",
237
- exitCode: 1,
238
- stderr: "[USER] Policy file error in auto-saved.toml: validation failed",
239
- })),
240
- ),
241
- /Policy file error in Gemini CLI[\s\S]*validation failed/,
242
- );
243
- });
244
- });
245
-
246
- test("Gemini bad PowerShell wrapper -p ambiguity is classified as wrapper conflict", async () => {
247
- const result = commandResult({
248
- status: "failed",
249
- exitCode: 1,
250
- stderr: "Parameter cannot be processed because the parameter name 'p' is ambiguous. Possible matches include: -ProgressAction -PipelineVariable",
251
- });
252
- assert.equal(classifyGeminiProbeFailure(result), "shell wrapper/function conflict");
253
-
254
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
255
- const validation = await validateGeminiRoute({
256
- cwd: process.cwd(),
257
- modelId: "gemini-3-flash-preview",
258
- runCommandImpl: mockRunCommand(result),
259
- });
260
- assert.equal(validation.status, "not-configured");
261
- assert.equal(validation.diagnostics?.failureReason, "shell wrapper/function conflict");
262
- assert.doesNotMatch(validation.message ?? "", /not found/i);
263
- });
264
- });
265
-
266
- test("Gemini route validation returns auth-unknown diagnostic if executable found but probe fails", async () => {
267
- await withGeminiEnv({ GEMINI_EXECUTABLE: "my-gemini" }, async () => {
268
- const validation = await validateGeminiRoute({
269
- cwd: process.cwd(),
270
- modelId: "gemini-3-flash-preview",
271
- runCommandImpl: mockRunCommand(commandResult({
272
- status: "completed",
273
- exitCode: 1,
274
- stderr: "Auth failed",
275
- })),
276
- });
277
-
278
- assert.equal(validation.status, "not-configured");
279
- assert.match(validation.message ?? "", /Gemini CLI installed, auth unknown/);
280
- });
281
- });
282
-
283
- test("Gemini route validation returns timeout diagnostic on probe timeout", async () => {
284
- await withGeminiEnv({ GEMINI_EXECUTABLE: "my-gemini" }, async () => {
285
- const validation = await validateGeminiRoute({
286
- cwd: process.cwd(),
287
- modelId: "gemini-3-flash-preview",
288
- runCommandImpl: mockRunCommand(commandResult({
289
- status: "timeout",
290
- exitCode: null,
291
- })),
292
- });
293
-
294
- assert.equal(validation.status, "not-configured");
295
- assert.equal(validation.message, "Installed but headless probe timed out.");
296
- });
297
- });
298
-
299
- test("Gemini route validation preferences authenticated CLI over API key", async () => {
300
- await withGeminiEnv({ GEMINI_API_KEY: "gemini-key" }, async () => {
301
- let commandCalled = false;
302
- const validation = await validateGeminiRoute({
303
- cwd: process.cwd(),
304
- modelId: "gemini-3-flash-preview",
305
- runCommandImpl: mockRunCommand(commandResult({
306
- stdout: JSON.stringify({ response: "READY" }),
307
- }), () => {
308
- commandCalled = true;
309
- }),
310
- });
311
-
312
- assert.equal(validation.status, "ready");
313
- assert.equal(validation.backendKind, "gemini-cli-auth");
314
- assert.equal(commandCalled, true);
315
- assert.equal(isGeminiRouteConfigured(), true);
316
- });
317
- });
318
-
319
- test("Gemini route validation falls back to GEMINI_API_KEY if CLI fails", async () => {
320
- await withGeminiEnv({ GEMINI_API_KEY: "gemini-key" }, async () => {
321
- const validation = await validateGeminiRoute({
322
- cwd: process.cwd(),
323
- modelId: "gemini-3-flash-preview",
324
- runCommandImpl: mockRunCommand(commandResult({
325
- status: "spawn_error",
326
- errorCode: "ENOENT",
327
- })),
328
- });
329
-
330
- assert.equal(validation.status, "ready");
331
- assert.equal(validation.backendKind, "gemini-api-key");
332
- });
333
- });
334
-
335
- test("Gemini non-zero model errors surface without silent retry", async () => {
336
- await withGeminiEnv({}, async () => {
337
- const calls: Array<Parameters<typeof runCommand>[0]> = [];
338
- await assert.rejects(
339
- () => runGeminiCliWithRunner(
340
- buildRequest(),
341
- mockRunCommand(commandResult({
342
- status: "failed",
343
- exitCode: 1,
344
- stderr: "ModelNotFoundError: Requested entity was not found.",
345
- }), (spec) => calls.push(spec)),
346
- ),
347
- /ModelNotFoundError: Requested entity was not found/,
348
- );
349
-
350
- assert.equal(calls.length, 1);
351
- assert.deepEqual(calls[0]?.args, ["--model", "gemini-3-flash-preview", "-p", "hello"]);
352
- });
353
- });
354
-
355
- test("Gemini diagnostics include command details without prompt text", async () => {
356
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
357
- await runGeminiCliWithRunner(
358
- buildRequest({ prompt: "secret prompt text" }),
359
- mockRunCommand(commandResult({ stdout: "done" })),
360
- );
361
-
362
- const diagnostics = await runGeminiDiagnostics({
363
- cwd: process.cwd(),
364
- runtime: resolveRuntimeConfig(normalizeRuntimeConfig({
365
- mode: "full-auto",
366
- geminiCommandPath: FAKE_GEMINI_EXE,
367
- })),
368
- selectedModel: "gemini-3-flash",
369
- selectedReasoning: "high",
370
- runCommandImpl: mockRunCommand(commandResult({ stdout: "READY\n" })),
371
- });
372
-
373
- assert.ok(diagnostics.includes(`Resolved executable path: ${FAKE_GEMINI_EXE}`), `Expected diagnostics to include resolved path`);
374
- assert.match(diagnostics, /Readiness command args: \["--model","gemini-3-flash-preview","-p","Respond with READY only\."\]/);
375
- assert.match(diagnostics, /Selected model: gemini-3-flash-preview/);
376
- assert.match(diagnostics, /Policy args included: false/);
377
- assert.match(diagnostics, /Gemini reasoning control is not supported by this CLI version/);
378
- assert.match(diagnostics, /Last prompt command args: \["--model","gemini-3-flash-preview","-p","<prompt>"/);
379
- assert.doesNotMatch(diagnostics, /secret prompt text/);
380
- });
381
- });
382
-
383
- test("Gemini API key detection remains provider-specific", () => {
384
- assert.equal(hasGeminiApiKey({ GEMINI_API_KEY: "key" } as NodeJS.ProcessEnv), true);
385
- assert.equal(hasGeminiApiKey({ GOOGLE_API_KEY: "key" } as NodeJS.ProcessEnv), true);
386
- assert.equal(hasGeminiApiKey({} as NodeJS.ProcessEnv), false);
387
- });
388
-
389
- // ─── Provider selection / runtime sync tests ─────────────────────────────────
390
-
391
- test("isGeminiRouteConfigured is false with no API key and no cached CLI validation", async () => {
392
- await withGeminiEnv({}, () => {
393
- assert.equal(isGeminiRouteConfigured(), false);
394
- });
395
- });
396
-
397
- test("isGeminiRouteConfigured is true with GEMINI_API_KEY set", async () => {
398
- await withGeminiEnv({ GEMINI_API_KEY: "test-key" }, () => {
399
- assert.equal(isGeminiRouteConfigured(), true);
400
- });
401
- });
402
-
403
- test("isGeminiRouteConfigured is true with GOOGLE_API_KEY set", async () => {
404
- await withGeminiEnv({ GOOGLE_API_KEY: "test-key" }, () => {
405
- assert.equal(isGeminiRouteConfigured(), true);
406
- });
407
- });
408
-
409
- test("isGeminiRouteConfigured is true after successful CLI validation", async () => {
410
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
411
- assert.equal(isGeminiRouteConfigured(), false);
412
- await validateGeminiRoute({
413
- cwd: process.cwd(),
414
- modelId: "gemini-3-flash-preview",
415
- runCommandImpl: mockRunCommand(commandResult({ stderr: "READY\n" })),
416
- });
417
- assert.equal(isGeminiRouteConfigured(), true);
418
- });
419
- });
420
-
421
- test("validateGeminiRoute not-configured message names missing credentials when CLI not found", async () => {
422
- await withGeminiEnv({ GEMINI_EXECUTABLE: FAKE_GEMINI_EXE }, async () => {
423
- const result = await validateGeminiRoute({
424
- cwd: process.cwd(),
425
- modelId: "gemini-3-flash-preview",
426
- runCommandImpl: mockRunCommand(commandResult({
427
- status: "spawn_error",
428
- exitCode: null,
429
- errorCode: "ENOENT",
430
- userMessage: "`gemini` is not installed or not available on PATH.",
431
- })),
432
- });
433
- assert.equal(result.status, "not-configured");
434
- assert.match(result.message ?? "", /GEMINI_API_KEY|GOOGLE_API_KEY|GEMINI_EXECUTABLE|Gemini CLI/);
435
- assert.equal(isGeminiRouteConfigured(), false);
436
- });
437
- });
@@ -1,168 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { deriveLmStudioApiRoot, fetchLmStudioModelInfo, fetchLmStudioModels, parseLmStudioModelsResponse } from "./lmstudio.js";
4
-
5
- const FIXTURE = {
6
- id: "google/gemma-4-26b-a4b",
7
- object: "model",
8
- type: "vlm",
9
- publisher: "google",
10
- arch: "gemma4",
11
- compatibility_type: "gguf",
12
- quantization: "Q4_K_M",
13
- state: "loaded",
14
- max_context_length: 262144,
15
- loaded_context_length: 64000,
16
- capabilities: ["tool_use"],
17
- };
18
-
19
- const LIST_FIXTURE = {
20
- data: [
21
- {
22
- id: "qwen/qwen3.6-27b",
23
- object: "model",
24
- type: "vlm",
25
- publisher: "qwen",
26
- arch: "qwen35",
27
- compatibility_type: "gguf",
28
- quantization: "Q4_K_M",
29
- state: "loaded",
30
- max_context_length: 262144,
31
- loaded_context_length: 32000,
32
- capabilities: [
33
- "tool_use",
34
- ],
35
- },
36
- ],
37
- object: "list",
38
- };
39
-
40
- test("deriveLmStudioApiRoot strips /v1 path", () => {
41
- assert.equal(deriveLmStudioApiRoot("http://localhost:1234/v1"), "http://localhost:1234/api/v0");
42
- });
43
-
44
- test("deriveLmStudioApiRoot strips trailing slash from /v1/", () => {
45
- assert.equal(deriveLmStudioApiRoot("http://localhost:1234/v1/"), "http://localhost:1234/api/v0");
46
- });
47
-
48
- test("deriveLmStudioApiRoot works with arbitrary host and port", () => {
49
- assert.equal(
50
- deriveLmStudioApiRoot("http://my-server.local:8080/api/v1"),
51
- "http://my-server.local:8080/api/v0",
52
- );
53
- });
54
-
55
- test("deriveLmStudioApiRoot returns null for invalid URL", () => {
56
- assert.equal(deriveLmStudioApiRoot("not-a-url"), null);
57
- });
58
-
59
- test("fetchLmStudioModelInfo URL-encodes model IDs containing slashes", async () => {
60
- let capturedUrl = "";
61
- const mockFetch = async (url: string) => {
62
- capturedUrl = url;
63
- return new Response(JSON.stringify(FIXTURE), { status: 200 });
64
- };
65
-
66
- await fetchLmStudioModelInfo({
67
- apiRoot: "http://localhost:1234/api/v0",
68
- modelId: "google/gemma-4-26b-a4b",
69
- fetchImpl: mockFetch as typeof fetch,
70
- });
71
-
72
- assert.match(capturedUrl, /google%2Fgemma-4-26b-a4b/);
73
- assert.doesNotMatch(capturedUrl, /google\/gemma/);
74
- });
75
-
76
- test("fetchLmStudioModelInfo returns fixture values on success", async () => {
77
- const mockFetch = async () => new Response(JSON.stringify(FIXTURE), { status: 200 });
78
-
79
- const result = await fetchLmStudioModelInfo({
80
- apiRoot: "http://localhost:1234/api/v0",
81
- modelId: "google/gemma-4-26b-a4b",
82
- fetchImpl: mockFetch as typeof fetch,
83
- });
84
-
85
- assert.deepEqual(result, FIXTURE);
86
- });
87
-
88
- test("fetchLmStudioModelInfo returns null on non-2xx response", async () => {
89
- const mockFetch = async () => new Response("Not Found", { status: 404 });
90
-
91
- const result = await fetchLmStudioModelInfo({
92
- apiRoot: "http://localhost:1234/api/v0",
93
- modelId: "some-model",
94
- fetchImpl: mockFetch as typeof fetch,
95
- });
96
-
97
- assert.equal(result, null);
98
- });
99
-
100
- test("fetchLmStudioModelInfo returns null on network error", async () => {
101
- const mockFetch = async (): Promise<Response> => {
102
- throw new Error("ECONNREFUSED");
103
- };
104
-
105
- const result = await fetchLmStudioModelInfo({
106
- apiRoot: "http://localhost:1234/api/v0",
107
- modelId: "some-model",
108
- fetchImpl: mockFetch as typeof fetch,
109
- });
110
-
111
- assert.equal(result, null);
112
- });
113
-
114
- test("fetchLmStudioModelInfo returns null on invalid JSON", async () => {
115
- const mockFetch = async () => new Response("not json{{{", { status: 200 });
116
-
117
- const result = await fetchLmStudioModelInfo({
118
- apiRoot: "http://localhost:1234/api/v0",
119
- modelId: "some-model",
120
- fetchImpl: mockFetch as typeof fetch,
121
- });
122
-
123
- assert.equal(result, null);
124
- });
125
-
126
- test("fetchLmStudioModelInfo returns null when response is missing id field", async () => {
127
- const mockFetch = async () =>
128
- new Response(JSON.stringify({ object: "model", type: "vlm" }), { status: 200 });
129
-
130
- const result = await fetchLmStudioModelInfo({
131
- apiRoot: "http://localhost:1234/api/v0",
132
- modelId: "some-model",
133
- fetchImpl: mockFetch as typeof fetch,
134
- });
135
-
136
- assert.equal(result, null);
137
- });
138
-
139
- test("parseLmStudioModelsResponse parses LM Studio data array list response", () => {
140
- const parsed = parseLmStudioModelsResponse(LIST_FIXTURE);
141
-
142
- assert.equal(parsed?.object, "list");
143
- assert.equal(parsed?.data[0]?.id, "qwen/qwen3.6-27b");
144
- assert.equal(parsed?.data[0]?.state, "loaded");
145
- assert.equal(parsed?.data[0]?.loaded_context_length, 32000);
146
- assert.equal(parsed?.data[0]?.max_context_length, 262144);
147
- assert.deepEqual(parsed?.data[0]?.capabilities, ["tool_use"]);
148
- });
149
-
150
- test("parseLmStudioModelsResponse does not expect the response itself to be an array", () => {
151
- assert.equal(parseLmStudioModelsResponse([LIST_FIXTURE.data[0]]), null);
152
- });
153
-
154
- test("fetchLmStudioModels reads native /api/v0/models list endpoint", async () => {
155
- let capturedUrl = "";
156
- const mockFetch = async (url: string) => {
157
- capturedUrl = url;
158
- return new Response(JSON.stringify(LIST_FIXTURE), { status: 200 });
159
- };
160
-
161
- const result = await fetchLmStudioModels({
162
- apiRoot: "http://localhost:1234/api/v0",
163
- fetchImpl: mockFetch as typeof fetch,
164
- });
165
-
166
- assert.equal(capturedUrl, "http://localhost:1234/api/v0/models");
167
- assert.equal(result?.data[0]?.id, "qwen/qwen3.6-27b");
168
- });