openclaw/src/gateway/protocol/cron-validators.test.ts
kkhomej33-netizen e7d9648fba
feat(cron): support custom session IDs and auto-bind to current session (#16511)
feat(cron): support persistent session targets for cron jobs (#9765)

Add support for `sessionTarget: "current"` and `session:<id>` so cron jobs can
bind to the creating session or a persistent named session instead of only
`main` or ephemeral `isolated` sessions.

Also:
- preserve custom session targets across reloads and restarts
- update gateway validation and normalization for the new target forms
- add cron coverage for current/custom session targets and fallback behavior
- fix merged CI regressions in Discord and diffs tests
- add a changelog entry for the new cron session behavior

Co-authored-by: kkhomej33-netizen <kkhomej33-netizen@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
2026-03-14 16:48:46 +11:00

129 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
validateCronAddParams,
validateCronListParams,
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("accepts current and custom session targets", () => {
expect(
validateCronAddParams({
...minimalAddParams,
sessionTarget: "current",
payload: { kind: "agentTurn", message: "tick" },
}),
).toBe(true);
expect(
validateCronAddParams({
...minimalAddParams,
sessionTarget: "session:project-alpha",
payload: { kind: "agentTurn", message: "tick" },
}),
).toBe(true);
expect(
validateCronUpdateParams({
id: "job-1",
patch: { sessionTarget: "session:project-alpha" },
}),
).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("accepts list paging/filter/sort params", () => {
expect(
validateCronListParams({
includeDisabled: true,
limit: 50,
offset: 0,
query: "daily",
enabled: "all",
sortBy: "nextRunAtMs",
sortDir: "asc",
}),
).toBe(true);
expect(validateCronListParams({ offset: -1 })).toBe(false);
});
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);
});
it("rejects cron.runs path traversal ids", () => {
expect(validateCronRunsParams({ id: "../job-1" })).toBe(false);
expect(validateCronRunsParams({ id: "nested/job-1" })).toBe(false);
expect(validateCronRunsParams({ jobId: "..\\job-2" })).toBe(false);
expect(validateCronRunsParams({ jobId: "nested\\job-2" })).toBe(false);
});
it("accepts runs paging/filter/sort params", () => {
expect(
validateCronRunsParams({
id: "job-1",
limit: 50,
offset: 0,
status: "error",
query: "timeout",
sortDir: "desc",
}),
).toBe(true);
expect(validateCronRunsParams({ id: "job-1", offset: -1 })).toBe(false);
});
it("accepts all-scope runs with multi-select filters", () => {
expect(
validateCronRunsParams({
scope: "all",
limit: 25,
statuses: ["ok", "error"],
deliveryStatuses: ["delivered", "not-requested"],
query: "fail",
sortDir: "desc",
}),
).toBe(true);
expect(
validateCronRunsParams({
scope: "job",
statuses: [],
}),
).toBe(false);
});
});