claude-code-provider-switch 1.1.4 → 1.1.6
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 +88 -13
- package/bin/claude-switch.js +78 -37
- package/index.js +19 -13
- package/lib/anthropic.js +3 -5
- package/lib/config.js +491 -36
- package/lib/menu.js +80 -52
- package/lib/ollama.js +3 -5
- package/lib/openrouter.js +2 -4
- package/package.json +12 -10
- package/test/run-tests.js +56 -0
- package/test/test-comprehensive.js +282 -0
- package/test/test-provider-integration.js +392 -0
- package/test/test-validation-errors.js +324 -0
package/lib/menu.js
CHANGED
|
@@ -9,6 +9,10 @@ const {
|
|
|
9
9
|
getProviderDefaultModel,
|
|
10
10
|
setDefaultProvider,
|
|
11
11
|
setDefaultModel,
|
|
12
|
+
showApiKeyMenu,
|
|
13
|
+
getConfigurationSource,
|
|
14
|
+
getConfigurationPath,
|
|
15
|
+
hasGlobalConfiguration,
|
|
12
16
|
} = require("./config");
|
|
13
17
|
const {
|
|
14
18
|
showModelSelection: showOpenRouterModelSelection,
|
|
@@ -27,12 +31,6 @@ async function showModelSelectionForProvider(provider) {
|
|
|
27
31
|
|
|
28
32
|
switch (provider.id) {
|
|
29
33
|
case "openrouter":
|
|
30
|
-
if (!envVars.OPENROUTER_AUTH_TOKEN) {
|
|
31
|
-
log("OpenRouter auth token required for model selection", "red");
|
|
32
|
-
log("Please set OPENROUTER_AUTH_TOKEN in .env file", "yellow");
|
|
33
|
-
log("Using default model...", "yellow");
|
|
34
|
-
return getProviderDefaultModel("openrouter");
|
|
35
|
-
}
|
|
36
34
|
return await showOpenRouterModelSelection();
|
|
37
35
|
|
|
38
36
|
case "ollama":
|
|
@@ -59,10 +57,12 @@ function showProviderMenu() {
|
|
|
59
57
|
output: process.stdout,
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
// Get current defaults
|
|
60
|
+
// Get current defaults and configuration source
|
|
63
61
|
const defaultProvider = getDefaultProvider();
|
|
64
62
|
const defaultModel = getDefaultModel();
|
|
63
|
+
const configSource = getConfigurationSource();
|
|
65
64
|
|
|
65
|
+
// Build providers array dynamically
|
|
66
66
|
const providers = [
|
|
67
67
|
{
|
|
68
68
|
id: "openrouter",
|
|
@@ -77,9 +77,25 @@ function showProviderMenu() {
|
|
|
77
77
|
aliases: ["original", "orig", "def", "d"],
|
|
78
78
|
},
|
|
79
79
|
{ id: "set-default", name: "Set as Default", aliases: ["set-default"] },
|
|
80
|
-
{ id: "
|
|
80
|
+
{ id: "api-keys", name: "Manage API Keys", aliases: ["api-keys", "keys"] },
|
|
81
81
|
];
|
|
82
82
|
|
|
83
|
+
// Add "Save Configuration Locally" option only if global configuration exists
|
|
84
|
+
if (hasGlobalConfiguration()) {
|
|
85
|
+
providers.push({
|
|
86
|
+
id: "save-local",
|
|
87
|
+
name: "Save Configuration Locally",
|
|
88
|
+
aliases: ["save-local", "local", "save-locally"],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Always add Help at the end
|
|
93
|
+
providers.push({
|
|
94
|
+
id: "help",
|
|
95
|
+
name: "Help",
|
|
96
|
+
aliases: ["help", "-h", "--help"],
|
|
97
|
+
});
|
|
98
|
+
|
|
83
99
|
let selectedIndex = 0;
|
|
84
100
|
|
|
85
101
|
function displayMenu() {
|
|
@@ -87,8 +103,18 @@ function showProviderMenu() {
|
|
|
87
103
|
log("Claude Code Provider Switcher", "green");
|
|
88
104
|
log("", "reset");
|
|
89
105
|
|
|
106
|
+
// Show configuration source with file path
|
|
107
|
+
const configSource = getConfigurationSource();
|
|
108
|
+
const configPath = getConfigurationPath();
|
|
109
|
+
log(`Configuration: ${configSource} (${configPath})`, "cyan");
|
|
110
|
+
log("", "reset");
|
|
111
|
+
|
|
90
112
|
// Show current defaults
|
|
91
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
defaultProvider &&
|
|
115
|
+
defaultProvider !== null &&
|
|
116
|
+
defaultProvider !== "default"
|
|
117
|
+
) {
|
|
92
118
|
const providerName =
|
|
93
119
|
providers.find((p) => p.id === defaultProvider)?.name ||
|
|
94
120
|
defaultProvider;
|
|
@@ -98,10 +124,8 @@ function showProviderMenu() {
|
|
|
98
124
|
`Current default: ${providerName}${currentModel ? ` (${currentModel})` : ""}`,
|
|
99
125
|
"yellow",
|
|
100
126
|
);
|
|
101
|
-
|
|
102
|
-
log("No default provider set", "yellow");
|
|
127
|
+
log("", "reset");
|
|
103
128
|
}
|
|
104
|
-
log("", "reset");
|
|
105
129
|
|
|
106
130
|
log("Available providers:", "yellow");
|
|
107
131
|
log("", "reset");
|
|
@@ -111,18 +135,21 @@ function showProviderMenu() {
|
|
|
111
135
|
const isDefault = provider.id === defaultProvider;
|
|
112
136
|
const defaultIndicator = isDefault ? " [DEFAULT]" : "";
|
|
113
137
|
const isSetDefaultOption = provider.id === "set-default";
|
|
138
|
+
const isApiKeysOption = provider.id === "api-keys";
|
|
114
139
|
const aliases =
|
|
115
140
|
provider.aliases.length > 0
|
|
116
141
|
? ` Aliases: (${provider.aliases.join(", ")})`
|
|
117
142
|
: "";
|
|
118
143
|
|
|
119
|
-
// Highlight
|
|
144
|
+
// Highlight special options with different colors
|
|
120
145
|
const color =
|
|
121
146
|
index === selectedIndex
|
|
122
147
|
? "green"
|
|
123
148
|
: isSetDefaultOption
|
|
124
149
|
? "orange"
|
|
125
|
-
:
|
|
150
|
+
: isApiKeysOption
|
|
151
|
+
? "cyan"
|
|
152
|
+
: "reset";
|
|
126
153
|
|
|
127
154
|
log(
|
|
128
155
|
`${marker} ${index + 1}) ${provider.name}${defaultIndicator}${aliases}`,
|
|
@@ -130,51 +157,47 @@ function showProviderMenu() {
|
|
|
130
157
|
);
|
|
131
158
|
});
|
|
132
159
|
|
|
133
|
-
log("", "reset");
|
|
134
|
-
log("Controls:", "yellow");
|
|
135
|
-
log("↑/↓ - Navigate", "reset");
|
|
136
|
-
log("Enter - Select provider", "reset");
|
|
137
|
-
log("1-6 - Quick select", "reset");
|
|
138
|
-
log("ESC - Exit", "reset");
|
|
139
160
|
log("", "reset");
|
|
140
161
|
|
|
141
162
|
// Add helpful usage text
|
|
142
163
|
log("💡 Quick Start:", "cyan");
|
|
143
164
|
log("• Use ↑/↓ arrows to navigate providers", "reset");
|
|
144
165
|
log("• Press Enter to launch selected provider", "reset");
|
|
166
|
+
log(`• Press 1-${providers.length} for quick select`, "reset");
|
|
145
167
|
log("• Use 'Set as Default' to save your preferred provider", "reset");
|
|
146
168
|
log("", "reset");
|
|
147
|
-
|
|
148
|
-
log(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
log(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
log("
|
|
167
|
-
log(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
log("
|
|
177
|
-
log("", "
|
|
169
|
+
// Commented out Commands section for cleaner menu
|
|
170
|
+
// log("Commands:", "yellow");
|
|
171
|
+
// log(
|
|
172
|
+
// " claude-switch - Show menu or use default",
|
|
173
|
+
// "reset",
|
|
174
|
+
// );
|
|
175
|
+
// log(
|
|
176
|
+
// " claude-switch openrouter - Use OpenRouter provider",
|
|
177
|
+
// "reset",
|
|
178
|
+
// );
|
|
179
|
+
// log(
|
|
180
|
+
// " claude-switch anthropic - Use Anthropic provider",
|
|
181
|
+
// "reset",
|
|
182
|
+
// );
|
|
183
|
+
// log(" claude-switch ollama - Use Ollama provider", "reset");
|
|
184
|
+
// log(
|
|
185
|
+
// " claude-switch set-default - Setup default provider",
|
|
186
|
+
// "reset",
|
|
187
|
+
// );
|
|
188
|
+
// log("", "reset");
|
|
189
|
+
// log("Model Selection:", "yellow");
|
|
190
|
+
// log(
|
|
191
|
+
// " claude-switch openrouter --model - Select OpenRouter model",
|
|
192
|
+
// "reset",
|
|
193
|
+
// );
|
|
194
|
+
// log(
|
|
195
|
+
// " claude-switch anthropic --model - Select Anthropic model",
|
|
196
|
+
// "reset",
|
|
197
|
+
// );
|
|
198
|
+
// log("", "reset");
|
|
199
|
+
// log("Type 'claude-switch --help' for complete documentation", "cyan");
|
|
200
|
+
// log("", "reset");
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
displayMenu();
|
|
@@ -197,7 +220,7 @@ function showProviderMenu() {
|
|
|
197
220
|
process.stdin.removeAllListeners("keypress");
|
|
198
221
|
rl.close();
|
|
199
222
|
resolve(providers[selectedIndex]);
|
|
200
|
-
} else if (str && /^[1-
|
|
223
|
+
} else if (str && /^[1-7]$/.test(str)) {
|
|
201
224
|
const index = parseInt(str) - 1;
|
|
202
225
|
if (index >= 0 && index < providers.length) {
|
|
203
226
|
selectedIndex = index;
|
|
@@ -483,6 +506,11 @@ function showUsage() {
|
|
|
483
506
|
);
|
|
484
507
|
log(" show-defaults - Display current default settings", "reset");
|
|
485
508
|
log(" clear-defaults - Reset all default settings", "reset");
|
|
509
|
+
log(" api-keys - Manage API keys for providers", "reset");
|
|
510
|
+
log(
|
|
511
|
+
" save-local - Save global configuration to local .env file",
|
|
512
|
+
"reset",
|
|
513
|
+
);
|
|
486
514
|
log("", "reset");
|
|
487
515
|
log("Options:", "reset");
|
|
488
516
|
log(" --model - Show interactive model selection menu", "reset");
|
package/lib/ollama.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
loadEnvFile,
|
|
11
11
|
findBestMatchingModel,
|
|
12
12
|
promptForApiKey,
|
|
13
|
-
|
|
13
|
+
updateConfigFile,
|
|
14
14
|
} = require("./config");
|
|
15
15
|
const { modelCache } = require("./cache");
|
|
16
16
|
const { OLLAMA, CACHE, HTTP_STATUS, DEFAULT_MODELS } = require("./constants");
|
|
@@ -97,7 +97,7 @@ async function showModelSelection() {
|
|
|
97
97
|
log("Ollama auth token is required for model selection", "red");
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
updateConfigFile("OLLAMA_AUTH_TOKEN", newToken, null);
|
|
101
101
|
authToken = newToken;
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -378,8 +378,6 @@ async function launchOllama(
|
|
|
378
378
|
extraArgs = [],
|
|
379
379
|
directModel = null,
|
|
380
380
|
) {
|
|
381
|
-
log("Launching Claude Code with Ollama settings...", "green");
|
|
382
|
-
|
|
383
381
|
const envVars = loadEnvFile();
|
|
384
382
|
log(`Loading environment from: ${envVars.envFile}`, "yellow");
|
|
385
383
|
|
|
@@ -391,7 +389,7 @@ async function launchOllama(
|
|
|
391
389
|
log("Press Enter to skip, or provide an auth token:", "reset");
|
|
392
390
|
authToken = await promptForApiKey("Ollama (optional)", "OLLAMA_AUTH_TOKEN");
|
|
393
391
|
if (authToken) {
|
|
394
|
-
|
|
392
|
+
updateConfigFile("OLLAMA_AUTH_TOKEN", authToken, null);
|
|
395
393
|
envVars.OLLAMA_AUTH_TOKEN = authToken;
|
|
396
394
|
}
|
|
397
395
|
}
|
package/lib/openrouter.js
CHANGED
|
@@ -9,7 +9,7 @@ const {
|
|
|
9
9
|
loadEnvFile,
|
|
10
10
|
findBestMatchingModel,
|
|
11
11
|
promptForApiKey,
|
|
12
|
-
|
|
12
|
+
updateConfigFile,
|
|
13
13
|
} = require("./config");
|
|
14
14
|
const { modelCache } = require("./cache");
|
|
15
15
|
const {
|
|
@@ -435,8 +435,6 @@ async function launchOpenRouter(
|
|
|
435
435
|
extraArgs = [],
|
|
436
436
|
directModel = null,
|
|
437
437
|
) {
|
|
438
|
-
log("Launching Claude Code with OpenRouter settings...", "green");
|
|
439
|
-
|
|
440
438
|
const envVars = loadEnvFile();
|
|
441
439
|
log(`Loading environment from: ${envVars.envFile}`, "yellow");
|
|
442
440
|
|
|
@@ -450,7 +448,7 @@ async function launchOpenRouter(
|
|
|
450
448
|
log("Error: OpenRouter auth token is required", "red");
|
|
451
449
|
process.exit(1);
|
|
452
450
|
}
|
|
453
|
-
|
|
451
|
+
updateConfigFile("OPENROUTER_AUTH_TOKEN", authToken, null);
|
|
454
452
|
envVars.OPENROUTER_AUTH_TOKEN = authToken;
|
|
455
453
|
}
|
|
456
454
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-provider-switch",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "Cross-platform Claude Code provider switcher (OpenRouter, Ollama, Anthropic)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/claude-switch.js",
|
|
11
11
|
"dev": "node bin/claude-switch.js",
|
|
12
|
-
"version": "npm version",
|
|
13
12
|
"link": "npm link",
|
|
14
13
|
"unlink": "npm unlink -g claude-code-provider-switch",
|
|
15
14
|
"pub": "npm version patch && npm publish && git push origin main --tags",
|
|
@@ -22,15 +21,16 @@
|
|
|
22
21
|
"test:all": "node test/run-tests.js"
|
|
23
22
|
},
|
|
24
23
|
"keywords": [
|
|
25
|
-
"claude",
|
|
26
24
|
"claude-code",
|
|
27
|
-
"openrouter",
|
|
28
|
-
"llm switcher",
|
|
29
|
-
"llm provider switcher",
|
|
30
|
-
"ai provider switcher",
|
|
31
|
-
"ai model switcher",
|
|
32
|
-
"ai model provider switcher",
|
|
33
25
|
"claude code provider switcher",
|
|
26
|
+
"claude code model switcher",
|
|
27
|
+
"claude code llm switcher",
|
|
28
|
+
"claude code llm provider switcher",
|
|
29
|
+
"ai provider switcher",
|
|
30
|
+
"claude code ai model switcher",
|
|
31
|
+
"claude code ai model provider switcher",
|
|
32
|
+
"claude code OpenRouter",
|
|
33
|
+
"claude code Ollama",
|
|
34
34
|
"cli"
|
|
35
35
|
],
|
|
36
36
|
"author": "Adrian R",
|
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
"files": [
|
|
42
42
|
"bin/",
|
|
43
43
|
"lib/",
|
|
44
|
-
"
|
|
44
|
+
"test/",
|
|
45
|
+
"README.md",
|
|
46
|
+
"index.js"
|
|
45
47
|
],
|
|
46
48
|
"preferGlobal": true
|
|
47
49
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
console.log("🧪 Running complete claude-switch test suite...\n");
|
|
7
|
+
|
|
8
|
+
// Test files in order
|
|
9
|
+
const testFiles = [
|
|
10
|
+
{ name: "Validation & Error Tests", file: "test-validation-errors.js" },
|
|
11
|
+
{ name: "Provider Integration Tests", file: "test-provider-integration.js" },
|
|
12
|
+
{ name: "Comprehensive Integration Tests", file: "test-comprehensive.js" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
let currentTest = 0;
|
|
16
|
+
|
|
17
|
+
function runNextTest() {
|
|
18
|
+
if (currentTest >= testFiles.length) {
|
|
19
|
+
console.log("\n🎉 All test suites passed!");
|
|
20
|
+
console.log("\n📊 Test Coverage Summary:");
|
|
21
|
+
console.log(" ✅ Constants and configuration");
|
|
22
|
+
console.log(" ✅ Error handling and validation");
|
|
23
|
+
console.log(" ✅ Provider-specific functionality");
|
|
24
|
+
console.log(" ✅ Integration and end-to-end tests");
|
|
25
|
+
console.log(" ✅ Cache and performance tests");
|
|
26
|
+
console.log(" ✅ Environment and configuration tests");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const test = testFiles[currentTest];
|
|
32
|
+
console.log(`📋 Running ${test.name}...`);
|
|
33
|
+
|
|
34
|
+
const testProcess = spawn("node", [path.join(__dirname, test.file)], {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
testProcess.on("close", (code) => {
|
|
39
|
+
if (code === 0) {
|
|
40
|
+
console.log(`✅ ${test.name} passed!\n`);
|
|
41
|
+
currentTest++;
|
|
42
|
+
runNextTest();
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`\n❌ ${test.name} failed!`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
testProcess.on("error", (error) => {
|
|
50
|
+
console.log(`\n💥 Failed to run ${test.name}: ${error.message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Start running tests
|
|
56
|
+
runNextTest();
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
|
|
7
|
+
// Test configuration
|
|
8
|
+
const testConfig = {
|
|
9
|
+
timeout: 10000, // 10 seconds timeout for async operations
|
|
10
|
+
scriptPath: path.join(__dirname, "..", "bin", "claude-switch.js"),
|
|
11
|
+
envPath: path.join(__dirname, "..", ".env"),
|
|
12
|
+
testDir: __dirname,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Utility functions
|
|
16
|
+
function runCommand(args, options = {}) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn("node", [testConfig.scriptPath, ...args], {
|
|
19
|
+
stdio: "pipe",
|
|
20
|
+
timeout: testConfig.timeout,
|
|
21
|
+
...options,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
|
|
27
|
+
child.stdout.on("data", (data) => {
|
|
28
|
+
stdout += data.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.stderr.on("data", (data) => {
|
|
32
|
+
stderr += data.toString();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on("close", (code) => {
|
|
36
|
+
resolve({ code, stdout, stderr });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on("error", (error) => {
|
|
40
|
+
reject(error);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runCommandWithInput(args, input, options = {}) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const child = spawn("node", [testConfig.scriptPath, ...args], {
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
49
|
+
timeout: testConfig.timeout,
|
|
50
|
+
...options,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let stdout = "";
|
|
54
|
+
let stderr = "";
|
|
55
|
+
|
|
56
|
+
child.stdout.on("data", (data) => {
|
|
57
|
+
stdout += data.toString();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.stderr.on("data", (data) => {
|
|
61
|
+
stderr += data.toString();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.on("close", (code) => {
|
|
65
|
+
resolve({ code, stdout, stderr });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.on("error", (error) => {
|
|
69
|
+
reject(error);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Send input
|
|
73
|
+
if (input) {
|
|
74
|
+
child.stdin.write(input);
|
|
75
|
+
child.stdin.end();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function test(description, testFn) {
|
|
81
|
+
try {
|
|
82
|
+
console.log(`🧪 ${description}`);
|
|
83
|
+
await testFn();
|
|
84
|
+
console.log(`✅ ${description} - PASSED`);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.log(`❌ ${description} - FAILED: ${error.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Test suite
|
|
92
|
+
async function runTests() {
|
|
93
|
+
console.log("🚀 Starting claude-switch test suite...\n");
|
|
94
|
+
|
|
95
|
+
// Test 1: Basic file existence
|
|
96
|
+
await test("Main script exists", () => {
|
|
97
|
+
if (!fs.existsSync(testConfig.scriptPath)) {
|
|
98
|
+
throw new Error("Main script not found");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await test("Environment file exists", () => {
|
|
103
|
+
if (!fs.existsSync(testConfig.envPath)) {
|
|
104
|
+
throw new Error("Environment file not found");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test("Package.json exists", () => {
|
|
109
|
+
const packagePath = path.join(__dirname, "..", "package.json");
|
|
110
|
+
if (!fs.existsSync(packagePath)) {
|
|
111
|
+
throw new Error("Package.json not found");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Test 2: Help command
|
|
116
|
+
await test("Help command works", async () => {
|
|
117
|
+
const result = await runCommand(["help"]);
|
|
118
|
+
if (result.code !== 0 || !result.stdout.includes("Usage:")) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Help command failed. Code: ${result.code}, Output: ${result.stdout}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Test 3: Version check
|
|
126
|
+
await test("Version information available", async () => {
|
|
127
|
+
const packagePath = path.join(__dirname, "..", "package.json");
|
|
128
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
129
|
+
if (!packageJson.version || !packageJson.version.includes("1.0.0")) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Version check failed. Expected: 1.0.0, Found: ${packageJson.version}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Test 4: Provider listing
|
|
137
|
+
await test("Provider listing works", async () => {
|
|
138
|
+
const result = await runCommand(["help"]);
|
|
139
|
+
if (result.code !== 0 || !result.stdout.includes("Providers:")) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Provider listing failed. Code: ${result.code}, Output: ${result.stdout}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Test 5: Default provider functionality
|
|
147
|
+
await test("Default provider functionality works", async () => {
|
|
148
|
+
const result = await runCommand(["show-defaults"]);
|
|
149
|
+
if (
|
|
150
|
+
result.code !== 0 ||
|
|
151
|
+
(!result.stdout.includes("Default provider:") &&
|
|
152
|
+
!result.stdout.includes("No default provider set"))
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Default provider functionality failed. Code: ${result.code}, Output: ${result.stdout}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Test 6: Model selection with OpenRouter (test with exit)
|
|
161
|
+
await test("OpenRouter model selection interface works", async () => {
|
|
162
|
+
const result = await runCommandWithInput(
|
|
163
|
+
["openrouter", "--model"],
|
|
164
|
+
"exit\n",
|
|
165
|
+
);
|
|
166
|
+
// Exit code 1 is acceptable when using default model after exit
|
|
167
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`OpenRouter model selection failed. Code: ${result.code}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (!result.stdout.includes("Found") || !result.stdout.includes("models")) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`OpenRouter model selection failed. Output: ${result.stdout}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Test 7: Model selection with Ollama (test with exit)
|
|
180
|
+
await test("Ollama model selection interface works", async () => {
|
|
181
|
+
const result = await runCommandWithInput(["ollama", "--model"], "exit\n");
|
|
182
|
+
// Exit code 1 is acceptable when using default model after exit
|
|
183
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
184
|
+
throw new Error(`Ollama model selection failed. Code: ${result.code}`);
|
|
185
|
+
}
|
|
186
|
+
if (!result.stdout.includes("Found") || !result.stdout.includes("models")) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Ollama model selection failed. Output: ${result.stdout}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Test 8: Model selection with Anthropic (test with enter)
|
|
194
|
+
await test("Anthropic model selection interface works", async () => {
|
|
195
|
+
const result = await runCommandWithInput(["anthropic", "--model"], "\n");
|
|
196
|
+
// Accept both successful model selection and API key error as valid
|
|
197
|
+
if (
|
|
198
|
+
!result.stdout.includes("Found") &&
|
|
199
|
+
!result.stdout.includes("models") &&
|
|
200
|
+
!result.stdout.includes("API key not found") &&
|
|
201
|
+
!result.stdout.includes("API key is required")
|
|
202
|
+
) {
|
|
203
|
+
throw new Error(`Anthropic model selection failed. Code: ${result.code}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Test 9: Error handling for invalid provider
|
|
208
|
+
await test("Invalid provider error handling", async () => {
|
|
209
|
+
const result = await runCommand(["invalid-provider"]);
|
|
210
|
+
if (result.code === 0 || !result.stdout.includes("Unknown command")) {
|
|
211
|
+
throw new Error("Invalid provider should fail with error message");
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Test 10: Virtual scrolling indicators present
|
|
216
|
+
await test("Virtual scrolling indicators present", async () => {
|
|
217
|
+
const result = await runCommandWithInput(
|
|
218
|
+
["openrouter", "--model"],
|
|
219
|
+
"exit\n",
|
|
220
|
+
);
|
|
221
|
+
if (!result.stdout.includes("Use ↑/↓ to scroll through results")) {
|
|
222
|
+
throw new Error("Virtual scrolling instructions not present");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Test 11: Default model highlighting
|
|
227
|
+
await test("Default model highlighting works", async () => {
|
|
228
|
+
const result = await runCommandWithInput(
|
|
229
|
+
["openrouter", "--model"],
|
|
230
|
+
"exit\n",
|
|
231
|
+
);
|
|
232
|
+
if (!result.stdout.includes("[DEFAULT]")) {
|
|
233
|
+
throw new Error("Default model not highlighted");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Test 12: Selection options instructions
|
|
238
|
+
await test("Selection options instructions present", async () => {
|
|
239
|
+
const result = await runCommandWithInput(
|
|
240
|
+
["openrouter", "--model"],
|
|
241
|
+
"exit\n",
|
|
242
|
+
);
|
|
243
|
+
const requiredInstructions = [
|
|
244
|
+
"Type number to select",
|
|
245
|
+
"Type model name to search",
|
|
246
|
+
"Press Enter to select the first model",
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const instruction of requiredInstructions) {
|
|
250
|
+
if (!result.stdout.includes(instruction)) {
|
|
251
|
+
throw new Error(`Missing instruction: ${instruction}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Test 13: Environment variable caching
|
|
257
|
+
await test("Environment caching works", async () => {
|
|
258
|
+
// This test checks that the app doesn't crash and loads environment properly
|
|
259
|
+
const result1 = await runCommand(["help"]);
|
|
260
|
+
const result2 = await runCommand(["help"]);
|
|
261
|
+
|
|
262
|
+
if (result1.code !== 0 || result2.code !== 0) {
|
|
263
|
+
throw new Error("Environment caching test failed");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Test 14: Main menu functionality
|
|
268
|
+
await test("Main menu interface works", async () => {
|
|
269
|
+
const result = await runCommandWithInput([], "7\n"); // Select option 7 (should work)
|
|
270
|
+
if (result.code !== 0) {
|
|
271
|
+
throw new Error("Main menu interface failed");
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
console.log("\n🎉 All tests passed! The application is working correctly.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Run tests
|
|
279
|
+
runTests().catch((error) => {
|
|
280
|
+
console.error("💥 Test suite failed:", error.message);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
});
|