import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CommandExecutorService } from './command-executor.service.js'; import type { SlashCommandPayload } from '@mosaicstack/types'; // Minimal mock implementations const mockRegistry = { getManifest: vi.fn(() => ({ version: 1, commands: [ { name: 'provider', aliases: [], scope: 'agent', execution: 'hybrid', available: true }, { name: 'mission', aliases: [], scope: 'agent', execution: 'socket', available: true }, { name: 'agent', aliases: ['a'], scope: 'agent', execution: 'socket', available: true }, { name: 'prdy', aliases: [], scope: 'agent', execution: 'socket', available: true }, { name: 'tools', aliases: [], scope: 'agent', execution: 'socket', available: true }, ], skills: [], })), }; const mockAgentService = { getSession: vi.fn(() => undefined), applyAgentConfig: vi.fn(), updateSessionModel: vi.fn(), }; const mockSystemOverride = { set: vi.fn(), get: vi.fn(), clear: vi.fn(), renew: vi.fn(), }; const mockSessionGC = { sweepOrphans: vi.fn(() => ({ orphanedSessions: 0, totalCleaned: [], duration: 0 })), }; const mockRedis = { set: vi.fn().mockResolvedValue('OK'), get: vi.fn(), del: vi.fn(), }; // Mock agent config returned by brain.agents.findByName for "my-agent-id" const mockAgentConfig = { id: 'my-agent-id', name: 'my-agent-id', model: 'claude-sonnet-4-6', provider: 'anthropic', systemPrompt: null, allowedTools: null, isSystem: false, ownerId: 'user-123', status: 'idle', createdAt: new Date(), updatedAt: new Date(), }; const mockBrain = { agents: { // findByName resolves with the agent when name matches, undefined otherwise findByName: vi.fn((name: string) => Promise.resolve(name === 'my-agent-id' ? mockAgentConfig : undefined), ), findById: vi.fn((id: string) => Promise.resolve(id === 'my-agent-id' ? mockAgentConfig : undefined), ), create: vi.fn(), }, }; const mockChatGateway = { broadcastSessionInfo: vi.fn(), }; function buildService(): CommandExecutorService { return new CommandExecutorService( mockRegistry as never, mockAgentService as never, mockSystemOverride as never, mockSessionGC as never, mockRedis as never, mockBrain as never, null, mockChatGateway as never, null, ); } describe('CommandExecutorService — P8-012 commands', () => { let service: CommandExecutorService; const userId = 'user-123'; const conversationId = 'conv-456'; beforeEach(() => { vi.clearAllMocks(); service = buildService(); }); // /provider login — missing provider name it('/provider login with no provider name returns usage error', async () => { const payload: SlashCommandPayload = { command: 'provider', args: 'login', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(false); expect(result.message).toContain('Usage: /provider login'); expect(result.command).toBe('provider'); }); // /provider login anthropic — success with URL containing poll token it('/provider login returns success with URL and poll token', async () => { const payload: SlashCommandPayload = { command: 'provider', args: 'login anthropic', conversationId, }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('provider'); expect(result.message).toContain('anthropic'); expect(result.message).toContain('http'); // data should contain loginUrl and pollToken expect(result.data).toBeDefined(); const data = result.data as Record; expect(typeof data['loginUrl']).toBe('string'); expect(typeof data['pollToken']).toBe('string'); expect(data['loginUrl'] as string).toContain('anthropic'); expect(data['loginUrl'] as string).toContain(data['pollToken'] as string); // Verify Valkey was called expect(mockRedis.set).toHaveBeenCalledOnce(); const [key, value, , ttl] = mockRedis.set.mock.calls[0] as [string, string, string, number]; expect(key).toContain('mosaic:auth:poll:'); const stored = JSON.parse(value) as { status: string; provider: string; userId: string }; expect(stored.status).toBe('pending'); expect(stored.provider).toBe('anthropic'); expect(stored.userId).toBe(userId); expect(ttl).toBe(300); }); // /provider with no args — returns usage it('/provider with no args returns usage message', async () => { const payload: SlashCommandPayload = { command: 'provider', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('Usage: /provider'); }); // /provider list it('/provider list returns success', async () => { const payload: SlashCommandPayload = { command: 'provider', args: 'list', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('provider'); }); // /provider logout with no name — usage error it('/provider logout with no name returns error', async () => { const payload: SlashCommandPayload = { command: 'provider', args: 'logout', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(false); expect(result.message).toContain('Usage: /provider logout'); }); // /provider unknown subcommand it('/provider unknown subcommand returns error', async () => { const payload: SlashCommandPayload = { command: 'provider', args: 'unknown', conversationId, }; const result = await service.execute(payload, userId); expect(result.success).toBe(false); expect(result.message).toContain('Unknown subcommand'); }); // /mission status it('/mission status returns stub message', async () => { const payload: SlashCommandPayload = { command: 'mission', args: 'status', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('mission'); expect(result.message).toContain('Mission status'); }); // /mission with no args it('/mission with no args returns status stub', async () => { const payload: SlashCommandPayload = { command: 'mission', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('Mission status'); }); // /mission set it('/mission set returns confirmation', async () => { const payload: SlashCommandPayload = { command: 'mission', args: 'set my-mission-123', conversationId, }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('my-mission-123'); }); // /agent list it('/agent list returns stub message', async () => { const payload: SlashCommandPayload = { command: 'agent', args: 'list', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('agent'); expect(result.message).toContain('agent'); }); // /agent with no args it('/agent with no args returns usage', async () => { const payload: SlashCommandPayload = { command: 'agent', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('Usage: /agent'); }); // /agent — switch it('/agent returns switch confirmation', async () => { const payload: SlashCommandPayload = { command: 'agent', args: 'my-agent-id', conversationId, }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.message).toContain('my-agent-id'); }); // /prdy it('/prdy returns PRD wizard message', async () => { const payload: SlashCommandPayload = { command: 'prdy', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('prdy'); expect(result.message).toContain('mosaic prdy'); }); // /tools it('/tools returns tools stub message', async () => { const payload: SlashCommandPayload = { command: 'tools', conversationId }; const result = await service.execute(payload, userId); expect(result.success).toBe(true); expect(result.command).toBe('tools'); expect(result.message).toContain('tools'); }); });