Apply reads YAML/JSON config files to sync servers, profiles, and projects to the daemon with create-or-update semantics. Setup provides an interactive wizard for configuring MCP servers with environment variables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { createSetupCommand } from '../../src/commands/setup.js';
|
|
import type { ApiClient } from '../../src/api-client.js';
|
|
import type { SetupPromptDeps } from '../../src/commands/setup.js';
|
|
|
|
function mockClient(): ApiClient {
|
|
return {
|
|
get: vi.fn(async () => []),
|
|
post: vi.fn(async () => ({ id: 'new-id', name: 'test' })),
|
|
put: vi.fn(async () => ({})),
|
|
delete: vi.fn(async () => {}),
|
|
} as unknown as ApiClient;
|
|
}
|
|
|
|
function mockPrompt(answers: Record<string, string | boolean>): SetupPromptDeps {
|
|
const answersQueue = { ...answers };
|
|
return {
|
|
input: vi.fn(async (message: string) => {
|
|
for (const [key, val] of Object.entries(answersQueue)) {
|
|
if (message.toLowerCase().includes(key.toLowerCase()) && typeof val === 'string') {
|
|
delete answersQueue[key];
|
|
return val;
|
|
}
|
|
}
|
|
return '';
|
|
}),
|
|
password: vi.fn(async () => 'secret-value'),
|
|
select: vi.fn(async () => 'STDIO') as SetupPromptDeps['select'],
|
|
confirm: vi.fn(async (message: string) => {
|
|
if (message.includes('profile')) return true;
|
|
if (message.includes('secret')) return false;
|
|
if (message.includes('another')) return false;
|
|
return false;
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe('setup command', () => {
|
|
let client: ReturnType<typeof mockClient>;
|
|
let output: string[];
|
|
const log = (...args: unknown[]) => output.push(args.map(String).join(' '));
|
|
|
|
beforeEach(() => {
|
|
client = mockClient();
|
|
output = [];
|
|
});
|
|
|
|
it('creates server with prompted values', async () => {
|
|
const prompt = mockPrompt({
|
|
'transport': 'STDIO',
|
|
'npm package': '@anthropic/slack-mcp',
|
|
'docker image': '',
|
|
'description': 'Slack server',
|
|
'profile name': 'default',
|
|
'environment variable name': '',
|
|
});
|
|
|
|
const cmd = createSetupCommand({ client, prompt, log });
|
|
await cmd.parseAsync(['slack'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({
|
|
name: 'slack',
|
|
transport: 'STDIO',
|
|
}));
|
|
expect(output.join('\n')).toContain("Server 'test' created");
|
|
});
|
|
|
|
it('creates profile with env vars', async () => {
|
|
vi.mocked(client.post)
|
|
.mockResolvedValueOnce({ id: 'srv-1', name: 'slack' }) // server create
|
|
.mockResolvedValueOnce({ id: 'prof-1', name: 'default' }); // profile create
|
|
|
|
const prompt = mockPrompt({
|
|
'transport': 'STDIO',
|
|
'npm package': '',
|
|
'docker image': '',
|
|
'description': '',
|
|
'profile name': 'default',
|
|
});
|
|
// Override confirm to create profile and add one env var
|
|
let confirmCallCount = 0;
|
|
vi.mocked(prompt.confirm).mockImplementation(async (msg: string) => {
|
|
confirmCallCount++;
|
|
if (msg.includes('profile')) return true;
|
|
if (msg.includes('secret')) return true;
|
|
if (msg.includes('another')) return false;
|
|
return false;
|
|
});
|
|
// Override input to provide env var name then empty to stop
|
|
let inputCallCount = 0;
|
|
vi.mocked(prompt.input).mockImplementation(async (msg: string) => {
|
|
inputCallCount++;
|
|
if (msg.includes('Profile name')) return 'default';
|
|
if (msg.includes('variable name') && inputCallCount <= 8) return 'API_KEY';
|
|
if (msg.includes('variable name')) return '';
|
|
return '';
|
|
});
|
|
|
|
const cmd = createSetupCommand({ client, prompt, log });
|
|
await cmd.parseAsync(['slack'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledTimes(2);
|
|
const profileCall = vi.mocked(client.post).mock.calls[1];
|
|
expect(profileCall?.[0]).toBe('/api/v1/profiles');
|
|
expect(profileCall?.[1]).toEqual(expect.objectContaining({
|
|
name: 'default',
|
|
serverId: 'srv-1',
|
|
}));
|
|
});
|
|
|
|
it('exits if server creation fails', async () => {
|
|
vi.mocked(client.post).mockRejectedValue(new Error('conflict'));
|
|
|
|
const prompt = mockPrompt({
|
|
'npm package': '',
|
|
'docker image': '',
|
|
'description': '',
|
|
});
|
|
|
|
const cmd = createSetupCommand({ client, prompt, log });
|
|
await cmd.parseAsync(['slack'], { from: 'user' });
|
|
|
|
expect(output.join('\n')).toContain('Failed to create server');
|
|
expect(client.post).toHaveBeenCalledTimes(1); // Only server create, no profile
|
|
});
|
|
|
|
it('skips profile creation when declined', async () => {
|
|
const prompt = mockPrompt({
|
|
'npm package': '',
|
|
'docker image': '',
|
|
'description': '',
|
|
});
|
|
vi.mocked(prompt.confirm).mockResolvedValue(false);
|
|
|
|
const cmd = createSetupCommand({ client, prompt, log });
|
|
await cmd.parseAsync(['test-server'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledTimes(1); // Only server create
|
|
expect(output.join('\n')).toContain('Setup complete');
|
|
});
|
|
});
|