fix(cron): land #31145 explicit delivery none in editor (@byungsker)

Landed from contributor PR #31145 by @byungsker.

Co-authored-by: byungsker <byungsker@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-02 02:29:42 +00:00
parent 1da7906a5d
commit 5850045df6
3 changed files with 105 additions and 16 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
- Windows/Plugin install: avoid `spawn EINVAL` on Windows npm/npx invocations by resolving to `node` + npm CLI scripts instead of spawning `.cmd` directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.
- LINE/Voice transcription: classify M4A voice media as `audio/mp4` (not `video/mp4`) by checking the MPEG-4 `ftyp` major brand (`M4A ` / `M4B `), restoring voice transcription for LINE voice messages. Landed from contributor PR #31151 by @scoootscooob. Thanks @scoootscooob.
- Cron/Delivery mode none: send explicit `delivery: { mode: "none" }` from cron editor for both add and update flows so previous announce delivery is actually cleared. Landed from contributor PR #31145 by @byungsker. Thanks @byungsker.
- Agents/Thinking fallback: when providers reject unsupported thinking levels without enumerating alternatives, retry with `think=off` to avoid hard failure during model/provider fallback chains. Landed from contributor PR #31002 by @yfge. Thanks @yfge.
- Agents/Failover reason classification: avoid false rate-limit classification from incidental `tpm` substrings by matching TPM as a standalone token/phrase and keeping auth-context errors on the auth path. Landed from contributor PR #31007 by @HOYALIM. Thanks @HOYALIM.
- Slack/Announce target account routing: enable session-backed announce-target lookup for Slack so multi-account announces resolve the correct `accountId` instead of defaulting to bot-token context. Landed from contributor PR #31028 by @taw0002. Thanks @taw0002.

View File

@@ -119,6 +119,91 @@ describe("cron controller", () => {
});
});
it('sends delivery: { mode: "none" } explicitly in cron.add payload', async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "cron.add") {
return { id: "job-none-add" };
}
if (method === "cron.list") {
return { jobs: [] };
}
if (method === "cron.status") {
return { enabled: true, jobs: 0, nextWakeAtMs: null };
}
return {};
});
const state = createState({
client: {
request,
} as unknown as CronState["client"],
cronForm: {
...DEFAULT_CRON_FORM,
name: "none delivery job",
scheduleKind: "every",
everyAmount: "1",
everyUnit: "minutes",
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payloadKind: "agentTurn",
payloadText: "run this",
deliveryMode: "none",
},
});
await addCronJob(state);
const addCall = request.mock.calls.find(([method]) => method === "cron.add");
expect(addCall).toBeDefined();
expect((addCall?.[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({
mode: "none",
});
});
it('sends delivery: { mode: "none" } explicitly in cron.update patch', async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "cron.update") {
return { id: "job-none-update" };
}
if (method === "cron.list") {
return { jobs: [{ id: "job-none-update" }] };
}
if (method === "cron.status") {
return { enabled: true, jobs: 1, nextWakeAtMs: null };
}
return {};
});
const state = createState({
client: {
request,
} as unknown as CronState["client"],
cronEditingJobId: "job-none-update",
cronForm: {
...DEFAULT_CRON_FORM,
name: "switch to none",
scheduleKind: "every",
everyAmount: "30",
everyUnit: "minutes",
sessionTarget: "isolated",
wakeMode: "next-heartbeat",
payloadKind: "agentTurn",
payloadText: "do work",
deliveryMode: "none",
},
});
await addCronJob(state);
const updateCall = request.mock.calls.find(([method]) => method === "cron.update");
expect(updateCall).toBeDefined();
expect(
(updateCall?.[1] as { patch?: { delivery?: unknown } } | undefined)?.patch?.delivery,
).toEqual({
mode: "none",
});
});
it("does not submit stale announce delivery when unsupported", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "cron.add") {
@@ -159,8 +244,13 @@ describe("cron controller", () => {
expect(addCall?.[1]).toMatchObject({
name: "main job",
});
expect((addCall?.[1] as { delivery?: unknown } | undefined)?.delivery).toBeUndefined();
expect(state.cronForm.deliveryMode).toBe("none");
// Delivery is explicitly sent as { mode: "none" } to clear the announce delivery on the backend.
// Previously this was sent as undefined, which left announce in place (bug #31075).
expect((addCall?.[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({
mode: "none",
});
// After submit, form is reset to defaults (deliveryMode = "announce" from DEFAULT_CRON_FORM).
expect(state.cronForm.deliveryMode).toBe("announce");
});
it("submits cron.update when editing an existing job", async () => {

View File

@@ -614,20 +614,18 @@ export async function addCronJob(state: CronState) {
const payload = buildCronPayload(form);
const selectedDeliveryMode = form.deliveryMode;
const delivery =
selectedDeliveryMode === "none"
? state.cronEditingJobId
? { mode: "none" as const }
: undefined
: selectedDeliveryMode
? {
mode: selectedDeliveryMode,
channel:
selectedDeliveryMode === "announce"
? form.deliveryChannel.trim() || "last"
: undefined,
to: form.deliveryTo.trim() || undefined,
bestEffort: form.deliveryBestEffort,
}
selectedDeliveryMode && selectedDeliveryMode !== "none"
? {
mode: selectedDeliveryMode,
channel:
selectedDeliveryMode === "announce"
? form.deliveryChannel.trim() || "last"
: undefined,
to: form.deliveryTo.trim() || undefined,
bestEffort: form.deliveryBestEffort,
}
: selectedDeliveryMode === "none"
? ({ mode: "none" } as const)
: undefined;
const failureAlert = buildFailureAlert(form);
const agentId = form.clearAgent ? null : form.agentId.trim();