import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { createStatusCommand } from '../../src/commands/status.js'; import type { StatusCommandDeps } from '../../src/commands/status.js'; import { saveConfig, DEFAULT_CONFIG } from '../../src/config/index.js'; import { saveCredentials } from '../../src/auth/index.js'; let tempDir: string; let output: string[]; let written: string[]; function log(...args: string[]) { output.push(args.join(' ')); } function write(text: string) { written.push(text); } function baseDeps(overrides?: Partial): Partial { return { configDeps: { configDir: tempDir }, credentialsDeps: { configDir: tempDir }, log, write, checkHealth: async () => true, isTTY: false, ...overrides, }; } beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-status-test-')); output = []; written = []; }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); describe('status command', () => { it('shows status in table format', async () => { const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); const out = output.join('\n'); expect(out).toContain('mcpctl v'); expect(out).toContain('mcplocal:'); expect(out).toContain('mcpd:'); expect(out).toContain('connected'); }); it('shows unreachable when daemons are down', async () => { const cmd = createStatusCommand(baseDeps({ checkHealth: async () => false })); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('unreachable'); }); it('shows not logged in when no credentials', async () => { const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('not logged in'); }); it('shows logged in user when credentials exist', async () => { saveCredentials({ token: 'tok', mcpdUrl: 'http://x:3100', user: 'alice@example.com' }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('logged in as alice@example.com'); }); it('shows status in JSON format', async () => { const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync(['-o', 'json'], { from: 'user' }); const parsed = JSON.parse(output[0]) as Record; expect(parsed['version']).toBe('0.1.0'); expect(parsed['mcplocalReachable']).toBe(true); expect(parsed['mcpdReachable']).toBe(true); }); it('shows status in YAML format', async () => { const cmd = createStatusCommand(baseDeps({ checkHealth: async () => false })); await cmd.parseAsync(['-o', 'yaml'], { from: 'user' }); expect(output[0]).toContain('mcplocalReachable: false'); }); it('checks correct URLs from config', async () => { saveConfig({ ...DEFAULT_CONFIG, mcplocalUrl: 'http://local:3200', mcpdUrl: 'http://remote:3100' }, { configDir: tempDir }); const checkedUrls: string[] = []; const cmd = createStatusCommand(baseDeps({ checkHealth: async (url) => { checkedUrls.push(url); return false; }, })); await cmd.parseAsync([], { from: 'user' }); expect(checkedUrls).toContain('http://local:3200'); expect(checkedUrls).toContain('http://remote:3100'); }); it('shows registries from config', async () => { saveConfig({ ...DEFAULT_CONFIG, registries: ['official'] }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('official'); expect(output.join('\n')).not.toContain('glama'); }); it('shows LLM not configured hint when no LLM is set', async () => { const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); const out = output.join('\n'); expect(out).toContain('LLM:'); expect(out).toContain('not configured'); expect(out).toContain('mcpctl config setup'); }); it('shows green check when LLM is healthy (non-TTY)', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'anthropic', model: 'claude-haiku-3-5-20241022' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ checkLlm: async () => 'ok' })); await cmd.parseAsync([], { from: 'user' }); const out = output.join('\n'); expect(out).toContain('anthropic / claude-haiku-3-5-20241022'); expect(out).toContain('✓ ok'); }); it('shows red cross when LLM check fails (non-TTY)', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ checkLlm: async () => 'not authenticated' })); await cmd.parseAsync([], { from: 'user' }); const out = output.join('\n'); expect(out).toContain('✗ not authenticated'); }); it('shows error message from mcplocal', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ checkLlm: async () => 'binary not found' })); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('✗ binary not found'); }); it('queries mcplocal URL for LLM health', async () => { saveConfig({ ...DEFAULT_CONFIG, mcplocalUrl: 'http://custom:9999', llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); let queriedUrl = ''; const cmd = createStatusCommand(baseDeps({ checkLlm: async (url) => { queriedUrl = url; return 'ok'; }, })); await cmd.parseAsync([], { from: 'user' }); expect(queriedUrl).toBe('http://custom:9999'); }); it('uses spinner on TTY and writes final result', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ isTTY: true, checkLlm: async () => 'ok', })); await cmd.parseAsync([], { from: 'user' }); // On TTY, the final LLM line goes through write(), not log() const finalWrite = written[written.length - 1]; expect(finalWrite).toContain('gemini-cli / gemini-2.5-flash'); expect(finalWrite).toContain('✓ ok'); }); it('uses spinner on TTY and shows failure', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ isTTY: true, checkLlm: async () => 'not authenticated', })); await cmd.parseAsync([], { from: 'user' }); const finalWrite = written[written.length - 1]; expect(finalWrite).toContain('✗ not authenticated'); }); it('shows not configured when LLM provider is none', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'none' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync([], { from: 'user' }); expect(output.join('\n')).toContain('not configured'); }); it('includes llm and llmStatus in JSON output', async () => { saveConfig({ ...DEFAULT_CONFIG, llm: { provider: 'gemini-cli', model: 'gemini-2.5-flash' } }, { configDir: tempDir }); const cmd = createStatusCommand(baseDeps({ checkLlm: async () => 'ok' })); await cmd.parseAsync(['-o', 'json'], { from: 'user' }); const parsed = JSON.parse(output[0]) as Record; expect(parsed['llm']).toBe('gemini-cli / gemini-2.5-flash'); expect(parsed['llmStatus']).toBe('ok'); }); it('includes null llm in JSON output when not configured', async () => { const cmd = createStatusCommand(baseDeps()); await cmd.parseAsync(['-o', 'json'], { from: 'user' }); const parsed = JSON.parse(output[0]) as Record; expect(parsed['llm']).toBeNull(); expect(parsed['llmStatus']).toBeNull(); }); });