fix(cron): recover flat patch params for update action and fix schema (openclaw#23221) thanks @charojo

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: charojo <4084797+charojo@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
charo
2026-03-01 20:50:51 -05:00
committed by GitHub
parent a779c2ca6a
commit 757e09fe43
3 changed files with 103 additions and 17 deletions

View File

@@ -512,4 +512,50 @@ describe("cron tool", () => {
).rejects.toThrow('delivery.mode="webhook" requires delivery.to to be a valid http(s) URL');
expect(callGatewayMock).toHaveBeenCalledTimes(0);
});
it("recovers flat patch params for update action", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool();
await tool.execute("call-update-flat", {
action: "update",
jobId: "job-1",
name: "new-name",
enabled: false,
});
expect(callGatewayMock).toHaveBeenCalledTimes(1);
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: { id?: string; patch?: { name?: string; enabled?: boolean } };
};
expect(call.method).toBe("cron.update");
expect(call.params?.id).toBe("job-1");
expect(call.params?.patch?.name).toBe("new-name");
expect(call.params?.patch?.enabled).toBe(false);
});
it("recovers additional flat patch params for update action", async () => {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const tool = createCronTool();
await tool.execute("call-update-flat-extra", {
action: "update",
id: "job-2",
sessionTarget: "main",
failureAlert: { after: 3, cooldownMs: 60_000 },
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
method?: string;
params?: {
id?: string;
patch?: { sessionTarget?: string; failureAlert?: { after?: number; cooldownMs?: number } };
};
};
expect(call.method).toBe("cron.update");
expect(call.params?.id).toBe("job-2");
expect(call.params?.patch?.sessionTarget).toBe("main");
expect(call.params?.patch?.failureAlert).toEqual({ after: 3, cooldownMs: 60_000 });
});
});

View File

@@ -28,23 +28,26 @@ const REMINDER_CONTEXT_TOTAL_MAX = 700;
const REMINDER_CONTEXT_MARKER = "\n\nRecent context:\n";
// Flattened schema: runtime validates per-action requirements.
const CronToolSchema = Type.Object({
action: stringEnum(CRON_ACTIONS),
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
includeDisabled: Type.Optional(Type.Boolean()),
job: Type.Optional(Type.Object({}, { additionalProperties: true })),
jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
patch: Type.Optional(Type.Object({}, { additionalProperties: true })),
text: Type.Optional(Type.String()),
mode: optionalStringEnum(CRON_WAKE_MODES),
runMode: optionalStringEnum(CRON_RUN_MODES),
contextMessages: Type.Optional(
Type.Number({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }),
),
});
const CronToolSchema = Type.Object(
{
action: stringEnum(CRON_ACTIONS),
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
includeDisabled: Type.Optional(Type.Boolean()),
job: Type.Optional(Type.Object({}, { additionalProperties: true })),
jobId: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
patch: Type.Optional(Type.Object({}, { additionalProperties: true })),
text: Type.Optional(Type.String()),
mode: optionalStringEnum(CRON_WAKE_MODES),
runMode: optionalStringEnum(CRON_RUN_MODES),
contextMessages: Type.Optional(
Type.Number({ minimum: 0, maximum: REMINDER_CONTEXT_MESSAGES_MAX }),
),
},
{ additionalProperties: true },
);
type CronToolOptions = {
agentSessionKey?: string;
@@ -435,6 +438,42 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
if (!id) {
throw new Error("jobId required (id accepted for backward compatibility)");
}
// Flat-params recovery for patch
if (
!params.patch ||
(typeof params.patch === "object" &&
params.patch !== null &&
Object.keys(params.patch as Record<string, unknown>).length === 0)
) {
const PATCH_KEYS: ReadonlySet<string> = new Set([
"name",
"schedule",
"payload",
"delivery",
"enabled",
"description",
"deleteAfterRun",
"agentId",
"sessionKey",
"sessionTarget",
"wakeMode",
"failureAlert",
"allowUnsafeExternalContent",
]);
const synthetic: Record<string, unknown> = {};
let found = false;
for (const key of Object.keys(params)) {
if (PATCH_KEYS.has(key) && params[key] !== undefined) {
synthetic[key] = params[key];
found = true;
}
}
if (found) {
params.patch = synthetic;
}
}
if (!params.patch || typeof params.patch !== "object") {
throw new Error("patch required");
}