2026-02-24 22:48:17 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { createConfigSetupCommand } from '../../src/commands/config-setup.js';
|
|
|
|
|
import type { ConfigSetupDeps, ConfigSetupPrompt } from '../../src/commands/config-setup.js';
|
|
|
|
|
import type { SecretStore } from '@mcpctl/shared';
|
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from 'node:fs';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
|
|
|
|
|
let tempDir: string;
|
|
|
|
|
let logs: string[];
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-config-setup-test-'));
|
|
|
|
|
logs = [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function cleanup() {
|
|
|
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockSecretStore(secrets: Record<string, string> = {}): SecretStore {
|
|
|
|
|
const store: Record<string, string> = { ...secrets };
|
|
|
|
|
return {
|
|
|
|
|
get: vi.fn(async (key: string) => store[key] ?? null),
|
|
|
|
|
set: vi.fn(async (key: string, value: string) => { store[key] = value; }),
|
|
|
|
|
delete: vi.fn(async () => true),
|
|
|
|
|
backend: () => 'mock',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mockPrompt(answers: unknown[]): ConfigSetupPrompt {
|
|
|
|
|
let callIndex = 0;
|
|
|
|
|
return {
|
|
|
|
|
select: vi.fn(async () => answers[callIndex++]),
|
|
|
|
|
input: vi.fn(async () => answers[callIndex++] as string),
|
|
|
|
|
password: vi.fn(async () => answers[callIndex++] as string),
|
|
|
|
|
confirm: vi.fn(async () => answers[callIndex++] as boolean),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDeps(overrides: {
|
|
|
|
|
secrets?: Record<string, string>;
|
|
|
|
|
answers?: unknown[];
|
|
|
|
|
fetchModels?: ConfigSetupDeps['fetchModels'];
|
2026-02-24 23:24:31 +00:00
|
|
|
whichBinary?: ConfigSetupDeps['whichBinary'];
|
2026-02-24 22:48:17 +00:00
|
|
|
} = {}): ConfigSetupDeps {
|
|
|
|
|
return {
|
|
|
|
|
configDeps: { configDir: tempDir },
|
|
|
|
|
secretStore: mockSecretStore(overrides.secrets),
|
|
|
|
|
log: (...args: string[]) => logs.push(args.join(' ')),
|
|
|
|
|
prompt: mockPrompt(overrides.answers ?? []),
|
|
|
|
|
fetchModels: overrides.fetchModels ?? vi.fn(async () => []),
|
2026-02-24 23:24:31 +00:00
|
|
|
whichBinary: overrides.whichBinary ?? vi.fn(async () => '/usr/bin/gemini'),
|
2026-02-24 22:48:17 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readConfig(): Record<string, unknown> {
|
|
|
|
|
const raw = readFileSync(join(tempDir, 'config.json'), 'utf-8');
|
|
|
|
|
return JSON.parse(raw) as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runSetup(deps: ConfigSetupDeps): Promise<void> {
|
|
|
|
|
const cmd = createConfigSetupCommand(deps);
|
|
|
|
|
await cmd.parseAsync([], { from: 'user' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('config setup wizard', () => {
|
|
|
|
|
describe('provider: none', () => {
|
|
|
|
|
it('disables LLM and saves config', async () => {
|
2026-02-25 02:16:08 +00:00
|
|
|
const deps = buildDeps({ answers: ['simple', 'none'] });
|
2026-02-24 22:48:17 +00:00
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
expect(config.llm).toEqual({ provider: 'none' });
|
|
|
|
|
expect(logs.some((l) => l.includes('LLM disabled'))).toBe(true);
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: gemini-cli', () => {
|
2026-02-24 23:24:31 +00:00
|
|
|
it('auto-detects binary path and saves config', async () => {
|
|
|
|
|
// Answers: select provider, select model (no binary prompt — auto-detected)
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'gemini-cli', 'gemini-2.5-flash'],
|
2026-02-24 23:24:31 +00:00
|
|
|
whichBinary: vi.fn(async () => '/home/user/.npm-global/bin/gemini'),
|
|
|
|
|
});
|
2026-02-24 22:48:17 +00:00
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
2026-02-24 23:24:31 +00:00
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('gemini-cli');
|
|
|
|
|
expect(llm.model).toBe('gemini-2.5-flash');
|
|
|
|
|
expect(llm.binaryPath).toBe('/home/user/.npm-global/bin/gemini');
|
|
|
|
|
expect(logs.some((l) => l.includes('Found gemini at'))).toBe(true);
|
2026-02-24 22:48:17 +00:00
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 23:24:31 +00:00
|
|
|
it('prompts for manual path when binary not found', async () => {
|
|
|
|
|
// Answers: select provider, select model, enter manual path
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'gemini-cli', 'gemini-2.5-flash', '/opt/gemini'],
|
2026-02-24 23:24:31 +00:00
|
|
|
whichBinary: vi.fn(async () => null),
|
|
|
|
|
});
|
2026-02-24 22:48:17 +00:00
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.binaryPath).toBe('/opt/gemini');
|
2026-02-24 23:24:31 +00:00
|
|
|
expect(logs.some((l) => l.includes('not found'))).toBe(true);
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('saves gemini-cli with custom model', async () => {
|
|
|
|
|
// Answers: select provider, select custom, enter model name
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'gemini-cli', '__custom__', 'gemini-3.0-flash'],
|
2026-02-24 23:24:31 +00:00
|
|
|
whichBinary: vi.fn(async () => '/usr/bin/gemini'),
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.model).toBe('gemini-3.0-flash');
|
2026-02-24 22:48:17 +00:00
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: ollama', () => {
|
|
|
|
|
it('fetches models and allows selection', async () => {
|
|
|
|
|
const fetchModels = vi.fn(async () => ['llama3.2', 'codellama', 'mistral']);
|
|
|
|
|
// Answers: select provider, enter URL, select model
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'ollama', 'http://localhost:11434', 'codellama'],
|
2026-02-24 22:48:17 +00:00
|
|
|
fetchModels,
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith('http://localhost:11434', '/api/tags');
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('ollama');
|
|
|
|
|
expect(llm.model).toBe('codellama');
|
|
|
|
|
expect(llm.url).toBe('http://localhost:11434');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to manual input when fetch fails', async () => {
|
|
|
|
|
const fetchModels = vi.fn(async () => []);
|
|
|
|
|
// Answers: select provider, enter URL, enter model manually
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'ollama', 'http://localhost:11434', 'llama3.2'],
|
2026-02-24 22:48:17 +00:00
|
|
|
fetchModels,
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
expect((config.llm as Record<string, unknown>).model).toBe('llama3.2');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: anthropic', () => {
|
|
|
|
|
it('prompts for API key and saves to secret store', async () => {
|
2026-03-03 19:07:39 +00:00
|
|
|
// Flow: simple → anthropic → (no existing key) → whichBinary('claude') returns null →
|
|
|
|
|
// log tip → password prompt → select model
|
2026-02-24 22:48:17 +00:00
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'anthropic', 'sk-ant-new-key', 'claude-haiku-3-5-20241022'],
|
2026-03-03 19:07:39 +00:00
|
|
|
whichBinary: vi.fn(async () => null),
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-new-key');
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('anthropic');
|
|
|
|
|
expect(llm.model).toBe('claude-haiku-3-5-20241022');
|
|
|
|
|
// API key should NOT be in config file
|
|
|
|
|
expect(llm).not.toHaveProperty('apiKey');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows existing key masked and allows keeping it', async () => {
|
|
|
|
|
// Answers: select provider, confirm change=false, select model
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
secrets: { 'anthropic-api-key': 'sk-ant-existing-key-1234' },
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'anthropic', false, 'claude-sonnet-4-20250514'],
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
// Should NOT have called set (kept existing key)
|
|
|
|
|
expect(deps.secretStore.set).not.toHaveBeenCalled();
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
expect((config.llm as Record<string, unknown>).model).toBe('claude-sonnet-4-20250514');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows replacing existing key', async () => {
|
|
|
|
|
// Answers: select provider, confirm change=true, enter new key, select model
|
2026-03-03 19:07:39 +00:00
|
|
|
// Change=true → promptForAnthropicKey → whichBinary returns null → password prompt
|
2026-02-24 22:48:17 +00:00
|
|
|
const deps = buildDeps({
|
|
|
|
|
secrets: { 'anthropic-api-key': 'sk-ant-old' },
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'anthropic', true, 'sk-ant-new', 'claude-haiku-3-5-20241022'],
|
2026-03-03 19:07:39 +00:00
|
|
|
whichBinary: vi.fn(async () => null),
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-new');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
2026-03-03 19:07:39 +00:00
|
|
|
|
|
|
|
|
it('detects claude binary and prompts for OAuth token', async () => {
|
|
|
|
|
// Flow: simple → anthropic → (no existing key) → whichBinary finds claude →
|
|
|
|
|
// confirm OAuth=true → password prompt → select model
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
answers: ['simple', 'anthropic', true, 'sk-ant-oat01-test-token', 'claude-haiku-3-5-20241022'],
|
|
|
|
|
whichBinary: vi.fn(async () => '/usr/bin/claude'),
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-oat01-test-token');
|
|
|
|
|
expect(logs.some((l) => l.includes('Found Claude CLI at'))).toBe(true);
|
|
|
|
|
expect(logs.some((l) => l.includes('claude setup-token'))).toBe(true);
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('anthropic');
|
|
|
|
|
expect(llm.model).toBe('claude-haiku-3-5-20241022');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to API key when claude binary not found', async () => {
|
|
|
|
|
// Flow: simple → anthropic → (no existing key) → whichBinary returns null →
|
|
|
|
|
// password prompt (API key) → select model
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
answers: ['simple', 'anthropic', 'sk-ant-api03-test', 'claude-sonnet-4-20250514'],
|
|
|
|
|
whichBinary: vi.fn(async () => null),
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-api03-test');
|
|
|
|
|
expect(logs.some((l) => l.includes('Tip: Install Claude CLI'))).toBe(true);
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.model).toBe('claude-sonnet-4-20250514');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows OAuth label when existing token is OAuth', async () => {
|
|
|
|
|
// Flow: simple → anthropic → existing OAuth key → confirm change=false → select model
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
secrets: { 'anthropic-api-key': 'sk-ant-oat01-existing-token' },
|
|
|
|
|
answers: ['simple', 'anthropic', false, 'claude-haiku-3-5-20241022'],
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
// Should NOT have called set (kept existing key)
|
|
|
|
|
expect(deps.secretStore.set).not.toHaveBeenCalled();
|
|
|
|
|
// Confirm prompt should have received an OAuth label
|
|
|
|
|
expect(deps.prompt.confirm).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('OAuth token stored'),
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('declines OAuth and enters API key instead', async () => {
|
|
|
|
|
// Flow: simple → anthropic → (no existing key) → whichBinary finds claude →
|
|
|
|
|
// confirm OAuth=false → password prompt (API key) → select model
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
answers: ['simple', 'anthropic', false, 'sk-ant-api03-manual', 'claude-sonnet-4-20250514'],
|
|
|
|
|
whichBinary: vi.fn(async () => '/usr/bin/claude'),
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('anthropic-api-key', 'sk-ant-api03-manual');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: vllm', () => {
|
|
|
|
|
it('fetches models from vLLM and allows selection', async () => {
|
|
|
|
|
const fetchModels = vi.fn(async () => ['my-model', 'llama-70b']);
|
|
|
|
|
// Answers: select provider, enter URL, select model
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'vllm', 'http://gpu:8000', 'llama-70b'],
|
2026-02-24 22:48:17 +00:00
|
|
|
fetchModels,
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith('http://gpu:8000', '/v1/models');
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('vllm');
|
|
|
|
|
expect(llm.url).toBe('http://gpu:8000');
|
|
|
|
|
expect(llm.model).toBe('llama-70b');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: openai', () => {
|
|
|
|
|
it('prompts for key, model, and optional custom endpoint', async () => {
|
|
|
|
|
// Answers: select provider, enter key, enter model, confirm custom URL=true, enter URL
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'openai', 'sk-openai-key', 'gpt-4o', true, 'https://custom.api.com'],
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('openai-api-key', 'sk-openai-key');
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('openai');
|
|
|
|
|
expect(llm.model).toBe('gpt-4o');
|
|
|
|
|
expect(llm.url).toBe('https://custom.api.com');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips custom URL when not requested', async () => {
|
|
|
|
|
// Answers: select provider, enter key, enter model, confirm custom URL=false
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'openai', 'sk-openai-key', 'gpt-4o-mini', false],
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.url).toBeUndefined();
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('provider: deepseek', () => {
|
|
|
|
|
it('prompts for key and model', async () => {
|
|
|
|
|
// Answers: select provider, enter key, select model
|
|
|
|
|
const deps = buildDeps({
|
2026-02-25 02:16:08 +00:00
|
|
|
answers: ['simple', 'deepseek', 'sk-ds-key', 'deepseek-chat'],
|
2026-02-24 22:48:17 +00:00
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(deps.secretStore.set).toHaveBeenCalledWith('deepseek-api-key', 'sk-ds-key');
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as Record<string, unknown>;
|
|
|
|
|
expect(llm.provider).toBe('deepseek');
|
|
|
|
|
expect(llm.model).toBe('deepseek-chat');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-03 19:07:39 +00:00
|
|
|
describe('advanced mode: duplicate names', () => {
|
|
|
|
|
it('generates unique default name when same provider added to both tiers', async () => {
|
|
|
|
|
// Flow: advanced →
|
|
|
|
|
// add fast? yes → anthropic → name "anthropic" (default) → whichBinary null → key → model → add more? no →
|
|
|
|
|
// add heavy? yes → anthropic → name "anthropic-2" (deduped default) → existing key, keep → model → add more? no
|
|
|
|
|
const deps = buildDeps({
|
|
|
|
|
answers: [
|
|
|
|
|
'advanced',
|
|
|
|
|
// fast tier
|
|
|
|
|
true, // add fast?
|
|
|
|
|
'anthropic', // fast provider type
|
|
|
|
|
'anthropic', // provider name (default)
|
|
|
|
|
'sk-ant-oat01-token', // API key (whichBinary returns null → password prompt)
|
|
|
|
|
'claude-haiku-3-5-20241022', // model
|
|
|
|
|
false, // add another fast?
|
|
|
|
|
// heavy tier
|
|
|
|
|
true, // add heavy?
|
|
|
|
|
'anthropic', // heavy provider type
|
|
|
|
|
'anthropic-2', // provider name (deduped default)
|
|
|
|
|
false, // keep existing key
|
|
|
|
|
'claude-opus-4-20250514', // model
|
|
|
|
|
false, // add another heavy?
|
|
|
|
|
],
|
|
|
|
|
whichBinary: vi.fn(async () => null),
|
|
|
|
|
});
|
|
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
const config = readConfig();
|
|
|
|
|
const llm = config.llm as { providers: Array<{ name: string; type: string; model: string; tier: string }> };
|
|
|
|
|
expect(llm.providers).toHaveLength(2);
|
|
|
|
|
expect(llm.providers[0].name).toBe('anthropic');
|
|
|
|
|
expect(llm.providers[0].tier).toBe('fast');
|
|
|
|
|
expect(llm.providers[1].name).toBe('anthropic-2');
|
|
|
|
|
expect(llm.providers[1].tier).toBe('heavy');
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-24 22:48:17 +00:00
|
|
|
describe('output messages', () => {
|
|
|
|
|
it('shows restart instruction', async () => {
|
2026-02-25 02:16:08 +00:00
|
|
|
const deps = buildDeps({ answers: ['simple', 'gemini-cli', 'gemini-2.5-flash'] });
|
2026-02-24 22:48:17 +00:00
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(logs.some((l) => l.includes('systemctl --user restart mcplocal'))).toBe(true);
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows configured provider and model', async () => {
|
2026-02-25 02:16:08 +00:00
|
|
|
const deps = buildDeps({ answers: ['simple', 'gemini-cli', 'gemini-2.5-flash'] });
|
2026-02-24 22:48:17 +00:00
|
|
|
await runSetup(deps);
|
|
|
|
|
|
|
|
|
|
expect(logs.some((l) => l.includes('gemini-cli') && l.includes('gemini-2.5-flash'))).toBe(true);
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|