Files
mcpctl/src/cli/tests/commands/prime-agent.test.ts

148 lines
5.6 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createConfigCommand } from '../../src/commands/config.js';
import type { ApiClient } from '../../src/api-client.js';
import { DEFAULT_MCPCTL_GATEWAY_URL } from '../../src/config/prime-agent.js';
function mockClient(): ApiClient {
return {
get: vi.fn(async () => ({})),
post: vi.fn(async () => ({ token: 'impersonated-tok', user: { email: 'other@test.com' } })),
put: vi.fn(async () => ({})),
delete: vi.fn(async () => {}),
} as unknown as ApiClient;
}
describe('config prime-agent', () => {
let client: ReturnType<typeof mockClient>;
let output: string[];
let tmpDir: string;
const log = (...args: string[]) => output.push(args.join(' '));
let prevCwd: string;
beforeEach(() => {
client = mockClient();
output = [];
tmpDir = mkdtempSync(join(tmpdir(), 'mcpctl-config-prime-agent-'));
// config prime-agent writes the .mcpctl-project marker into cwd, so run
// every test from an isolated temp dir to avoid polluting the repo.
prevCwd = process.cwd();
process.chdir(tmpDir);
});
afterEach(() => {
process.chdir(prevCwd);
process.exitCode = 0;
rmSync(tmpDir, { recursive: true, force: true });
});
it('requires --project', async () => {
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--skip-skills'], { from: 'user' });
expect(output.join('\n')).toContain('--project is required');
expect(process.exitCode).toBe(1);
});
it('writes proxy MCP entry into prime-agent settings.json', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'homeautomation', '-o', settingsPath, '--skip-skills'], { from: 'user' });
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
expect(written.mcpServers['homeautomation']).toEqual({
type: 'http',
url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/homeautomation/mcp`,
});
expect(output.join('\n')).toContain('homeautomation');
});
it('merges with existing servers and preserves other settings', async () => {
const settingsPath = join(tmpDir, 'settings.json');
writeFileSync(settingsPath, JSON.stringify({
defaultProvider: 'itaz',
mcpServers: {
sre: { type: 'http', url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/sre/mcp` },
},
}));
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'proj-1', '-o', settingsPath, '--skip-skills'], { from: 'user' });
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
expect(written.defaultProvider).toBe('itaz'); // untouched
expect(written.mcpServers['sre']).toBeDefined(); // preserved
expect(written.mcpServers['proj-1']).toEqual({
type: 'http',
url: `${DEFAULT_MCPCTL_GATEWAY_URL}/projects/proj-1/mcp`,
});
});
it('writes a project marker for later skills sync', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'sre', '-o', settingsPath, '--skip-skills'], { from: 'user' });
const markerPath = join(tmpDir, '.mcpctl-project');
expect(readFileSync(markerPath, 'utf-8').trim()).toBe('sre');
});
it('--dry-run prints the change without writing', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'proj-2', '-o', settingsPath, '--dry-run'], { from: 'user' });
expect(output.join('\n')).toContain('proj-2');
// No file should have been created.
expect(exceptionSafeRead(settingsPath)).toBeNull();
});
it('does not call the API when --skip-skills is set', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'proj-3', '-o', settingsPath, '--skip-skills'], { from: 'user' });
expect(client.get).not.toHaveBeenCalled();
});
it('backward compat: prime-agent-generate still works', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent-generate', '--project', 'proj-1', '-o', settingsPath, '--skip-skills'], { from: 'user' });
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'));
expect(written.mcpServers['proj-1']).toBeDefined();
});
});
function exceptionSafeRead(path: string): string | null {
try {
return readFileSync(path, 'utf-8');
} catch {
return null;
}
}