From 22b3bd4415a01c40d3864b46ce832f24eed5c123 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 22:40:51 +0100 Subject: [PATCH] fix: migrate cron payload channel alias --- src/cron/normalize.test.ts | 26 +++++++++++++++++++ src/cron/normalize.ts | 11 ++++++++ src/cron/service.test.ts | 51 ++++++++++++++++++++++++++++++++++++++ src/cron/service.ts | 22 ++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 src/cron/normalize.test.ts diff --git a/src/cron/normalize.test.ts b/src/cron/normalize.test.ts new file mode 100644 index 000000000..4dc6d830c --- /dev/null +++ b/src/cron/normalize.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeCronJobCreate } from "./normalize.js"; + +describe("normalizeCronJobCreate", () => { + it("maps legacy payload.channel to payload.provider and strips channel", () => { + const normalized = normalizeCronJobCreate({ + name: "legacy", + enabled: true, + schedule: { kind: "cron", expr: "* * * * *" }, + sessionTarget: "isolated", + wakeMode: "now", + payload: { + kind: "agentTurn", + message: "hi", + deliver: true, + channel: "telegram", + to: "7200373102", + }, + }) as unknown as Record; + + const payload = normalized.payload as Record; + expect(payload.provider).toBe("telegram"); + expect("channel" in payload).toBe(false); + }); +}); diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index 8586d56f8..2a4715c0b 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -32,6 +32,17 @@ function coercePayload(payload: UnknownRecord) { if (typeof payload.text === "string") next.kind = "systemEvent"; else if (typeof payload.message === "string") next.kind = "agentTurn"; } + + // Back-compat: older configs used `channel` for delivery provider. + const providerRaw = + typeof payload.provider === "string" ? payload.provider.trim() : ""; + const channelRaw = + typeof payload.channel === "string" ? payload.channel.trim() : ""; + const provider = + (providerRaw || channelRaw).trim().toLowerCase() || + (providerRaw || channelRaw).trim(); + if (!providerRaw && provider) next.provider = provider; + if ("channel" in next) delete next.channel; return next; } diff --git a/src/cron/service.test.ts b/src/cron/service.test.ts index 16edc8422..781782936 100644 --- a/src/cron/service.test.ts +++ b/src/cron/service.test.ts @@ -118,6 +118,57 @@ describe("CronService", () => { await store.cleanup(); }); + it("migrates legacy payload.channel to payload.provider on load", async () => { + const store = await makeStorePath(); + const enqueueSystemEvent = vi.fn(); + const requestHeartbeatNow = vi.fn(); + + const rawJob = { + id: "legacy-1", + name: "legacy", + enabled: true, + createdAtMs: Date.now(), + updatedAtMs: Date.now(), + schedule: { kind: "cron", expr: "* * * * *" }, + sessionTarget: "isolated", + wakeMode: "now", + payload: { + kind: "agentTurn", + message: "hi", + deliver: true, + channel: "telegram", + to: "7200373102", + }, + state: {}, + }; + + await fs.mkdir(path.dirname(store.storePath), { recursive: true }); + await fs.writeFile( + store.storePath, + JSON.stringify({ version: 1, jobs: [rawJob] }, null, 2), + "utf-8", + ); + + const cron = new CronService({ + storePath: store.storePath, + cronEnabled: true, + log: noopLogger, + enqueueSystemEvent, + requestHeartbeatNow, + runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })), + }); + + await cron.start(); + const jobs = await cron.list({ includeDisabled: true }); + const job = jobs.find((j) => j.id === rawJob.id); + const payload = job?.payload as unknown as Record; + expect(payload.provider).toBe("telegram"); + expect("channel" in payload).toBe(false); + + cron.stop(); + await store.cleanup(); + }); + it("posts last output to main even when isolated job errors", async () => { const store = await makeStorePath(); const enqueueSystemEvent = vi.fn(); diff --git a/src/cron/service.ts b/src/cron/service.ts index f1e40fdd2..2391d57de 100644 --- a/src/cron/service.ts +++ b/src/cron/service.ts @@ -317,6 +317,28 @@ export class CronService { raw.description = desc; mutated = true; } + + const payload = raw.payload; + if (payload && typeof payload === "object" && !Array.isArray(payload)) { + const legacyChannel = + typeof (payload as Record).channel === "string" + ? String((payload as Record).channel).trim() + : ""; + const provider = + typeof (payload as Record).provider === "string" + ? String((payload as Record).provider).trim() + : ""; + // Back-compat: older cron payloads used `channel` for delivery provider. + if (!provider && legacyChannel) { + (payload as Record).provider = + legacyChannel.toLowerCase(); + mutated = true; + } + if ("channel" in (payload as Record)) { + delete (payload as Record).channel; + mutated = true; + } + } } this.store = { version: 1, jobs: jobs as unknown as CronJob[] }; if (mutated) await this.persist();