Files
flynn/src/automation/cron.test.ts
T
William Valentin b9e008ea23 feat(automation): add CronScheduler channel adapter
Implements CronScheduler as a ChannelAdapter that fires InboundMessages
on cron schedules and routes agent responses to configured output
channels (e.g. Telegram). Includes 9 tests.
2026-02-05 22:22:13 -08:00

135 lines
4.3 KiB
TypeScript

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>): 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<typeof vi.fn> };
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']);
});
});