fix: migrate cron payload channel alias
This commit is contained in:
26
src/cron/normalize.test.ts
Normal file
26
src/cron/normalize.test.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
const payload = normalized.payload as Record<string, unknown>;
|
||||||
|
expect(payload.provider).toBe("telegram");
|
||||||
|
expect("channel" in payload).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,6 +32,17 @@ function coercePayload(payload: UnknownRecord) {
|
|||||||
if (typeof payload.text === "string") next.kind = "systemEvent";
|
if (typeof payload.text === "string") next.kind = "systemEvent";
|
||||||
else if (typeof payload.message === "string") next.kind = "agentTurn";
|
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;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,57 @@ describe("CronService", () => {
|
|||||||
await store.cleanup();
|
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<string, unknown>;
|
||||||
|
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 () => {
|
it("posts last output to main even when isolated job errors", async () => {
|
||||||
const store = await makeStorePath();
|
const store = await makeStorePath();
|
||||||
const enqueueSystemEvent = vi.fn();
|
const enqueueSystemEvent = vi.fn();
|
||||||
|
|||||||
@@ -317,6 +317,28 @@ export class CronService {
|
|||||||
raw.description = desc;
|
raw.description = desc;
|
||||||
mutated = true;
|
mutated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payload = raw.payload;
|
||||||
|
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
||||||
|
const legacyChannel =
|
||||||
|
typeof (payload as Record<string, unknown>).channel === "string"
|
||||||
|
? String((payload as Record<string, unknown>).channel).trim()
|
||||||
|
: "";
|
||||||
|
const provider =
|
||||||
|
typeof (payload as Record<string, unknown>).provider === "string"
|
||||||
|
? String((payload as Record<string, unknown>).provider).trim()
|
||||||
|
: "";
|
||||||
|
// Back-compat: older cron payloads used `channel` for delivery provider.
|
||||||
|
if (!provider && legacyChannel) {
|
||||||
|
(payload as Record<string, unknown>).provider =
|
||||||
|
legacyChannel.toLowerCase();
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
|
if ("channel" in (payload as Record<string, unknown>)) {
|
||||||
|
delete (payload as Record<string, unknown>).channel;
|
||||||
|
mutated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.store = { version: 1, jobs: jobs as unknown as CronJob[] };
|
this.store = { version: 1, jobs: jobs as unknown as CronJob[] };
|
||||||
if (mutated) await this.persist();
|
if (mutated) await this.persist();
|
||||||
|
|||||||
Reference in New Issue
Block a user