feat(cron): enhance legacy delivery handling in job patches
- Introduced logic to map legacy payload delivery updates onto the delivery object for `agentTurn` jobs, ensuring backward compatibility with legacy clients. - Added tests to validate the correct application of legacy delivery settings in job patches, improving reliability in job configuration. - Refactored delivery handling functions to streamline the merging of legacy delivery fields into the current job structure. This update enhances the flexibility of delivery configurations, ensuring that legacy settings are properly handled in the context of new job patches.
This commit is contained in:
committed by
Peter Steinberger
parent
246896d64b
commit
6fb8d8850e
@@ -29,4 +29,75 @@ describe("applyJobPatch", () => {
|
||||
expect(job.payload.kind).toBe("systemEvent");
|
||||
expect(job.delivery).toBeUndefined();
|
||||
});
|
||||
|
||||
it("maps legacy payload delivery updates onto delivery", () => {
|
||||
const now = Date.now();
|
||||
const job: CronJob = {
|
||||
id: "job-2",
|
||||
name: "job-2",
|
||||
enabled: true,
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "do it" },
|
||||
delivery: { mode: "announce", channel: "telegram", to: "123" },
|
||||
state: {},
|
||||
};
|
||||
|
||||
const patch: CronJobPatch = {
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
deliver: false,
|
||||
channel: "Signal",
|
||||
to: "555",
|
||||
bestEffortDeliver: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => applyJobPatch(job, patch)).not.toThrow();
|
||||
expect(job.payload.kind).toBe("agentTurn");
|
||||
if (job.payload.kind === "agentTurn") {
|
||||
expect(job.payload.deliver).toBe(false);
|
||||
expect(job.payload.channel).toBe("Signal");
|
||||
expect(job.payload.to).toBe("555");
|
||||
expect(job.payload.bestEffortDeliver).toBe(true);
|
||||
}
|
||||
expect(job.delivery).toEqual({
|
||||
mode: "none",
|
||||
channel: "signal",
|
||||
to: "555",
|
||||
bestEffort: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats legacy payload targets as announce requests", () => {
|
||||
const now = Date.now();
|
||||
const job: CronJob = {
|
||||
id: "job-3",
|
||||
name: "job-3",
|
||||
enabled: true,
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "do it" },
|
||||
delivery: { mode: "none", channel: "telegram" },
|
||||
state: {},
|
||||
};
|
||||
|
||||
const patch: CronJobPatch = {
|
||||
payload: { kind: "agentTurn", to: " 999 " },
|
||||
};
|
||||
|
||||
expect(() => applyJobPatch(job, patch)).not.toThrow();
|
||||
expect(job.delivery).toEqual({
|
||||
mode: "announce",
|
||||
channel: "telegram",
|
||||
to: "999",
|
||||
bestEffort: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,6 +155,17 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
|
||||
if (patch.payload) {
|
||||
job.payload = mergeCronPayload(job.payload, patch.payload);
|
||||
}
|
||||
if (!patch.delivery && patch.payload?.kind === "agentTurn") {
|
||||
// Back-compat: legacy clients still update delivery via payload fields.
|
||||
const legacyDeliveryPatch = buildLegacyDeliveryPatch(patch.payload);
|
||||
if (
|
||||
legacyDeliveryPatch &&
|
||||
job.sessionTarget === "isolated" &&
|
||||
job.payload.kind === "agentTurn"
|
||||
) {
|
||||
job.delivery = mergeCronDelivery(job.delivery, legacyDeliveryPatch);
|
||||
}
|
||||
}
|
||||
if (patch.delivery) {
|
||||
job.delivery = mergeCronDelivery(job.delivery, patch.delivery);
|
||||
}
|
||||
@@ -216,6 +227,47 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildLegacyDeliveryPatch(
|
||||
payload: Extract<CronPayloadPatch, { kind: "agentTurn" }>,
|
||||
): CronDeliveryPatch | null {
|
||||
const deliver = payload.deliver;
|
||||
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
|
||||
const hasLegacyHints =
|
||||
typeof deliver === "boolean" ||
|
||||
typeof payload.bestEffortDeliver === "boolean" ||
|
||||
Boolean(toRaw);
|
||||
if (!hasLegacyHints) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const patch: CronDeliveryPatch = {};
|
||||
let hasPatch = false;
|
||||
|
||||
if (deliver === false) {
|
||||
patch.mode = "none";
|
||||
hasPatch = true;
|
||||
} else if (deliver === true || toRaw) {
|
||||
patch.mode = "announce";
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
if (typeof payload.channel === "string") {
|
||||
const channel = payload.channel.trim().toLowerCase();
|
||||
patch.channel = channel ? channel : undefined;
|
||||
hasPatch = true;
|
||||
}
|
||||
if (typeof payload.to === "string") {
|
||||
patch.to = payload.to.trim();
|
||||
hasPatch = true;
|
||||
}
|
||||
if (typeof payload.bestEffortDeliver === "boolean") {
|
||||
patch.bestEffort = payload.bestEffortDeliver;
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
return hasPatch ? patch : null;
|
||||
}
|
||||
|
||||
function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
|
||||
if (patch.kind === "systemEvent") {
|
||||
if (typeof patch.text !== "string" || patch.text.length === 0) {
|
||||
|
||||
@@ -39,6 +39,69 @@ function buildDeliveryFromLegacyPayload(payload: Record<string, unknown>) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildDeliveryPatchFromLegacyPayload(payload: Record<string, unknown>) {
|
||||
const deliver = payload.deliver;
|
||||
const channelRaw =
|
||||
typeof payload.channel === "string" ? payload.channel.trim().toLowerCase() : "";
|
||||
const toRaw = typeof payload.to === "string" ? payload.to.trim() : "";
|
||||
const next: Record<string, unknown> = {};
|
||||
let hasPatch = false;
|
||||
|
||||
if (deliver === false) {
|
||||
next.mode = "none";
|
||||
hasPatch = true;
|
||||
} else if (deliver === true || toRaw) {
|
||||
next.mode = "announce";
|
||||
hasPatch = true;
|
||||
}
|
||||
if (channelRaw) {
|
||||
next.channel = channelRaw;
|
||||
hasPatch = true;
|
||||
}
|
||||
if (toRaw) {
|
||||
next.to = toRaw;
|
||||
hasPatch = true;
|
||||
}
|
||||
if (typeof payload.bestEffortDeliver === "boolean") {
|
||||
next.bestEffort = payload.bestEffortDeliver;
|
||||
hasPatch = true;
|
||||
}
|
||||
|
||||
return hasPatch ? next : null;
|
||||
}
|
||||
|
||||
function mergeLegacyDeliveryInto(
|
||||
delivery: Record<string, unknown>,
|
||||
payload: Record<string, unknown>,
|
||||
) {
|
||||
const patch = buildDeliveryPatchFromLegacyPayload(payload);
|
||||
if (!patch) {
|
||||
return { delivery, mutated: false };
|
||||
}
|
||||
|
||||
const next = { ...delivery };
|
||||
let mutated = false;
|
||||
|
||||
if ("mode" in patch && patch.mode !== next.mode) {
|
||||
next.mode = patch.mode;
|
||||
mutated = true;
|
||||
}
|
||||
if ("channel" in patch && patch.channel !== next.channel) {
|
||||
next.channel = patch.channel;
|
||||
mutated = true;
|
||||
}
|
||||
if ("to" in patch && patch.to !== next.to) {
|
||||
next.to = patch.to;
|
||||
mutated = true;
|
||||
}
|
||||
if ("bestEffort" in patch && patch.bestEffort !== next.bestEffort) {
|
||||
next.bestEffort = patch.bestEffort;
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
return { delivery: next, mutated };
|
||||
}
|
||||
|
||||
function stripLegacyDeliveryFields(payload: Record<string, unknown>) {
|
||||
if ("deliver" in payload) {
|
||||
delete payload.deliver;
|
||||
@@ -180,6 +243,16 @@ export async function ensureLoaded(state: CronServiceState) {
|
||||
mutated = true;
|
||||
}
|
||||
if (payloadRecord && hasLegacyDelivery) {
|
||||
if (hasDelivery) {
|
||||
const merged = mergeLegacyDeliveryInto(
|
||||
delivery as Record<string, unknown>,
|
||||
payloadRecord,
|
||||
);
|
||||
if (merged.mutated) {
|
||||
raw.delivery = merged.delivery;
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
stripLegacyDeliveryFields(payloadRecord);
|
||||
mutated = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user