mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 23:01:08 +00:00
fix(cron): preserve session-scoped failure fallback delivery
This commit is contained in:
@@ -101,6 +101,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Exec approvals/node host: forward prepared `system.run` approval plans on the async node invoke path so mutable script operands keep their approval-time binding and drift revalidation instead of dropping back to unbound execution.
|
||||
- Synology Chat/security: default low-level HTTPS helper TLS verification to on so helper/API defaults match the shipped safe account default, and only explicit `allowInsecureSsl: true` opts out.
|
||||
- Android/canvas security: require exact normalized A2UI URL matches before forwarding canvas bridge actions, rejecting query mismatches and descendant paths while still allowing fragment-only A2UI navigation.
|
||||
- Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit `failureDestination` is configured. (#60622) Thanks @artwalker.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -95,6 +95,27 @@ describe("sendFailureNotificationAnnounce", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes sessionKey through to delivery-target resolution", async () => {
|
||||
await sendFailureNotificationAnnounce(
|
||||
{} as never,
|
||||
{} as never,
|
||||
"main",
|
||||
"job-1",
|
||||
{
|
||||
channel: "telegram",
|
||||
sessionKey: "agent:main:telegram:direct:123:thread:99",
|
||||
},
|
||||
"Cron failed",
|
||||
);
|
||||
|
||||
expect(mocks.resolveDeliveryTarget).toHaveBeenCalledWith({} as never, "main", {
|
||||
channel: "telegram",
|
||||
to: undefined,
|
||||
accountId: undefined,
|
||||
sessionKey: "agent:main:telegram:direct:123:thread:99",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not send when target resolution fails", async () => {
|
||||
mocks.resolveDeliveryTarget.mockResolvedValue({
|
||||
ok: false,
|
||||
|
||||
@@ -32,13 +32,14 @@ export async function sendFailureNotificationAnnounce(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
jobId: string,
|
||||
target: { channel?: string; to?: string; accountId?: string },
|
||||
target: { channel?: string; to?: string; accountId?: string; sessionKey?: string },
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
const resolvedTarget = await resolveDeliveryTarget(cfg, agentId, {
|
||||
channel: target.channel as CronMessageChannel | undefined,
|
||||
to: target.to,
|
||||
accountId: target.accountId,
|
||||
sessionKey: target.sessionKey,
|
||||
});
|
||||
|
||||
if (!resolvedTarget.ok) {
|
||||
|
||||
@@ -476,6 +476,7 @@ export function buildGatewayCronService(params: {
|
||||
channel: failureDest.channel,
|
||||
to: failureDest.to,
|
||||
accountId: failureDest.accountId,
|
||||
sessionKey: job.sessionKey,
|
||||
},
|
||||
`⚠️ ${failureMessage}`,
|
||||
);
|
||||
@@ -494,6 +495,7 @@ export function buildGatewayCronService(params: {
|
||||
channel: primaryPlan.channel,
|
||||
to: primaryPlan.to,
|
||||
accountId: primaryPlan.accountId,
|
||||
sessionKey: job.sessionKey,
|
||||
},
|
||||
`⚠️ ${failureMessage}`,
|
||||
);
|
||||
|
||||
@@ -24,6 +24,8 @@ const fetchWithSsrFGuardMock = vi.hoisted(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const sendFailureNotificationAnnounceMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
|
||||
vi.mock("../infra/net/fetch-guard.js", () => ({
|
||||
fetchWithSsrFGuard: (...args: unknown[]) =>
|
||||
(
|
||||
@@ -35,6 +37,17 @@ vi.mock("../infra/net/fetch-guard.js", () => ({
|
||||
)(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../cron/delivery.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../cron/delivery.js")>("../cron/delivery.js");
|
||||
return {
|
||||
...actual,
|
||||
sendFailureNotificationAnnounce: (...args: unknown[]) =>
|
||||
(
|
||||
sendFailureNotificationAnnounceMock as unknown as (...innerArgs: unknown[]) => Promise<void>
|
||||
)(...args),
|
||||
};
|
||||
});
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
const CRON_WAIT_TIMEOUT_MS = 3_000;
|
||||
const EMPTY_CRON_STORE_CONTENT = JSON.stringify({ version: 1, jobs: [] });
|
||||
@@ -232,6 +245,7 @@ describe("gateway server cron", () => {
|
||||
beforeEach(() => {
|
||||
// Keep polling helpers deterministic even if other tests left fake timers enabled.
|
||||
vi.useRealTimers();
|
||||
sendFailureNotificationAnnounceMock.mockClear();
|
||||
});
|
||||
|
||||
test("handles cron CRUD, normalization, and patch semantics", { timeout: 20_000 }, async () => {
|
||||
@@ -976,6 +990,61 @@ describe("gateway server cron", () => {
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
test("falls back to the primary delivery channel on job failure and preserves sessionKey", async () => {
|
||||
const { prevSkipCron } = await setupCronTestRun({
|
||||
tempPrefix: "openclaw-gw-cron-failure-primary-fallback-",
|
||||
cronEnabled: false,
|
||||
});
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
try {
|
||||
cronIsolatedRun.mockResolvedValueOnce({ status: "error", summary: "delivery failed" });
|
||||
const jobId = await addWebhookCronJob({
|
||||
ws,
|
||||
name: "primary delivery fallback",
|
||||
sessionTarget: "isolated",
|
||||
delivery: {
|
||||
mode: "announce",
|
||||
channel: "last",
|
||||
},
|
||||
});
|
||||
|
||||
const updateRes = await rpcReq(ws, "cron.update", {
|
||||
id: jobId,
|
||||
patch: {
|
||||
sessionKey: "agent:main:telegram:direct:123:thread:99",
|
||||
},
|
||||
});
|
||||
expect(updateRes.ok).toBe(true);
|
||||
|
||||
const finished = waitForCronEvent(
|
||||
ws,
|
||||
(payload) => payload?.jobId === jobId && payload?.action === "finished",
|
||||
);
|
||||
await runCronJobForce(ws, jobId);
|
||||
await finished;
|
||||
|
||||
expect(sendFailureNotificationAnnounceMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendFailureNotificationAnnounceMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.any(String),
|
||||
jobId,
|
||||
{
|
||||
channel: "last",
|
||||
to: undefined,
|
||||
accountId: undefined,
|
||||
sessionKey: "agent:main:telegram:direct:123:thread:99",
|
||||
},
|
||||
'⚠️ Cron job "primary delivery fallback" failed: unknown error',
|
||||
);
|
||||
} finally {
|
||||
await cleanupCronTestRun({ ws, server, prevSkipCron });
|
||||
}
|
||||
}, 45_000);
|
||||
|
||||
test("ignores non-string cron.webhookToken values without crashing webhook delivery", async () => {
|
||||
const { prevSkipCron } = await setupCronTestRun({
|
||||
tempPrefix: "openclaw-gw-cron-webhook-secretinput-",
|
||||
|
||||
Reference in New Issue
Block a user