diff --git a/src/gateway/protocol/cron-validators.test.ts b/src/gateway/protocol/cron-validators.test.ts new file mode 100644 index 00000000000..13fc84a5944 --- /dev/null +++ b/src/gateway/protocol/cron-validators.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { + validateCronAddParams, + validateCronRemoveParams, + validateCronRunParams, + validateCronRunsParams, + validateCronUpdateParams, +} from "./index.js"; + +const minimalAddParams = { + name: "daily-summary", + schedule: { kind: "every", everyMs: 60_000 }, + sessionTarget: "main", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "tick" }, +} as const; + +describe("cron protocol validators", () => { + it("accepts minimal add params", () => { + expect(validateCronAddParams(minimalAddParams)).toBe(true); + }); + + it("rejects add params when required scheduling fields are missing", () => { + const { wakeMode: _wakeMode, ...withoutWakeMode } = minimalAddParams; + expect(validateCronAddParams(withoutWakeMode)).toBe(false); + }); + + it("accepts update params for id and jobId selectors", () => { + expect(validateCronUpdateParams({ id: "job-1", patch: { enabled: false } })).toBe(true); + expect(validateCronUpdateParams({ jobId: "job-2", patch: { enabled: true } })).toBe(true); + }); + + it("accepts remove params for id and jobId selectors", () => { + expect(validateCronRemoveParams({ id: "job-1" })).toBe(true); + expect(validateCronRemoveParams({ jobId: "job-2" })).toBe(true); + }); + + it("accepts run params mode for id and jobId selectors", () => { + expect(validateCronRunParams({ id: "job-1", mode: "force" })).toBe(true); + expect(validateCronRunParams({ jobId: "job-2", mode: "due" })).toBe(true); + }); + + it("enforces runs limit minimum for id and jobId selectors", () => { + expect(validateCronRunsParams({ id: "job-1", limit: 1 })).toBe(true); + expect(validateCronRunsParams({ jobId: "job-2", limit: 1 })).toBe(true); + expect(validateCronRunsParams({ id: "job-1", limit: 0 })).toBe(false); + expect(validateCronRunsParams({ jobId: "job-2", limit: 0 })).toBe(false); + }); +}); diff --git a/src/gateway/protocol/schema/cron.ts b/src/gateway/protocol/schema/cron.ts index 270ca4a7421..99672b05211 100644 --- a/src/gateway/protocol/schema/cron.ts +++ b/src/gateway/protocol/schema/cron.ts @@ -19,6 +19,35 @@ function cronAgentTurnPayloadSchema(params: { message: TSchema }) { ); } +const CronSessionTargetSchema = Type.Union([Type.Literal("main"), Type.Literal("isolated")]); +const CronWakeModeSchema = Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]); +const CronCommonOptionalFields = { + agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), + sessionKey: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), + description: Type.Optional(Type.String()), + enabled: Type.Optional(Type.Boolean()), + deleteAfterRun: Type.Optional(Type.Boolean()), +}; + +function cronIdOrJobIdParams(extraFields: Record) { + return Type.Union([ + Type.Object( + { + id: NonEmptyString, + ...extraFields, + }, + { additionalProperties: false }, + ), + Type.Object( + { + jobId: NonEmptyString, + ...extraFields, + }, + { additionalProperties: false }, + ), + ]); +} + export const CronScheduleSchema = Type.Union([ Type.Object( { @@ -144,8 +173,8 @@ export const CronJobSchema = Type.Object( createdAtMs: Type.Integer({ minimum: 0 }), updatedAtMs: Type.Integer({ minimum: 0 }), schedule: CronScheduleSchema, - sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]), - wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]), + sessionTarget: CronSessionTargetSchema, + wakeMode: CronWakeModeSchema, payload: CronPayloadSchema, delivery: Type.Optional(CronDeliverySchema), state: CronJobStateSchema, @@ -165,14 +194,10 @@ export const CronStatusParamsSchema = Type.Object({}, { additionalProperties: fa export const CronAddParamsSchema = Type.Object( { name: NonEmptyString, - agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), - sessionKey: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), - description: Type.Optional(Type.String()), - enabled: Type.Optional(Type.Boolean()), - deleteAfterRun: Type.Optional(Type.Boolean()), + ...CronCommonOptionalFields, schedule: CronScheduleSchema, - sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]), - wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]), + sessionTarget: CronSessionTargetSchema, + wakeMode: CronWakeModeSchema, payload: CronPayloadSchema, delivery: Type.Optional(CronDeliverySchema), }, @@ -182,14 +207,10 @@ export const CronAddParamsSchema = Type.Object( export const CronJobPatchSchema = Type.Object( { name: Type.Optional(NonEmptyString), - agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), - sessionKey: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), - description: Type.Optional(Type.String()), - enabled: Type.Optional(Type.Boolean()), - deleteAfterRun: Type.Optional(Type.Boolean()), + ...CronCommonOptionalFields, schedule: Type.Optional(CronScheduleSchema), - sessionTarget: Type.Optional(Type.Union([Type.Literal("main"), Type.Literal("isolated")])), - wakeMode: Type.Optional(Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")])), + sessionTarget: Type.Optional(CronSessionTargetSchema), + wakeMode: Type.Optional(CronWakeModeSchema), payload: Type.Optional(CronPayloadPatchSchema), delivery: Type.Optional(CronDeliveryPatchSchema), state: Type.Optional(Type.Partial(CronJobStateSchema)), @@ -197,71 +218,19 @@ export const CronJobPatchSchema = Type.Object( { additionalProperties: false }, ); -export const CronUpdateParamsSchema = Type.Union([ - Type.Object( - { - id: NonEmptyString, - patch: CronJobPatchSchema, - }, - { additionalProperties: false }, - ), - Type.Object( - { - jobId: NonEmptyString, - patch: CronJobPatchSchema, - }, - { additionalProperties: false }, - ), -]); +export const CronUpdateParamsSchema = cronIdOrJobIdParams({ + patch: CronJobPatchSchema, +}); -export const CronRemoveParamsSchema = Type.Union([ - Type.Object( - { - id: NonEmptyString, - }, - { additionalProperties: false }, - ), - Type.Object( - { - jobId: NonEmptyString, - }, - { additionalProperties: false }, - ), -]); +export const CronRemoveParamsSchema = cronIdOrJobIdParams({}); -export const CronRunParamsSchema = Type.Union([ - Type.Object( - { - id: NonEmptyString, - mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])), - }, - { additionalProperties: false }, - ), - Type.Object( - { - jobId: NonEmptyString, - mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])), - }, - { additionalProperties: false }, - ), -]); +export const CronRunParamsSchema = cronIdOrJobIdParams({ + mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])), +}); -export const CronRunsParamsSchema = Type.Union([ - Type.Object( - { - id: NonEmptyString, - limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })), - }, - { additionalProperties: false }, - ), - Type.Object( - { - jobId: NonEmptyString, - limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })), - }, - { additionalProperties: false }, - ), -]); +export const CronRunsParamsSchema = cronIdOrJobIdParams({ + limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })), +}); export const CronRunLogEntrySchema = Type.Object( {