@golba98/codexa 1.0.2 → 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.
- package/README.md +391 -94
- package/package.json +12 -7
- package/src/app.tsx +149 -117
- package/src/commands/handler.ts +4 -4
- package/src/config/buildInfo.ts +2 -2
- package/src/config/persistence.ts +10 -0
- package/src/config/settings.ts +7 -15
- package/src/config/updateCheckCache.ts +19 -1
- package/src/core/channel.ts +23 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +61 -43
- package/src/core/launchContext.ts +33 -33
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/updateCheck.ts +111 -95
- package/src/session/types.ts +2 -1
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +9 -24
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +117 -78
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +10 -10
- package/src/ui/ModelReasoningPicker.tsx +14 -14
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +8 -8
- package/src/ui/ProviderPicker.tsx +18 -18
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/SelectionPanel.tsx +4 -4
- package/src/ui/SettingsPanel.tsx +8 -8
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +10 -10
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +21 -17
- package/src/ui/TopHeader.tsx +421 -293
- package/src/ui/TurnGroup.tsx +31 -31
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +203 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +1 -1
- package/src/ui/logoVariants.ts +107 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +100 -0
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +29 -19
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
|
@@ -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
|
-
});
|