fix(cron): migrate legacy string schedule and command jobs

This commit is contained in:
bmendonca3
2026-03-02 09:50:15 -07:00
committed by Peter Steinberger
parent c424836fbe
commit 4cd04e4652
2 changed files with 78 additions and 1 deletions

View File

@@ -178,4 +178,47 @@ describe("cron store migration", () => {
expect(schedule.kind).toBe("cron");
expect(schedule.staggerMs).toBeUndefined();
});
it("migrates legacy string schedules and command-only payloads (#18445)", async () => {
const store = await makeStorePath();
try {
await writeLegacyStore(store.storePath, {
id: "imessage-refresh",
name: "iMessage Refresh",
enabled: true,
createdAtMs: 1_700_000_000_000,
updatedAtMs: 1_700_000_000_000,
schedule: "0 */2 * * *",
command: "bash /tmp/imessage-refresh.sh",
timeout: 120,
state: {},
});
await migrateAndLoadFirstJob(store.storePath);
const loaded = await loadCronStore(store.storePath);
const migrated = loaded.jobs[0] as Record<string, unknown>;
expect(migrated.schedule).toEqual(
expect.objectContaining({
kind: "cron",
expr: "0 */2 * * *",
}),
);
expect(migrated.sessionTarget).toBe("main");
expect(migrated.wakeMode).toBe("now");
expect(migrated.payload).toEqual({
kind: "systemEvent",
text: "bash /tmp/imessage-refresh.sh",
});
expect("command" in migrated).toBe(false);
expect("timeout" in migrated).toBe(false);
const scheduleWarn = noopLogger.warn.mock.calls.find((args) =>
String(args[1] ?? "").includes("failed to compute next run for job (skipping)"),
);
expect(scheduleWarn).toBeUndefined();
} finally {
await store.cleanup();
}
});
});

View File

@@ -92,6 +92,7 @@ function normalizePayloadKind(payload: Record<string, unknown>) {
function inferPayloadIfMissing(raw: Record<string, unknown>) {
const message = typeof raw.message === "string" ? raw.message.trim() : "";
const text = typeof raw.text === "string" ? raw.text.trim() : "";
const command = typeof raw.command === "string" ? raw.command.trim() : "";
if (message) {
raw.payload = { kind: "agentTurn", message };
return true;
@@ -100,6 +101,10 @@ function inferPayloadIfMissing(raw: Record<string, unknown>) {
raw.payload = { kind: "systemEvent", text };
return true;
}
if (command) {
raw.payload = { kind: "systemEvent", text: command };
return true;
}
return false;
}
@@ -209,6 +214,12 @@ function stripLegacyTopLevelFields(raw: Record<string, unknown>) {
if ("provider" in raw) {
delete raw.provider;
}
if ("command" in raw) {
delete raw.command;
}
if ("timeout" in raw) {
delete raw.timeout;
}
}
async function getFileMtimeMs(path: string): Promise<number | null> {
@@ -262,6 +273,12 @@ export async function ensureLoaded(
mutated = true;
}
if (typeof raw.schedule === "string") {
const expr = raw.schedule.trim();
raw.schedule = { kind: "cron", expr };
mutated = true;
}
const nameRaw = raw.name;
if (typeof nameRaw !== "string" || nameRaw.trim().length === 0) {
raw.name = inferLegacyName({
@@ -353,7 +370,9 @@ export async function ensureLoaded(
"channel" in raw ||
"to" in raw ||
"bestEffortDeliver" in raw ||
"provider" in raw;
"provider" in raw ||
"command" in raw ||
"timeout" in raw;
if (hadLegacyTopLevelFields) {
stripLegacyTopLevelFields(raw);
mutated = true;
@@ -469,6 +488,21 @@ export async function ensureLoaded(
const payloadKind =
payloadRecord && typeof payloadRecord.kind === "string" ? payloadRecord.kind : "";
const normalizedSessionTarget =
typeof raw.sessionTarget === "string" ? raw.sessionTarget.trim().toLowerCase() : "";
if (normalizedSessionTarget === "main" || normalizedSessionTarget === "isolated") {
if (raw.sessionTarget !== normalizedSessionTarget) {
raw.sessionTarget = normalizedSessionTarget;
mutated = true;
}
} else {
const inferredSessionTarget = payloadKind === "agentTurn" ? "isolated" : "main";
if (raw.sessionTarget !== inferredSessionTarget) {
raw.sessionTarget = inferredSessionTarget;
mutated = true;
}
}
const sessionTarget =
typeof raw.sessionTarget === "string" ? raw.sessionTarget.trim().toLowerCase() : "";
const isIsolatedAgentTurn =