import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { CronScheduler } from './cron.js'; import type { CronJobConfig } from '../config/schema.js'; import type { InboundMessage } from '../channels/types.js'; function makeCronJob(overrides?: Partial): CronJobConfig { return { name: 'test-job', schedule: '0 9 * * *', message: 'Hello from cron', output: { channel: 'telegram', peer: '123' }, enabled: true, ...overrides, }; } describe('CronScheduler', () => { let scheduler: CronScheduler; let mockChannelRegistry: { get: ReturnType }; beforeEach(() => { mockChannelRegistry = { get: vi.fn(), }; }); afterEach(async () => { if (scheduler) { await scheduler.disconnect(); } }); it('implements ChannelAdapter interface', () => { scheduler = new CronScheduler([], mockChannelRegistry as any); expect(scheduler.name).toBe('cron'); expect(scheduler.status).toBe('disconnected'); }); it('status changes to connected after connect()', async () => { scheduler = new CronScheduler([], mockChannelRegistry as any); await scheduler.connect(); expect(scheduler.status).toBe('connected'); }); it('status changes to disconnected after disconnect()', async () => { scheduler = new CronScheduler([], mockChannelRegistry as any); await scheduler.connect(); await scheduler.disconnect(); expect(scheduler.status).toBe('disconnected'); }); it('skips disabled jobs', async () => { const jobs = [makeCronJob({ enabled: false })]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); await scheduler.connect(); // Disabled job should not fire expect(messages).toHaveLength(0); }); it('fires a message when triggerJob is called', async () => { const jobs = [makeCronJob()]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); const messages: InboundMessage[] = []; scheduler.onMessage((msg: InboundMessage) => messages.push(msg)); await scheduler.connect(); // Manually trigger (simulates cron firing) scheduler.triggerJob('test-job'); expect(messages).toHaveLength(1); expect(messages[0].channel).toBe('cron'); expect(messages[0].senderId).toBe('test-job'); expect(messages[0].text).toBe('Hello from cron'); }); it('forwards response to output channel on send()', async () => { const mockOutputAdapter = { send: vi.fn().mockResolvedValue(undefined), }; mockChannelRegistry.get.mockReturnValue(mockOutputAdapter); const jobs = [makeCronJob()]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); await scheduler.connect(); await scheduler.send('test-job', { text: 'Agent response' }); expect(mockChannelRegistry.get).toHaveBeenCalledWith('telegram'); expect(mockOutputAdapter.send).toHaveBeenCalledWith('123', { text: 'Agent response' }); }); it('logs warning when output channel not found', async () => { mockChannelRegistry.get.mockReturnValue(undefined); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const jobs = [makeCronJob()]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); await scheduler.connect(); await scheduler.send('test-job', { text: 'Agent response' }); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Output channel')); warnSpy.mockRestore(); }); it('logs warning when job name not found in send()', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const jobs = [makeCronJob()]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); await scheduler.connect(); await scheduler.send('nonexistent-job', { text: 'response' }); expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No cron job')); warnSpy.mockRestore(); }); it('lists registered job names', () => { const jobs = [ makeCronJob({ name: 'job-a' }), makeCronJob({ name: 'job-b', enabled: false }), ]; scheduler = new CronScheduler(jobs, mockChannelRegistry as any); const names = scheduler.getJobNames(); expect(names).toEqual(['job-a', 'job-b']); }); });