148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
|
|
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;
|
||
|
|
}
|
||
|
|
}
|