diff --git a/src/gateway/server-methods/cron.ts b/src/gateway/server-methods/cron.ts index 6a0c0d28eb0e..def56fb1af5a 100644 --- a/src/gateway/server-methods/cron.ts +++ b/src/gateway/server-methods/cron.ts @@ -1,5 +1,6 @@ // Gateway RPC handlers for cron job CRUD, run logs, wake, and delivery previews. import { parseBoolean } from "@openclaw/normalization-core/boolean-coercion"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { ErrorCodes, errorShape, @@ -318,7 +319,8 @@ function assertCronDoesNotTargetAgentHarness(input: { } function resolveCronJobId(params: CronJobIdParams): string | undefined { - return params.id ?? params.jobId; + // Exact store lookups; clipboard/UI padding must not fake "id not found". + return normalizeOptionalString(params.id ?? params.jobId); } function respondInvalidCronParams(respond: RespondFn, method: string, reason: string): void { @@ -783,7 +785,7 @@ export const cronHandlers: GatewayRequestHandlers = { expectedConfigRevision?: string; }; const callerScope = readCronCallerScope(client); - const jobId = p.id ?? p.jobId; + const jobId = resolveCronJobId(p); if (!jobId) { respond( false, @@ -1026,8 +1028,9 @@ export const cronHandlers: GatewayRequestHandlers = { const p = params as CronRunsRequestParams; const callerScope = readCronCallerScope(client); const explicitScope = p.scope; + const hasJobSelector = p.id !== undefined || p.jobId !== undefined; const jobId = resolveCronJobId(p); - const scope: "job" | "all" = explicitScope ?? (jobId ? "job" : "all"); + const scope: "job" | "all" = explicitScope ?? (hasJobSelector ? "job" : "all"); if (scope === "job" && !jobId) { respondMissingCronJobId(respond, "cron.runs"); return; diff --git a/src/gateway/server-methods/cron.validation.test.ts b/src/gateway/server-methods/cron.validation.test.ts index a341d34f5a9e..6d0f59cd622e 100644 --- a/src/gateway/server-methods/cron.validation.test.ts +++ b/src/gateway/server-methods/cron.validation.test.ts @@ -575,6 +575,31 @@ describe("cron method validation", () => { }); }); + it("trims whitespace around cron.get job ids before lookup", async () => { + const job = createCronJob({ id: "cron-42" }); + const { context, respond } = await invokeCronGet({ jobId: " cron-42 " }, job); + + expect(context.cron.readJob).toHaveBeenCalledWith("cron-42"); + expectCronReadSuccess(respond, job); + }); + + it("trims whitespace around cron.run job ids before lookup", async () => { + const job = createCronJob({ id: "cron-42" }); + const { context, respond } = await invokeCron( + "cron.run", + { id: " cron-42 " }, + { currentJob: job }, + ); + + expect(context.cron.readJob).toHaveBeenCalledWith("cron-42"); + expect(context.cron.enqueueRun).toHaveBeenCalledWith("cron-42", "force"); + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ ok: true, enqueued: true, runId: "run-1" }), + undefined, + ); + }); + it("returns a single cron job for cron.get", async () => { const job = createCronJob({ id: "cron-42", name: "single job" }); @@ -1837,6 +1862,20 @@ describe("cron method validation", () => { expectCronSuccess(respond); }); + it("trims whitespace around legacy cron.update job ids before lookup", async () => { + const { context, respond } = await invokeCronUpdate( + { + jobId: " cron-1 ", + patch: { enabled: false }, + }, + createCronJob(), + ); + + expect(context.cron.readJob).toHaveBeenCalledWith("cron-1"); + expect(context.cron.update).toHaveBeenCalledWith("cron-1", { enabled: false }); + expectCronSuccess(respond); + }); + it("allows cron.update to clear a display name", async () => { const { context, respond } = await invokeCronUpdate( { id: "cron-1", patch: { displayName: null } }, @@ -3254,6 +3293,18 @@ describe("cron method validation", () => { }); }); + it("does not widen a whitespace-only cron.runs selector to all history", async () => { + const context = createCronContext(); + + const { respond } = await invokeCron("cron.runs", { id: " " }, { context }); + + expect(context.cron.list).not.toHaveBeenCalled(); + expectResponseError(respond, { + code: "INVALID_REQUEST", + messageIncludes: "invalid cron.runs params: missing id", + }); + }); + it("hides caller-scoped cron.runs for a foreign job", async () => { const context = createCronContext(createCronJob({ id: "cron-1", agentId: "worker" }));