Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).
- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { createGetCommand } from '../../src/commands/get.js';
|
|
import type { GetCommandDeps } from '../../src/commands/get.js';
|
|
|
|
function makeDeps(items: unknown[] = []): GetCommandDeps & { output: string[] } {
|
|
const output: string[] = [];
|
|
return {
|
|
output,
|
|
fetchResource: vi.fn(async () => items),
|
|
log: (...args: string[]) => output.push(args.join(' ')),
|
|
};
|
|
}
|
|
|
|
describe('get command', () => {
|
|
it('lists servers in table format', async () => {
|
|
const deps = makeDeps([
|
|
{ id: 'srv-1', name: 'slack', transport: 'STDIO', packageName: '@slack/mcp', dockerImage: null },
|
|
{ id: 'srv-2', name: 'github', transport: 'SSE', packageName: null, dockerImage: 'ghcr.io/github-mcp' },
|
|
]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'servers']);
|
|
|
|
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
|
|
expect(deps.output[0]).toContain('NAME');
|
|
expect(deps.output[0]).toContain('TRANSPORT');
|
|
expect(deps.output.join('\n')).toContain('slack');
|
|
expect(deps.output.join('\n')).toContain('github');
|
|
});
|
|
|
|
it('resolves resource aliases', async () => {
|
|
const deps = makeDeps([]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'srv']);
|
|
expect(deps.fetchResource).toHaveBeenCalledWith('servers', undefined);
|
|
});
|
|
|
|
it('passes ID when provided', async () => {
|
|
const deps = makeDeps([{ id: 'srv-1', name: 'slack' }]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'servers', 'srv-1']);
|
|
expect(deps.fetchResource).toHaveBeenCalledWith('servers', 'srv-1');
|
|
});
|
|
|
|
it('outputs apply-compatible JSON format', async () => {
|
|
const deps = makeDeps([{ id: 'srv-1', name: 'slack', createdAt: '2025-01-01', updatedAt: '2025-01-01', version: 1 }]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'json']);
|
|
|
|
const parsed = JSON.parse(deps.output[0] ?? '');
|
|
// Wrapped in resource key, internal fields stripped
|
|
expect(parsed).toHaveProperty('servers');
|
|
expect(parsed.servers[0].name).toBe('slack');
|
|
expect(parsed.servers[0]).not.toHaveProperty('id');
|
|
expect(parsed.servers[0]).not.toHaveProperty('createdAt');
|
|
expect(parsed.servers[0]).not.toHaveProperty('updatedAt');
|
|
expect(parsed.servers[0]).not.toHaveProperty('version');
|
|
});
|
|
|
|
it('outputs apply-compatible YAML format', async () => {
|
|
const deps = makeDeps([{ id: 'srv-1', name: 'slack', createdAt: '2025-01-01' }]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'servers', '-o', 'yaml']);
|
|
const text = deps.output[0];
|
|
expect(text).toContain('servers:');
|
|
expect(text).toContain('name: slack');
|
|
expect(text).not.toContain('id:');
|
|
expect(text).not.toContain('createdAt:');
|
|
});
|
|
|
|
it('lists instances with correct columns', async () => {
|
|
const deps = makeDeps([
|
|
{ id: 'inst-1', serverId: 'srv-1', status: 'RUNNING', containerId: 'abc123def456', port: 3000 },
|
|
]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'instances']);
|
|
expect(deps.output[0]).toContain('STATUS');
|
|
expect(deps.output.join('\n')).toContain('RUNNING');
|
|
});
|
|
|
|
it('shows no results message for empty list', async () => {
|
|
const deps = makeDeps([]);
|
|
const cmd = createGetCommand(deps);
|
|
await cmd.parseAsync(['node', 'test', 'servers']);
|
|
expect(deps.output[0]).toContain('No servers found');
|
|
});
|
|
});
|