From e157bc610207fdadfbe601314ae33a8af63e0162 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 22:12:12 -0800 Subject: [PATCH] feat(config): add automation.cron schema for scheduled jobs --- src/config/schema.test.ts | 78 +++++++++++++++++++++++++++++++++++++++ src/config/schema.ts | 18 +++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/config/schema.test.ts diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts new file mode 100644 index 0000000..4d3041e --- /dev/null +++ b/src/config/schema.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from 'vitest'; +import { configSchema } from './schema.js'; + +describe('configSchema automation', () => { + const baseConfig = { + telegram: { bot_token: 'test-token', allowed_chat_ids: [123] }, + models: { default: { provider: 'anthropic', model: 'claude-sonnet' } }, + }; + + it('accepts config without automation section', () => { + const result = configSchema.parse(baseConfig); + expect(result.automation).toBeDefined(); + expect(result.automation.cron).toEqual([]); + }); + + it('accepts config with cron jobs', () => { + const result = configSchema.parse({ + ...baseConfig, + automation: { + cron: [{ + name: 'morning-briefing', + schedule: '0 9 * * *', + message: 'Good morning!', + output: { channel: 'telegram', peer: '123' }, + }], + }, + }); + expect(result.automation.cron).toHaveLength(1); + expect(result.automation.cron[0].name).toBe('morning-briefing'); + expect(result.automation.cron[0].enabled).toBe(true); // default + }); + + it('rejects cron job with empty name', () => { + expect(() => configSchema.parse({ + ...baseConfig, + automation: { + cron: [{ + name: '', + schedule: '0 9 * * *', + message: 'test', + output: { channel: 'telegram', peer: '123' }, + }], + }, + })).toThrow(); + }); + + it('rejects cron job with empty schedule', () => { + expect(() => configSchema.parse({ + ...baseConfig, + automation: { + cron: [{ + name: 'test', + schedule: '', + message: 'test', + output: { channel: 'telegram', peer: '123' }, + }], + }, + })).toThrow(); + }); + + it('accepts cron job with optional fields', () => { + const result = configSchema.parse({ + ...baseConfig, + automation: { + cron: [{ + name: 'test', + schedule: '0 9 * * *', + message: 'test', + output: { channel: 'telegram', peer: '123' }, + enabled: false, + timezone: 'America/New_York', + }], + }, + }); + expect(result.automation.cron[0].enabled).toBe(false); + expect(result.automation.cron[0].timezone).toBe('America/New_York'); + }); +}); diff --git a/src/config/schema.ts b/src/config/schema.ts index 858750e..4cb37e8 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -71,6 +71,22 @@ const mcpSchema = z.object({ servers: z.array(mcpServerSchema).default([]), }).default({ servers: [] }); +const cronJobSchema = z.object({ + name: z.string().min(1, 'Cron job name is required'), + schedule: z.string().min(1, 'Cron schedule is required'), + message: z.string().min(1, 'Cron message is required'), + output: z.object({ + channel: z.string().min(1), + peer: z.string().min(1), + }), + enabled: z.boolean().default(true), + timezone: z.string().optional(), +}); + +const automationSchema = z.object({ + cron: z.array(cronJobSchema).default([]), +}).default({}); + export const configSchema = z.object({ telegram: telegramSchema, server: serverSchema.default({}), @@ -79,8 +95,10 @@ export const configSchema = z.object({ hooks: hooksSchema.default({}), skills: skillsSchema.default({}), mcp: mcpSchema.default({ servers: [] }), + automation: automationSchema, }); export type Config = z.infer; export type TelegramConfig = z.infer; export type ModelConfig = z.infer; +export type CronJobConfig = z.infer;