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; 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; } }