Files
flynn/src/gateway/handlers/services.test.ts
T

172 lines
6.3 KiB
TypeScript

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<string, unknown> {
return config as Config & Record<string, unknown>;
}
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<string, unknown>).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',
});
});
});