feat: add create/edit commands, apply-compatible output, better describe
- `create server/profile/project` with all CLI flags (kubectl parity) - `edit server/profile/project` opens $EDITOR for in-flight editing - `get -o yaml/json` now outputs apply-compatible format (strips internal fields, wraps in resource key) - `describe` shows visually clean sectioned output with aligned columns - Extract shared utilities (resolveResource, resolveNameOrId, stripInternalFields) - Instances are immutable (no create/edit, like pods) - Full test coverage for create, edit, and updated describe/get Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
144
src/cli/tests/commands/create.test.ts
Normal file
144
src/cli/tests/commands/create.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createCreateCommand } from '../../src/commands/create.js';
|
||||
import type { ApiClient } from '../../src/api-client.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;
|
||||
}
|
||||
|
||||
describe('create command', () => {
|
||||
let client: ReturnType<typeof mockClient>;
|
||||
let output: string[];
|
||||
const log = (...args: unknown[]) => output.push(args.map(String).join(' '));
|
||||
|
||||
beforeEach(() => {
|
||||
client = mockClient();
|
||||
output = [];
|
||||
});
|
||||
|
||||
describe('create server', () => {
|
||||
it('creates a server with minimal flags', async () => {
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync(['server', 'my-server'], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({
|
||||
name: 'my-server',
|
||||
transport: 'STDIO',
|
||||
replicas: 1,
|
||||
}));
|
||||
expect(output.join('\n')).toContain("server 'test' created");
|
||||
});
|
||||
|
||||
it('creates a server with all flags', async () => {
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync([
|
||||
'server', 'ha-mcp',
|
||||
'-d', 'Home Assistant MCP',
|
||||
'--docker-image', 'ghcr.io/ha-mcp:latest',
|
||||
'--transport', 'STREAMABLE_HTTP',
|
||||
'--external-url', 'http://localhost:8086/mcp',
|
||||
'--container-port', '3000',
|
||||
'--replicas', '2',
|
||||
'--command', 'python',
|
||||
'--command', '-c',
|
||||
'--command', 'print("hello")',
|
||||
'--env-template', 'API_KEY:API key:true',
|
||||
'--env-template', 'BASE_URL:Base URL:false',
|
||||
], { from: 'user' });
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/servers', {
|
||||
name: 'ha-mcp',
|
||||
description: 'Home Assistant MCP',
|
||||
dockerImage: 'ghcr.io/ha-mcp:latest',
|
||||
transport: 'STREAMABLE_HTTP',
|
||||
externalUrl: 'http://localhost:8086/mcp',
|
||||
containerPort: 3000,
|
||||
replicas: 2,
|
||||
command: ['python', '-c', 'print("hello")'],
|
||||
envTemplate: [
|
||||
{ name: 'API_KEY', description: 'API key', isSecret: true },
|
||||
{ name: 'BASE_URL', description: 'Base URL', isSecret: false },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults transport to STDIO', async () => {
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync(['server', 'test'], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/servers', expect.objectContaining({
|
||||
transport: 'STDIO',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create profile', () => {
|
||||
it('creates a profile resolving server name', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue([
|
||||
{ id: 'srv-abc', name: 'ha-mcp' },
|
||||
]);
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync(['profile', 'production', '--server', 'ha-mcp'], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/profiles', expect.objectContaining({
|
||||
name: 'production',
|
||||
serverId: 'srv-abc',
|
||||
}));
|
||||
});
|
||||
|
||||
it('parses --env KEY=value entries', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue([
|
||||
{ id: 'srv-1', name: 'test' },
|
||||
]);
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync([
|
||||
'profile', 'dev',
|
||||
'--server', 'test',
|
||||
'--env', 'FOO=bar',
|
||||
'--env', 'SECRET=s3cr3t',
|
||||
], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/profiles', expect.objectContaining({
|
||||
envOverrides: { FOO: 'bar', SECRET: 's3cr3t' },
|
||||
}));
|
||||
});
|
||||
|
||||
it('passes permissions', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue([
|
||||
{ id: 'srv-1', name: 'test' },
|
||||
]);
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync([
|
||||
'profile', 'admin',
|
||||
'--server', 'test',
|
||||
'--permissions', 'read',
|
||||
'--permissions', 'write',
|
||||
], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/profiles', expect.objectContaining({
|
||||
permissions: ['read', 'write'],
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('create project', () => {
|
||||
it('creates a project', async () => {
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync(['project', 'my-project', '-d', 'A test project'], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
|
||||
name: 'my-project',
|
||||
description: 'A test project',
|
||||
});
|
||||
expect(output.join('\n')).toContain("project 'test' created");
|
||||
});
|
||||
|
||||
it('creates a project with no description', async () => {
|
||||
const cmd = createCreateCommand({ client, log });
|
||||
await cmd.parseAsync(['project', 'minimal'], { from: 'user' });
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
|
||||
name: 'minimal',
|
||||
description: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user