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, 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, 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', undefined); }); it('outputs apply-compatible JSON format (multi-doc)', 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] ?? ''); // Array of documents with kind field, internal fields stripped expect(Array.isArray(parsed)).toBe(true); expect(parsed[0].kind).toBe('server'); expect(parsed[0].name).toBe('slack'); expect(parsed[0]).not.toHaveProperty('id'); expect(parsed[0]).not.toHaveProperty('createdAt'); expect(parsed[0]).not.toHaveProperty('updatedAt'); expect(parsed[0]).not.toHaveProperty('version'); }); it('outputs apply-compatible YAML format (multi-doc)', 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('kind: server'); 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', server: { name: 'my-grafana' }, status: 'RUNNING', containerId: 'abc123def456', port: 3000 }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'instances']); expect(deps.output[0]).toContain('NAME'); expect(deps.output[0]).toContain('STATUS'); expect(deps.output.join('\n')).toContain('my-grafana'); 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'); }); it('lists users with correct columns (no ROLE column)', async () => { const deps = makeDeps([ { id: 'usr-1', email: 'alice@test.com', name: 'Alice', provider: null }, { id: 'usr-2', email: 'bob@test.com', name: null, provider: 'oidc' }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'users']); expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined, undefined); const text = deps.output.join('\n'); expect(text).toContain('EMAIL'); expect(text).toContain('NAME'); expect(text).not.toContain('ROLE'); expect(text).toContain('PROVIDER'); expect(text).toContain('alice@test.com'); expect(text).toContain('Alice'); expect(text).toContain('bob@test.com'); expect(text).toContain('oidc'); }); it('resolves user alias', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'user']); expect(deps.fetchResource).toHaveBeenCalledWith('users', undefined, undefined); }); it('lists groups with correct columns', async () => { const deps = makeDeps([ { id: 'grp-1', name: 'dev-team', description: 'Developers', members: [{ user: { email: 'alice@test.com' } }, { user: { email: 'bob@test.com' } }], }, { id: 'grp-2', name: 'ops-team', description: 'Operations', members: [] }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'groups']); expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined, undefined); const text = deps.output.join('\n'); expect(text).toContain('NAME'); expect(text).toContain('MEMBERS'); expect(text).toContain('DESCRIPTION'); expect(text).toContain('dev-team'); expect(text).toContain('2'); expect(text).toContain('ops-team'); expect(text).toContain('0'); }); it('resolves group alias', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'group']); expect(deps.fetchResource).toHaveBeenCalledWith('groups', undefined, undefined); }); it('lists rbac definitions with correct columns', async () => { const deps = makeDeps([ { id: 'rbac-1', name: 'admins', subjects: [{ kind: 'User', name: 'admin@test.com' }], roleBindings: [{ role: 'edit', resource: '*' }], }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'rbac']); expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined, undefined); const text = deps.output.join('\n'); expect(text).toContain('NAME'); expect(text).toContain('SUBJECTS'); expect(text).toContain('BINDINGS'); expect(text).toContain('admins'); expect(text).toContain('User:admin@test.com'); expect(text).toContain('edit:*'); }); it('resolves rbac-definition alias', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'rbac-definition']); expect(deps.fetchResource).toHaveBeenCalledWith('rbac', undefined, undefined); }); it('lists projects with new columns', async () => { const deps = makeDeps([{ id: 'proj-1', name: 'smart-home', description: 'Home automation', ownerId: 'usr-1', servers: [{ server: { name: 'grafana' } }], }]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'projects']); const text = deps.output.join('\n'); expect(text).toContain('SERVERS'); expect(text).toContain('smart-home'); expect(text).toContain('1'); }); it('displays mixed resource and operation bindings', async () => { const deps = makeDeps([ { id: 'rbac-1', name: 'admin-access', subjects: [{ kind: 'Group', name: 'admin' }], roleBindings: [ { role: 'edit', resource: '*' }, { role: 'run', action: 'logs' }, { role: 'run', action: 'backup' }, ], }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'rbac']); const text = deps.output.join('\n'); expect(text).toContain('edit:*'); expect(text).toContain('run>logs'); expect(text).toContain('run>backup'); }); it('displays name-scoped resource bindings', async () => { const deps = makeDeps([ { id: 'rbac-1', name: 'ha-viewer', subjects: [{ kind: 'User', name: 'alice@test.com' }], roleBindings: [{ role: 'view', resource: 'servers', name: 'my-ha' }], }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'rbac']); const text = deps.output.join('\n'); expect(text).toContain('view:servers:my-ha'); }); it('shows no results message for empty users list', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'users']); expect(deps.output[0]).toContain('No users found'); }); it('shows no results message for empty groups list', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'groups']); expect(deps.output[0]).toContain('No groups found'); }); it('shows no results message for empty rbac list', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'rbac']); expect(deps.output[0]).toContain('No rbac found'); }); it('lists prompts with project name column', async () => { const deps = makeDeps([ { id: 'p-1', name: 'debug-guide', projectId: 'proj-1', project: { name: 'smart-home' }, createdAt: '2025-01-01T00:00:00Z' }, { id: 'p-2', name: 'global-rules', projectId: null, project: null, createdAt: '2025-01-01T00:00:00Z' }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts']); const text = deps.output.join('\n'); expect(text).toContain('NAME'); expect(text).toContain('PROJECT'); expect(text).toContain('debug-guide'); expect(text).toContain('smart-home'); expect(text).toContain('global-rules'); expect(text).toContain('(global)'); }); it('lists promptrequests with project name column', async () => { const deps = makeDeps([ { id: 'pr-1', name: 'new-rule', projectId: 'proj-1', project: { name: 'my-project' }, createdBySession: 'sess-abc123def456', createdAt: '2025-01-01T00:00:00Z' }, ]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'promptrequests']); const text = deps.output.join('\n'); expect(text).toContain('new-rule'); expect(text).toContain('my-project'); expect(text).toContain('sess-abc123d'); }); it('passes --project option to fetchResource', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts', '--project', 'smart-home']); expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { project: 'smart-home' }); }); it('does not pass project when --project is not specified', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts']); expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, undefined); }); it('passes --all flag to fetchResource', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts', '-A']); expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { all: true }); }); it('passes both --project and --all when both given', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts', '--project', 'my-proj', '-A']); expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, { project: 'my-proj', all: true }); }); it('resolves prompt alias', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompt']); expect(deps.fetchResource).toHaveBeenCalledWith('prompts', undefined, undefined); }); it('resolves pr alias to promptrequests', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'pr']); expect(deps.fetchResource).toHaveBeenCalledWith('promptrequests', undefined, undefined); }); it('shows no results message for empty prompts list', async () => { const deps = makeDeps([]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'prompts']); expect(deps.output[0]).toContain('No prompts found'); }); it('lists projects with PLUGIN column showing resolved proxyModel', async () => { const deps = makeDeps([{ id: 'proj-1', name: 'home', description: '', proxyModel: '', gated: true, ownerId: 'usr-1', servers: [], }]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'projects']); const text = deps.output.join('\n'); expect(text).toContain('PLUGIN'); expect(text).not.toContain('GATED'); // proxyModel is empty but gated=true, table shows 'default' expect(text).toContain('default'); }); it('project JSON output resolves proxyModel from gated=true', async () => { const deps = makeDeps([{ id: 'proj-1', name: 'home', description: '', proxyModel: '', gated: true, ownerId: 'usr-1', servers: [], }]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']); const parsed = JSON.parse(deps.output[0] ?? '') as Array>; expect(parsed[0]!.proxyModel).toBe('default'); expect(parsed[0]).not.toHaveProperty('gated'); }); it('project JSON output resolves proxyModel from gated=false', async () => { const deps = makeDeps([{ id: 'proj-1', name: 'tools', description: '', proxyModel: '', gated: false, ownerId: 'usr-1', servers: [], }]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']); const parsed = JSON.parse(deps.output[0] ?? '') as Array>; expect(parsed[0]!.proxyModel).toBe('content-pipeline'); expect(parsed[0]).not.toHaveProperty('gated'); }); it('project JSON output preserves explicit proxyModel and drops gated', async () => { const deps = makeDeps([{ id: 'proj-1', name: 'custom', description: '', proxyModel: 'gate', gated: true, ownerId: 'usr-1', servers: [], }]); const cmd = createGetCommand(deps); await cmd.parseAsync(['node', 'test', 'projects', '-o', 'json']); const parsed = JSON.parse(deps.output[0] ?? '') as Array>; expect(parsed[0]!.proxyModel).toBe('gate'); expect(parsed[0]).not.toHaveProperty('gated'); }); });