






















@@ -130,6 +130,58 @@ function createCronJob(overrides: Partial<CronJob> = {}): CronJob {
130130};
131131}
132132133+function requireRecord(value: unknown, label: string): Record<string, unknown> {
134+if (!value || typeof value !== "object" || Array.isArray(value)) {
135+throw new Error(`expected ${label} to be an object`);
136+}
137+return value as Record<string, unknown>;
138+}
139+140+function requireCronAddPayload(
141+context: ReturnType<typeof createCronContext>,
142+): Record<string, unknown> {
143+const calls = context.cron.add.mock.calls as unknown as [unknown][];
144+return requireRecord(calls[0]?.[0], "cron.add payload");
145+}
146+147+function requireCronUpdatePatch(
148+context: ReturnType<typeof createCronContext>,
149+): Record<string, unknown> {
150+const calls = context.cron.update.mock.calls as unknown as [unknown, unknown][];
151+return requireRecord(calls[0]?.[1], "cron.update patch");
152+}
153+154+function requireCronUpdateId(context: ReturnType<typeof createCronContext>): unknown {
155+const calls = context.cron.update.mock.calls as unknown as [unknown, unknown][];
156+return calls[0]?.[0];
157+}
158+159+function expectDeliveryFields(payload: Record<string, unknown>, expected: Record<string, unknown>) {
160+const delivery = requireRecord(payload.delivery, "delivery");
161+for (const [key, value] of Object.entries(expected)) {
162+expect(delivery[key]).toBe(value);
163+}
164+}
165+166+function expectResponseError(
167+respond: ReturnType<typeof vi.fn>,
168+expected: { code?: string; messageIncludes?: string },
169+) {
170+const call = respond.mock.calls[0];
171+if (!call) {
172+throw new Error("expected response call");
173+}
174+expect(call[0]).toBe(false);
175+expect(call[1]).toBeUndefined();
176+const error = requireRecord(call[2], "response error");
177+if (expected.code) {
178+expect(error.code).toBe(expected.code);
179+}
180+if (expected.messageIncludes) {
181+expect(String(error.message)).toContain(expected.messageIncludes);
182+}
183+}
184+133185describe("cron method validation", () => {
134186beforeEach(() => {
135187getRuntimeConfig.mockReset().mockReturnValue({} as OpenClawConfig);
@@ -169,16 +221,12 @@ describe("cron method validation", () => {
169221},
170222});
171223172-expect(context.cron.add).toHaveBeenCalledWith(
173-expect.objectContaining({
174-delivery: expect.objectContaining({
175-mode: "announce",
176-channel: "telegram",
177-to: "-1001234567890",
178-threadId: 123,
179-}),
180-}),
181-);
224+expectDeliveryFields(requireCronAddPayload(context), {
225+mode: "announce",
226+channel: "telegram",
227+to: "-1001234567890",
228+threadId: 123,
229+});
182230expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
183231});
184232@@ -213,17 +261,13 @@ describe("cron method validation", () => {
213261}),
214262);
215263216-expect(context.cron.update).toHaveBeenCalledWith(
217-"cron-1",
218-expect.objectContaining({
219-delivery: expect.objectContaining({
220-mode: "announce",
221-channel: "telegram",
222-to: "-1001234567890",
223-threadId: "456",
224-}),
225-}),
226-);
264+expect(requireCronUpdateId(context)).toBe("cron-1");
265+expectDeliveryFields(requireCronUpdatePatch(context), {
266+mode: "announce",
267+channel: "telegram",
268+to: "-1001234567890",
269+threadId: "456",
270+});
227271expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
228272});
229273@@ -251,13 +295,7 @@ describe("cron method validation", () => {
251295);
252296253297expect(context.cron.update).not.toHaveBeenCalled();
254-expect(respond).toHaveBeenCalledWith(
255-false,
256-undefined,
257-expect.objectContaining({
258-code: "INVALID_REQUEST",
259-}),
260-);
298+expectResponseError(respond, { code: "INVALID_REQUEST" });
261299});
262300263301it("rejects ambiguous announce delivery on add when multiple channels are configured", async () => {
@@ -293,13 +331,7 @@ describe("cron method validation", () => {
293331});
294332295333expect(context.cron.add).not.toHaveBeenCalled();
296-expect(respond).toHaveBeenCalledWith(
297-false,
298-undefined,
299-expect.objectContaining({
300-message: expect.stringContaining("delivery.channel is required"),
301-}),
302-);
334+expectResponseError(respond, { messageIncludes: "delivery.channel is required" });
303335});
304336305337it("accepts provider-prefixed announce target without delivery.channel when multiple channels are configured", async () => {
@@ -368,13 +400,7 @@ describe("cron method validation", () => {
368400});
369401370402expect(context.cron.add).not.toHaveBeenCalled();
371-expect(respond).toHaveBeenCalledWith(
372-false,
373-undefined,
374-expect.objectContaining({
375-message: expect.stringContaining("belongs to telegram, not slack"),
376-}),
377-);
403+expectResponseError(respond, { messageIncludes: "belongs to telegram, not slack" });
378404});
379405380406it("accepts provider-prefixed announce targets when delivery.channel uses a channel alias", async () => {
@@ -443,13 +469,7 @@ describe("cron method validation", () => {
443469);
444470445471expect(context.cron.update).not.toHaveBeenCalled();
446-expect(respond).toHaveBeenCalledWith(
447-false,
448-undefined,
449-expect.objectContaining({
450-message: expect.stringContaining("belongs to telegram, not slack"),
451-}),
452-);
472+expectResponseError(respond, { messageIncludes: "belongs to telegram, not slack" });
453473});
454474455475it("rejects underscored provider prefixes for a different explicit delivery channel", async () => {
@@ -482,13 +502,7 @@ describe("cron method validation", () => {
482502});
483503484504expect(context.cron.add).not.toHaveBeenCalled();
485-expect(respond).toHaveBeenCalledWith(
486-false,
487-undefined,
488-expect.objectContaining({
489-message: expect.stringContaining("belongs to synology-chat, not slack"),
490-}),
491-);
505+expectResponseError(respond, { messageIncludes: "belongs to synology-chat, not slack" });
492506});
493507494508it("rejects ambiguous announce delivery on update when multiple channels are configured", async () => {
@@ -524,13 +538,7 @@ describe("cron method validation", () => {
524538);
525539526540expect(context.cron.update).not.toHaveBeenCalled();
527-expect(respond).toHaveBeenCalledWith(
528-false,
529-undefined,
530-expect.objectContaining({
531-message: expect.stringContaining("delivery.channel is required"),
532-}),
533-);
541+expectResponseError(respond, { messageIncludes: "delivery.channel is required" });
534542});
535543536544it("rejects target ids mistakenly supplied as delivery.channel providers", async () => {
@@ -566,13 +574,7 @@ describe("cron method validation", () => {
566574});
567575568576expect(context.cron.add).not.toHaveBeenCalled();
569-expect(respond).toHaveBeenCalledWith(
570-false,
571-undefined,
572-expect.objectContaining({
573-message: expect.stringContaining("delivery.channel must be one of: slack"),
574-}),
575-);
577+expectResponseError(respond, { messageIncludes: "delivery.channel must be one of: slack" });
576578});
577579578580it("returns INVALID_REQUEST when cron.add throws a croner parse error (#74066)", async () => {
@@ -595,14 +597,7 @@ describe("cron method validation", () => {
595597isWebchatConnect: () => false,
596598});
597599598-expect(respond).toHaveBeenCalledWith(
599-false,
600-undefined,
601-expect.objectContaining({
602-code: "INVALID_REQUEST",
603-message: expect.stringContaining("CronPattern"),
604-}),
605-);
600+expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "CronPattern" });
606601});
607602608603it("returns INVALID_REQUEST when cron.update throws a croner parse error (#74066)", async () => {
@@ -626,14 +621,7 @@ describe("cron method validation", () => {
626621isWebchatConnect: () => false,
627622});
628623629-expect(respond).toHaveBeenCalledWith(
630-false,
631-undefined,
632-expect.objectContaining({
633-code: "INVALID_REQUEST",
634-message: expect.stringContaining("CronPattern"),
635-}),
636-);
624+expectResponseError(respond, { code: "INVALID_REQUEST", messageIncludes: "CronPattern" });
637625});
638626639627it("re-throws non-parse errors from cron.add instead of masking as INVALID_REQUEST", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。