proxyMode "direct" was a security hole (leaked secrets as plaintext env vars in .mcp.json) and bypassed all mcplocal features (gating, audit, RBAC, content pipeline, namespacing). Removed from schema, API, CLI, and all tests. Old configs with proxyMode are accepted but silently stripped via Zod .transform() for backward compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
559 lines
22 KiB
TypeScript
559 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { createCreateCommand } from '../../src/commands/create.js';
|
|
import { type ApiClient, ApiError } 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', 'API_KEY=secretRef:creds:API_KEY',
|
|
'--env', 'BASE_URL=http://localhost',
|
|
], { 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")'],
|
|
env: [
|
|
{ name: 'API_KEY', valueFrom: { secretRef: { name: 'creds', key: 'API_KEY' } } },
|
|
{ name: 'BASE_URL', value: 'http://localhost' },
|
|
],
|
|
});
|
|
});
|
|
|
|
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',
|
|
}));
|
|
});
|
|
|
|
it('strips null values from template when using --from-template', async () => {
|
|
vi.mocked(client.get).mockResolvedValueOnce([{
|
|
id: 'tpl-1',
|
|
name: 'grafana',
|
|
version: '1.0.0',
|
|
description: 'Grafana MCP',
|
|
packageName: '@leval/mcp-grafana',
|
|
dockerImage: null,
|
|
transport: 'STDIO',
|
|
repositoryUrl: 'https://github.com/test',
|
|
externalUrl: null,
|
|
command: null,
|
|
containerPort: null,
|
|
replicas: 1,
|
|
env: [{ name: 'TOKEN', required: true, description: 'A token' }],
|
|
healthCheck: { tool: 'test', arguments: {} },
|
|
createdAt: '2025-01-01',
|
|
updatedAt: '2025-01-01',
|
|
}] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'server', 'my-grafana', '--from-template=grafana',
|
|
'--env', 'TOKEN=secretRef:creds:TOKEN',
|
|
], { from: 'user' });
|
|
const call = vi.mocked(client.post).mock.calls[0]![1] as Record<string, unknown>;
|
|
// null fields from template should NOT be in the body
|
|
expect(call).not.toHaveProperty('dockerImage');
|
|
expect(call).not.toHaveProperty('externalUrl');
|
|
expect(call).not.toHaveProperty('command');
|
|
expect(call).not.toHaveProperty('containerPort');
|
|
// non-null fields should be present
|
|
expect(call.packageName).toBe('@leval/mcp-grafana');
|
|
expect(call.healthCheck).toEqual({ tool: 'test', arguments: {} });
|
|
expect(call.templateName).toBe('grafana');
|
|
});
|
|
|
|
it('throws on 409 without --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists: my-server"}'));
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(cmd.parseAsync(['server', 'my-server'], { from: 'user' })).rejects.toThrow('API error 409');
|
|
});
|
|
|
|
it('updates existing server on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Server already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'srv-1', name: 'my-server' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['server', 'my-server', '--force'], { from: 'user' });
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/servers/srv-1', expect.objectContaining({
|
|
transport: 'STDIO',
|
|
}));
|
|
expect(output.join('\n')).toContain("server 'my-server' updated");
|
|
});
|
|
});
|
|
|
|
describe('create secret', () => {
|
|
it('creates a secret with --data flags', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'secret', 'ha-creds',
|
|
'--data', 'TOKEN=abc123',
|
|
'--data', 'URL=https://ha.local',
|
|
], { from: 'user' });
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', {
|
|
name: 'ha-creds',
|
|
data: { TOKEN: 'abc123', URL: 'https://ha.local' },
|
|
});
|
|
expect(output.join('\n')).toContain("secret 'test' created");
|
|
});
|
|
|
|
it('creates a secret with empty data', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['secret', 'empty-secret'], { from: 'user' });
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', {
|
|
name: 'empty-secret',
|
|
data: {},
|
|
});
|
|
});
|
|
|
|
it('throws on 409 without --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists: my-creds"}'));
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val'], { from: 'user' })).rejects.toThrow('API error 409');
|
|
});
|
|
|
|
it('updates existing secret on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Secret already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'sec-1', name: 'my-creds' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['secret', 'my-creds', '--data', 'KEY=val', '--force'], { from: 'user' });
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/secrets/sec-1', { data: { KEY: 'val' } });
|
|
expect(output.join('\n')).toContain("secret 'my-creds' updated");
|
|
});
|
|
});
|
|
|
|
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: '',
|
|
});
|
|
});
|
|
|
|
it('updates existing project on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Project already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-proj' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['project', 'my-proj', '-d', 'updated', '--force'], { from: 'user' });
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/projects/proj-1', { description: 'updated' });
|
|
expect(output.join('\n')).toContain("project 'my-proj' updated");
|
|
});
|
|
});
|
|
|
|
describe('create user', () => {
|
|
it('creates a user with password and name', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'usr-1', email: 'alice@test.com' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'user', 'alice@test.com',
|
|
'--password', 'secret123',
|
|
'--name', 'Alice',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/users', {
|
|
email: 'alice@test.com',
|
|
password: 'secret123',
|
|
name: 'Alice',
|
|
});
|
|
expect(output.join('\n')).toContain("user 'alice@test.com' created");
|
|
});
|
|
|
|
it('does not send role field (RBAC is the auth mechanism)', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'usr-1', email: 'admin@test.com' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'user', 'admin@test.com',
|
|
'--password', 'pass123',
|
|
], { from: 'user' });
|
|
|
|
const callBody = vi.mocked(client.post).mock.calls[0]![1] as Record<string, unknown>;
|
|
expect(callBody).not.toHaveProperty('role');
|
|
});
|
|
|
|
it('requires --password', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(cmd.parseAsync(['user', 'alice@test.com'], { from: 'user' })).rejects.toThrow('--password is required');
|
|
});
|
|
|
|
it('throws on 409 without --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"User already exists"}'));
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['user', 'alice@test.com', '--password', 'pass'], { from: 'user' }),
|
|
).rejects.toThrow('API error 409');
|
|
});
|
|
|
|
it('updates existing user on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"User already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'usr-1', email: 'alice@test.com' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'user', 'alice@test.com', '--password', 'newpass', '--name', 'Alice New', '--force',
|
|
], { from: 'user' });
|
|
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/users/usr-1', {
|
|
password: 'newpass',
|
|
name: 'Alice New',
|
|
});
|
|
expect(output.join('\n')).toContain("user 'alice@test.com' updated");
|
|
});
|
|
});
|
|
|
|
describe('create group', () => {
|
|
it('creates a group with members', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'grp-1', name: 'dev-team' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'group', 'dev-team',
|
|
'--description', 'Development team',
|
|
'--member', 'alice@test.com',
|
|
'--member', 'bob@test.com',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/groups', {
|
|
name: 'dev-team',
|
|
description: 'Development team',
|
|
members: ['alice@test.com', 'bob@test.com'],
|
|
});
|
|
expect(output.join('\n')).toContain("group 'dev-team' created");
|
|
});
|
|
|
|
it('creates a group with no members', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'grp-1', name: 'empty-group' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['group', 'empty-group'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/groups', {
|
|
name: 'empty-group',
|
|
members: [],
|
|
});
|
|
});
|
|
|
|
it('throws on 409 without --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Group already exists"}'));
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['group', 'dev-team'], { from: 'user' }),
|
|
).rejects.toThrow('API error 409');
|
|
});
|
|
|
|
it('updates existing group on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"Group already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'grp-1', name: 'dev-team' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'group', 'dev-team', '--member', 'new@test.com', '--force',
|
|
], { from: 'user' });
|
|
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/groups/grp-1', {
|
|
members: ['new@test.com'],
|
|
});
|
|
expect(output.join('\n')).toContain("group 'dev-team' updated");
|
|
});
|
|
});
|
|
|
|
describe('create rbac', () => {
|
|
it('creates an RBAC definition with subjects and bindings', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'developers' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'rbac', 'developers',
|
|
'--subject', 'User:alice@test.com',
|
|
'--subject', 'Group:dev-team',
|
|
'--binding', 'edit:servers',
|
|
'--binding', 'view:instances',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
|
|
name: 'developers',
|
|
subjects: [
|
|
{ kind: 'User', name: 'alice@test.com' },
|
|
{ kind: 'Group', name: 'dev-team' },
|
|
],
|
|
roleBindings: [
|
|
{ role: 'edit', resource: 'servers' },
|
|
{ role: 'view', resource: 'instances' },
|
|
],
|
|
});
|
|
expect(output.join('\n')).toContain("rbac 'developers' created");
|
|
});
|
|
|
|
it('creates an RBAC definition with wildcard resource', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'admins' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'rbac', 'admins',
|
|
'--subject', 'User:admin@test.com',
|
|
'--binding', 'edit:*',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
|
|
name: 'admins',
|
|
subjects: [{ kind: 'User', name: 'admin@test.com' }],
|
|
roleBindings: [{ role: 'edit', resource: '*' }],
|
|
});
|
|
});
|
|
|
|
it('creates an RBAC definition with empty subjects and bindings', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'empty' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['rbac', 'empty'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
|
|
name: 'empty',
|
|
subjects: [],
|
|
roleBindings: [],
|
|
});
|
|
});
|
|
|
|
it('throws on invalid subject format', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['rbac', 'bad', '--subject', 'no-colon'], { from: 'user' }),
|
|
).rejects.toThrow('Invalid subject format');
|
|
});
|
|
|
|
it('throws on invalid binding format', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['rbac', 'bad', '--binding', 'no-colon'], { from: 'user' }),
|
|
).rejects.toThrow('Invalid binding format');
|
|
});
|
|
|
|
it('throws on 409 without --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"RBAC already exists"}'));
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['rbac', 'developers', '--subject', 'User:a@b.com', '--binding', 'edit:servers'], { from: 'user' }),
|
|
).rejects.toThrow('API error 409');
|
|
});
|
|
|
|
it('updates existing RBAC on 409 with --force', async () => {
|
|
vi.mocked(client.post).mockRejectedValueOnce(new ApiError(409, '{"error":"RBAC already exists"}'));
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'rbac-1', name: 'developers' }] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'rbac', 'developers',
|
|
'--subject', 'User:new@test.com',
|
|
'--binding', 'edit:*',
|
|
'--force',
|
|
], { from: 'user' });
|
|
|
|
expect(client.put).toHaveBeenCalledWith('/api/v1/rbac/rbac-1', {
|
|
subjects: [{ kind: 'User', name: 'new@test.com' }],
|
|
roleBindings: [{ role: 'edit', resource: '*' }],
|
|
});
|
|
expect(output.join('\n')).toContain("rbac 'developers' updated");
|
|
});
|
|
|
|
it('creates an RBAC definition with operation bindings', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'ops' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'rbac', 'ops',
|
|
'--subject', 'Group:ops-team',
|
|
'--binding', 'edit:servers',
|
|
'--operation', 'logs',
|
|
'--operation', 'backup',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
|
|
name: 'ops',
|
|
subjects: [{ kind: 'Group', name: 'ops-team' }],
|
|
roleBindings: [
|
|
{ role: 'edit', resource: 'servers' },
|
|
{ role: 'run', action: 'logs' },
|
|
{ role: 'run', action: 'backup' },
|
|
],
|
|
});
|
|
expect(output.join('\n')).toContain("rbac 'ops' created");
|
|
});
|
|
|
|
it('creates an RBAC definition with name-scoped binding', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'rbac-1', name: 'ha-viewer' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync([
|
|
'rbac', 'ha-viewer',
|
|
'--subject', 'User:alice@test.com',
|
|
'--binding', 'view:servers:my-ha',
|
|
], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/rbac', {
|
|
name: 'ha-viewer',
|
|
subjects: [{ kind: 'User', name: 'alice@test.com' }],
|
|
roleBindings: [
|
|
{ role: 'view', resource: 'servers', name: 'my-ha' },
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('create prompt', () => {
|
|
it('creates a prompt with content', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'test-prompt' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['prompt', 'test-prompt', '--content', 'Hello world'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', {
|
|
name: 'test-prompt',
|
|
content: 'Hello world',
|
|
});
|
|
expect(output.join('\n')).toContain("prompt 'test-prompt' created");
|
|
});
|
|
|
|
it('requires content or content-file', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['prompt', 'no-content'], { from: 'user' }),
|
|
).rejects.toThrow('--content or --content-file is required');
|
|
});
|
|
|
|
it('--priority sets prompt priority', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'pri-prompt' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['prompt', 'pri-prompt', '--content', 'x', '--priority', '8'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
|
|
priority: 8,
|
|
}));
|
|
});
|
|
|
|
it('--priority validates range 1-10', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '15'], { from: 'user' }),
|
|
).rejects.toThrow('--priority must be a number between 1 and 10');
|
|
});
|
|
|
|
it('--priority rejects zero', async () => {
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--priority', '0'], { from: 'user' }),
|
|
).rejects.toThrow('--priority must be a number between 1 and 10');
|
|
});
|
|
|
|
it('--link sets linkTarget', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'linked' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['prompt', 'linked', '--content', 'x', '--link', 'proj/srv:docmost://pages/abc'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
|
|
linkTarget: 'proj/srv:docmost://pages/abc',
|
|
}));
|
|
});
|
|
|
|
it('--project resolves project name to ID', async () => {
|
|
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-project' }] as never);
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'p-1', name: 'scoped' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['prompt', 'scoped', '--content', 'x', '--project', 'my-project'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/prompts', expect.objectContaining({
|
|
projectId: 'proj-1',
|
|
}));
|
|
});
|
|
|
|
it('--project throws when project not found', async () => {
|
|
vi.mocked(client.get).mockResolvedValueOnce([] as never);
|
|
const cmd = createCreateCommand({ client, log });
|
|
await expect(
|
|
cmd.parseAsync(['prompt', 'bad', '--content', 'x', '--project', 'nope'], { from: 'user' }),
|
|
).rejects.toThrow("Project 'nope' not found");
|
|
});
|
|
});
|
|
|
|
describe('create promptrequest', () => {
|
|
it('creates a prompt request with priority', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'r-1', name: 'req' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['promptrequest', 'req', '--content', 'proposal', '--priority', '7'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/promptrequests', expect.objectContaining({
|
|
name: 'req',
|
|
content: 'proposal',
|
|
priority: 7,
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('create project', () => {
|
|
it('creates a project with --gated', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'gated-proj' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['project', 'gated-proj', '--gated'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
|
|
gated: true,
|
|
}));
|
|
});
|
|
|
|
it('creates a project with --no-gated', async () => {
|
|
vi.mocked(client.post).mockResolvedValueOnce({ id: 'proj-1', name: 'open-proj' });
|
|
const cmd = createCreateCommand({ client, log });
|
|
await cmd.parseAsync(['project', 'open-proj', '--no-gated'], { from: 'user' });
|
|
|
|
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
|
|
gated: false,
|
|
}));
|
|
});
|
|
});
|
|
});
|