import { describe, it, expect } from 'vitest'; import { discoverServices } from './services.js'; import { ChannelRegistry } from '../../channels/registry.js'; import type { Config } from '../../config/index.js'; import type { CronJobConfig } from '../../config/schema.js'; function withMutableConfig(config: Config): Config & Record { return config as Config & Record; } function makeBaseConfig(): Config { return { server: { localhost: true, port: 18800 }, models: { default: { provider: 'anthropic', model: 'claude-sonnet-4', api_key: 'sk-test' }, fallback_chain: ['anthropic'] }, backends: { native: { enabled: true }, opencode: { enabled: false }, claude_code: { enabled: false } }, hooks: { confirm: [], log: [], silent: [] }, mcp: { servers: [] }, backup: { enabled: false, schedule: undefined, interval: '24h', run_on_start: false, notify: undefined, failure_threshold: 1, notify_recovery: true, local_dir: '~/.local/share/flynn/backups', include_vectors: true, minio: { enabled: false, endpoint: undefined, access_key: undefined, secret_key: undefined, bucket: undefined, prefix: 'flynn', secure: true, }, }, automation: { cron: [], webhooks: [], gmail: undefined, heartbeat: undefined, gcal: undefined, gdocs: undefined, gdrive: undefined, gtasks: undefined, }, telegram: undefined, discord: undefined, slack: undefined, whatsapp: undefined, matrix: undefined, signal: undefined, mattermost: undefined, teams: undefined, google_chat: undefined, bluebubbles: undefined, line: undefined, feishu: undefined, zalo: undefined, } as unknown as Config; } describe('discoverServices', () => { it('includes known services and marks not_configured when disabled', () => { const cfg = makeBaseConfig(); const reg = new ChannelRegistry(); const services = discoverServices(cfg, reg); expect(services).toEqual(expect.arrayContaining([ expect.objectContaining({ name: 'telegram', status: 'not_configured' }), expect.objectContaining({ name: 'matrix', status: 'not_configured' }), expect.objectContaining({ name: 'signal', status: 'not_configured' }), expect.objectContaining({ name: 'mattermost', status: 'not_configured' }), expect.objectContaining({ name: 'teams', status: 'not_configured' }), expect.objectContaining({ name: 'google_chat', status: 'not_configured' }), expect.objectContaining({ name: 'bluebubbles', status: 'not_configured' }), expect.objectContaining({ name: 'line', status: 'not_configured' }), expect.objectContaining({ name: 'feishu', status: 'not_configured' }), expect.objectContaining({ name: 'zalo', status: 'not_configured' }), expect.objectContaining({ name: 'cron', status: 'not_configured' }), expect.objectContaining({ name: 'daily_briefing', status: 'not_configured' }), expect.objectContaining({ name: 'backup', status: 'not_configured' }), expect.objectContaining({ name: 'mcp', status: 'not_configured' }), expect.objectContaining({ name: 'web_search', status: 'configured' }), expect.objectContaining({ name: 'audio_transcription', status: 'not_configured' }), ])); }); it('marks configured channels as disconnected when adapter is not registered', () => { const cfg = makeBaseConfig(); withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123], require_mention: false }; const reg = new ChannelRegistry(); const services = discoverServices(cfg, reg); expect(services.find(s => s.name === 'telegram')?.status).toBe('disconnected'); }); it('uses adapter status when channel adapter is registered', () => { const cfg = makeBaseConfig(); withMutableConfig(cfg).telegram = { bot_token: 'x', allowed_chat_ids: [123], require_mention: false }; const reg = new ChannelRegistry(); reg.register({ name: 'telegram', status: 'connected', connect: async () => {}, disconnect: async () => {}, send: async () => {}, onMessage: () => {}, }); const services = discoverServices(cfg, reg); expect(services.find(s => s.name === 'telegram')?.status).toBe('connected'); }); it('marks enabled automation subsystems as configured and carries item counts', () => { const cfg = makeBaseConfig(); cfg.automation.cron = [ { name: 'job', schedule: '0 0 * * *', message: 'hi', output: { channel: 'webchat', peer: 'x' }, enabled: true }, ] as CronJobConfig[]; (cfg.automation as Record).daily_briefing = { enabled: true, output: { channel: 'webchat', peer: 'x' }, }; cfg.mcp.servers = [{ name: 'srv', command: 'x', args: [] }]; cfg.backup.enabled = true; cfg.backup.schedule = '0 2 * * *'; const reg = new ChannelRegistry(); const services = discoverServices(cfg, reg); expect(services.find(s => s.name === 'cron')?.status).toBe('configured'); expect(services.find(s => s.name === 'cron')?.itemCount).toBe(2); expect(services.find(s => s.name === 'daily_briefing')?.status).toBe('configured'); expect(services.find(s => s.name === 'backup')?.status).toBe('configured'); expect(services.find(s => s.name === 'backup')?.metadata).toMatchObject({ schedule: '0 2 * * *' }); expect(services.find(s => s.name === 'mcp')?.metadata).toEqual({ serverCount: 1 }); }); it('marks audio transcription as configured and includes endpoint metadata', () => { const cfg = makeBaseConfig(); withMutableConfig(cfg).audio = { enabled: true, provider: { type: 'custom', endpoint: 'http://localhost:18801/v1/audio/transcriptions', model: 'whisper-1', }, talk_mode: { enabled: false, wake_phrase: 'hey flynn', timeout_ms: 120000, allow_manual_toggle: true, }, }; const reg = new ChannelRegistry(); const services = discoverServices(cfg, reg); const audio = services.find(s => s.name === 'audio_transcription'); expect(audio?.status).toBe('configured'); expect(audio?.metadata).toMatchObject({ provider: 'custom', endpoint: 'http://localhost:18801/v1/audio/transcriptions', model: 'whisper-1', }); }); });