From 793ab78ebbe3fb79055f3fcf6067e541d7add666 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 2 Jun 2026 04:07:57 +0200 Subject: [PATCH] refactor: share cron validation test helpers --- .../server-methods/cron.validation.test.ts | 171 +++++++----------- 1 file changed, 69 insertions(+), 102 deletions(-) diff --git a/src/gateway/server-methods/cron.validation.test.ts b/src/gateway/server-methods/cron.validation.test.ts index 5267c8fbe77d..56ba6c1aa34c 100644 --- a/src/gateway/server-methods/cron.validation.test.ts +++ b/src/gateway/server-methods/cron.validation.test.ts @@ -121,6 +121,19 @@ async function invokeCronUpdate(params: Record, currentJob?: Cr return await invokeCron("cron.update", params, { currentJob }); } +async function invokeCronUpdateDelivery( + delivery: Record, + currentJob = createCronJob(), +) { + return await invokeCronUpdate( + { + id: "cron-1", + patch: { delivery }, + }, + currentJob, + ); +} + async function invokeCronRemove( params: Record, options?: { removeResult?: { ok: boolean; removed: boolean } }, @@ -231,6 +244,19 @@ function slackSynologyConfig(): OpenClawConfig { } as OpenClawConfig; } +function slackConfig(params: { includeMainSession?: boolean } = {}): OpenClawConfig { + return { + ...(params.includeMainSession ? { session: { mainKey: "main" } } : {}), + channels: { + slack: { + botToken: "xoxb-slack-token", + appToken: "xapp-slack-token", + }, + }, + plugins: pluginEntries("slack"), + } as OpenClawConfig; +} + function agentTurnCronParams(overrides: Record = {}) { return { name: "cron job", @@ -280,6 +306,14 @@ function expectDeliveryFields(payload: Record, expected: Record } } +function expectCronUpdateDeliveryPatch( + context: ReturnType, + expected: unknown, +) { + expect(context.cron.update).toHaveBeenCalled(); + expect(requireCronUpdatePatch(context).delivery).toEqual(expected); +} + function expectResponseError( respond: ReturnType, expected: { code?: string; messageIncludes?: string }, @@ -299,6 +333,10 @@ function expectResponseError( } } +function expectInvalidCronPatternError(respond: ReturnType): void { + expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "CronPattern" }); +} + describe("cron method validation", () => { beforeEach(() => { getRuntimeConfig.mockReset().mockReturnValue({} as OpenClawConfig); @@ -506,13 +544,8 @@ describe("cron method validation", () => { it("validates announce delivery patches that omit mode", async () => { setRuntimeConfig(telegramSlackConfig()); - const { context, respond } = await invokeCronUpdate( - { - id: "cron-1", - patch: { - delivery: { channel: "slack", to: "telegram:123" }, - }, - }, + const { context, respond } = await invokeCronUpdateDelivery( + { channel: "slack", to: "telegram:123" }, createCronJob({ delivery: { mode: "announce", channel: "telegram", to: "123" }, }), @@ -591,8 +624,7 @@ describe("cron method validation", () => { }), ); - expect(context.cron.update).toHaveBeenCalled(); - expect(requireCronUpdatePatch(context).delivery).toEqual({ + expectCronUpdateDeliveryPatch(context, { channel: null, to: null, threadId: null, @@ -605,18 +637,13 @@ describe("cron method validation", () => { it("accepts nullable failure destination field clears on update", async () => { setRuntimeConfig(telegramSlackConfig()); - const { context, respond } = await invokeCronUpdate( + const { context, respond } = await invokeCronUpdateDelivery( { - id: "cron-1", - patch: { - delivery: { - failureDestination: { - channel: null, - to: null, - accountId: null, - mode: null, - }, - }, + failureDestination: { + channel: null, + to: null, + accountId: null, + mode: null, }, }, createCronJob({ @@ -624,8 +651,7 @@ describe("cron method validation", () => { }), ); - expect(context.cron.update).toHaveBeenCalled(); - expect(requireCronUpdatePatch(context).delivery).toEqual({ + expectCronUpdateDeliveryPatch(context, { failureDestination: { channel: null, to: null, @@ -653,15 +679,7 @@ describe("cron method validation", () => { it("rejects ambiguous announce delivery on update when multiple channels are configured", async () => { setRuntimeConfig(telegramSlackConfig({ includeMainSession: true })); - const { context, respond } = await invokeCronUpdate( - { - id: "cron-1", - patch: { - delivery: { mode: "announce" }, - }, - }, - createCronJob(), - ); + const { context, respond } = await invokeCronUpdateDelivery({ mode: "announce" }); expect(context.cron.update).not.toHaveBeenCalled(); expectResponseError(respond, { messageIncludes: "delivery.channel is required" }); @@ -713,22 +731,7 @@ describe("cron method validation", () => { }); it("does not revalidate stale delivery config for unrelated updates", async () => { - setRuntimeConfig({ - session: { - mainKey: "main", - }, - channels: { - slack: { - botToken: "xoxb-slack-token", - appToken: "xapp-slack-token", - }, - }, - plugins: { - entries: { - slack: { enabled: true }, - }, - }, - }); + setRuntimeConfig(slackConfig({ includeMainSession: true })); const { context, respond } = await invokeCronUpdate( { @@ -747,18 +750,7 @@ describe("cron method validation", () => { }); it("rejects target ids mistakenly supplied as delivery.channel providers", async () => { - setRuntimeConfig({ - session: { - mainKey: "main", - }, - channels: { - slack: { - botToken: "xoxb-slack-token", - appToken: "xapp-slack-token", - }, - }, - plugins: pluginEntries("slack"), - } as OpenClawConfig); + setRuntimeConfig(slackConfig({ includeMainSession: true })); const { context, respond } = await invokeCronAdd( agentTurnCronParams({ @@ -791,7 +783,7 @@ describe("cron method validation", () => { { context }, ); - expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "CronPattern" }); + expectInvalidCronPatternError(respond); }); it("returns INVALID_REQUEST when cron.add rejects an incompatible main agent", async () => { @@ -801,10 +793,9 @@ describe("cron method validation", () => { 'cron: sessionTarget "main" is only valid for the default agent. Use sessionTarget "isolated" with payload.kind "agentTurn" for non-default agents (agentId: worker)', ), ); - const respond = vi.fn(); - await cronHandlers["cron.add"]({ - req: {} as never, - params: { + const { respond } = await invokeCron( + "cron.add", + { name: "bad-main-agent", enabled: true, schedule: { kind: "every", everyMs: 60_000 }, @@ -812,12 +803,9 @@ describe("cron method validation", () => { wakeMode: "next-heartbeat", payload: { kind: "systemEvent", text: "ping" }, agentId: "worker", - } as never, - respond: respond as never, - context: context as never, - client: null, - isWebchatConnect: () => false, - }); + }, + { context }, + ); expectResponseError(respond, { code: "INVALID_REQUEST", @@ -842,7 +830,7 @@ describe("cron method validation", () => { { context }, ); - expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "CronPattern" }); + expectInvalidCronPatternError(respond); }); it("returns INVALID_REQUEST when cron.update cannot find the job", async () => { @@ -882,15 +870,7 @@ describe("cron method validation", () => { it("returns INVALID_REQUEST when cron.run cannot find the job", async () => { const context = createCronContext(); context.cron.enqueueRun.mockRejectedValueOnce(new Error("unknown cron job id: missing")); - const respond = vi.fn(); - await cronHandlers["cron.run"]({ - req: {} as never, - params: { id: "missing" } as never, - respond: respond as never, - context: context as never, - client: null, - isWebchatConnect: () => false, - }); + const { respond } = await invokeCron("cron.run", { id: "missing" }, { context }); expectResponseError(respond, { code: "INVALID_REQUEST", @@ -945,11 +925,18 @@ describe("cron method validation", () => { expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined); }); - it("rejects empty-string sessionKey at schema", async () => { + it.each([ + { name: "empty-string sessionKey at schema", sessionKey: "" }, + { name: "non-string sessionKey at schema", sessionKey: 42 }, + { + name: "subagent sessionKey targets before enqueueing", + sessionKey: "agent:main:subagent:worker", + }, + ])("rejects $name", async ({ sessionKey }) => { const { context, respond } = await invokeWake({ mode: "now", text: "ping", - sessionKey: "", + sessionKey, }); expect(context.cron.wake).not.toHaveBeenCalled(); expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "sessionKey" }); @@ -967,25 +954,5 @@ describe("cron method validation", () => { }); expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined); }); - - it("rejects non-string sessionKey at schema", async () => { - const { context, respond } = await invokeWake({ - mode: "now", - text: "ping", - sessionKey: 42, - }); - expect(context.cron.wake).not.toHaveBeenCalled(); - expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "sessionKey" }); - }); - - it("rejects subagent sessionKey targets before enqueueing", async () => { - const { context, respond } = await invokeWake({ - mode: "now", - text: "ping", - sessionKey: "agent:main:subagent:worker", - }); - expect(context.cron.wake).not.toHaveBeenCalled(); - expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "sessionKey" }); - }); }); });