Files
mcpctl/src/cli/tests/commands/edit.test.ts

154 lines
4.7 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { readFileSync, writeFileSync } from 'node:fs';
import yaml from 'js-yaml';
import { createEditCommand } from '../../src/commands/edit.js';
import type { ApiClient } from '../../src/api-client.js';
function mockClient(): ApiClient {
return {
get: vi.fn(async () => ({})),
post: vi.fn(async () => ({})),
put: vi.fn(async () => ({})),
delete: vi.fn(async () => {}),
} as unknown as ApiClient;
}
describe('edit command', () => {
let client: ReturnType<typeof mockClient>;
let output: string[];
const log = (...args: unknown[]) => output.push(args.map(String).join(' '));
beforeEach(() => {
client = mockClient();
output = [];
});
it('fetches server, opens editor, applies changes on save', async () => {
// GET /api/v1/servers returns list for resolveNameOrId
vi.mocked(client.get).mockImplementation(async (path: string) => {
if (path === '/api/v1/servers') {
return [{ id: 'srv-1', name: 'ha-mcp' }];
}
// GET /api/v1/servers/srv-1 returns full server
return {
id: 'srv-1',
name: 'ha-mcp',
description: 'Old desc',
transport: 'STDIO',
replicas: 1,
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
version: 1,
};
});
const cmd = createEditCommand({
client,
log,
getEditor: () => 'vi',
openEditor: (filePath) => {
// Simulate user editing the file
const content = readFileSync(filePath, 'utf-8');
const modified = content
.replace('Old desc', 'New desc')
.replace('replicas: 1', 'replicas: 3');
writeFileSync(filePath, modified, 'utf-8');
},
});
await cmd.parseAsync(['server', 'ha-mcp'], { from: 'user' });
expect(client.put).toHaveBeenCalledWith('/api/v1/servers/srv-1', expect.objectContaining({
description: 'New desc',
replicas: 3,
}));
expect(output.join('\n')).toContain("server 'ha-mcp' updated");
});
it('detects no changes and skips PUT', async () => {
vi.mocked(client.get).mockImplementation(async (path: string) => {
if (path === '/api/v1/servers') return [{ id: 'srv-1', name: 'test' }];
return {
id: 'srv-1', name: 'test', description: '', transport: 'STDIO',
createdAt: '2025-01-01', updatedAt: '2025-01-01', version: 1,
};
});
const cmd = createEditCommand({
client,
log,
getEditor: () => 'vi',
openEditor: () => {
// Don't modify the file
},
});
await cmd.parseAsync(['server', 'test'], { from: 'user' });
expect(client.put).not.toHaveBeenCalled();
expect(output.join('\n')).toContain("unchanged");
});
it('handles empty file as cancel', async () => {
vi.mocked(client.get).mockImplementation(async (path: string) => {
if (path === '/api/v1/servers') return [{ id: 'srv-1', name: 'test' }];
return { id: 'srv-1', name: 'test', createdAt: '2025-01-01', updatedAt: '2025-01-01', version: 1 };
});
const cmd = createEditCommand({
client,
log,
getEditor: () => 'vi',
openEditor: (filePath) => {
writeFileSync(filePath, '', 'utf-8');
},
});
await cmd.parseAsync(['server', 'test'], { from: 'user' });
expect(client.put).not.toHaveBeenCalled();
expect(output.join('\n')).toContain('cancelled');
});
it('strips read-only fields from editor content', async () => {
vi.mocked(client.get).mockImplementation(async (path: string) => {
if (path === '/api/v1/servers') return [{ id: 'srv-1', name: 'test' }];
return {
id: 'srv-1', name: 'test', description: '', transport: 'STDIO',
createdAt: '2025-01-01', updatedAt: '2025-01-01', version: 1,
};
});
let editorContent = '';
const cmd = createEditCommand({
client,
log,
getEditor: () => 'vi',
openEditor: (filePath) => {
editorContent = readFileSync(filePath, 'utf-8');
},
});
await cmd.parseAsync(['server', 'test'], { from: 'user' });
// The editor content should NOT contain read-only fields
expect(editorContent).not.toContain('id:');
expect(editorContent).not.toContain('createdAt');
expect(editorContent).not.toContain('updatedAt');
expect(editorContent).not.toContain('version');
// But should contain editable fields
expect(editorContent).toContain('name:');
});
it('rejects edit instance with error message', async () => {
const cmd = createEditCommand({ client, log });
await cmd.parseAsync(['instance', 'inst-1'], { from: 'user' });
expect(client.get).not.toHaveBeenCalled();
expect(client.put).not.toHaveBeenCalled();
expect(output.join('\n')).toContain('immutable');
});
});