refactor: cron payload migration cleanup (#621)
* refactor: centralize cron payload migration * test: stabilize block streaming mocks * test: adjust chunker fence-close case
This commit is contained in:
committed by
GitHub
parent
e3c340fd38
commit
98d0318d4e
@@ -14,7 +14,7 @@ describe("normalizeCronJobCreate", () => {
|
||||
kind: "agentTurn",
|
||||
message: "hi",
|
||||
deliver: true,
|
||||
channel: "telegram",
|
||||
channel: " TeLeGrAm ",
|
||||
to: "7200373102",
|
||||
},
|
||||
}) as unknown as Record<string, unknown>;
|
||||
@@ -23,4 +23,24 @@ describe("normalizeCronJobCreate", () => {
|
||||
expect(payload.provider).toBe("telegram");
|
||||
expect("channel" in payload).toBe(false);
|
||||
});
|
||||
|
||||
it("canonicalizes payload.provider casing", () => {
|
||||
const normalized = normalizeCronJobCreate({
|
||||
name: "legacy provider",
|
||||
enabled: true,
|
||||
schedule: { kind: "cron", expr: "* * * * *" },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "hi",
|
||||
deliver: true,
|
||||
provider: "Telegram",
|
||||
to: "7200373102",
|
||||
},
|
||||
}) as unknown as Record<string, unknown>;
|
||||
|
||||
const payload = normalized.payload as Record<string, unknown>;
|
||||
expect(payload.provider).toBe("telegram");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
||||
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
@@ -34,15 +35,7 @@ function coercePayload(payload: UnknownRecord) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
migrateLegacyCronPayload(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
38
src/cron/payload-migration.ts
Normal file
38
src/cron/payload-migration.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeProvider(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function migrateLegacyCronPayload(payload: UnknownRecord): boolean {
|
||||
let mutated = false;
|
||||
|
||||
const providerValue = readString(payload.provider);
|
||||
const channelValue = readString(payload.channel);
|
||||
|
||||
const nextProvider =
|
||||
typeof providerValue === "string" && providerValue.trim().length > 0
|
||||
? normalizeProvider(providerValue)
|
||||
: typeof channelValue === "string" && channelValue.trim().length > 0
|
||||
? normalizeProvider(channelValue)
|
||||
: "";
|
||||
|
||||
if (nextProvider) {
|
||||
if (providerValue !== nextProvider) {
|
||||
payload.provider = nextProvider;
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ("channel" in payload) {
|
||||
delete payload.channel;
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
return mutated;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ describe("CronService", () => {
|
||||
kind: "agentTurn",
|
||||
message: "hi",
|
||||
deliver: true,
|
||||
channel: "telegram",
|
||||
channel: " TeLeGrAm ",
|
||||
to: "7200373102",
|
||||
},
|
||||
state: {},
|
||||
@@ -169,6 +169,56 @@ describe("CronService", () => {
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
it("canonicalizes payload.provider casing on load", async () => {
|
||||
const store = await makeStorePath();
|
||||
const enqueueSystemEvent = vi.fn();
|
||||
const requestHeartbeatNow = vi.fn();
|
||||
|
||||
const rawJob = {
|
||||
id: "legacy-2",
|
||||
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,
|
||||
provider: "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");
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { truncateUtf16Safe } from "../utils.js";
|
||||
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
||||
import { computeNextRunAtMs } from "./schedule.js";
|
||||
import { loadCronStore, saveCronStore } from "./store.js";
|
||||
import type {
|
||||
@@ -320,22 +321,7 @@ export class CronService {
|
||||
|
||||
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;
|
||||
if (migrateLegacyCronPayload(payload as Record<string, unknown>)) {
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user