Skip to content

Commit ca76e2f

Browse files
brunolorenteAlec123galtaywtf
authored
fix(cron-tool): add typed properties to job/patch schemas (#55043)
Merged via squash. Prepared head SHA: 979bb0e Co-authored-by: brunolorente <[email protected]> Co-authored-by: altaywtf <[email protected]> Reviewed-by: @altaywtf
1 parent 7027dda commit ca76e2f

10 files changed

Lines changed: 1260 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
1919
- Plugins/commands: pass the active host `sessionKey` into plugin command contexts, and include `sessionId` when it is already available from the active session entry, so bundled and third-party commands can resolve the current conversation reliably. (#59044) Thanks @jalehman.
2020
- Agents/auth: honor `models.providers.*.authHeader` for pi embedded runner model requests by injecting `Authorization: Bearer <apiKey>` when requested. (#54390) Thanks @lndyzwdxhs.
2121
- UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life.
22+
- Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente.
2223

2324
## 2026.4.2
2425

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, expect, it } from "vitest";
2+
import { CronToolSchema } from "./cron-tool.js";
3+
4+
/** Walk a TypeBox schema by dot-separated property path and return sorted keys. */
5+
function keysAt(schema: Record<string, unknown>, path: string): string[] {
6+
let cursor: Record<string, unknown> | undefined = schema;
7+
for (const segment of path.split(".")) {
8+
const props = cursor?.["properties"] as Record<string, Record<string, unknown>> | undefined;
9+
cursor = props?.[segment];
10+
}
11+
const leaf = cursor?.["properties"] as Record<string, unknown> | undefined;
12+
return leaf ? Object.keys(leaf).toSorted() : [];
13+
}
14+
15+
function propertyAt(
16+
schema: Record<string, unknown>,
17+
path: string,
18+
): Record<string, unknown> | undefined {
19+
let cursor: Record<string, unknown> | undefined = schema;
20+
for (const segment of path.split(".")) {
21+
const props = cursor?.["properties"] as Record<string, Record<string, unknown>> | undefined;
22+
cursor = props?.[segment];
23+
}
24+
return cursor;
25+
}
26+
27+
describe("CronToolSchema", () => {
28+
// Regression: models like GPT-5.4 rely on these fields to populate job/patch.
29+
// If a field is removed from this list the test must be updated intentionally.
30+
31+
it("job exposes the expected top-level fields", () => {
32+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job")).toEqual(
33+
[
34+
"agentId",
35+
"deleteAfterRun",
36+
"delivery",
37+
"description",
38+
"enabled",
39+
"failureAlert",
40+
"name",
41+
"payload",
42+
"schedule",
43+
"sessionKey",
44+
"sessionTarget",
45+
"wakeMode",
46+
].toSorted(),
47+
);
48+
});
49+
50+
it("patch exposes the expected top-level fields", () => {
51+
expect(keysAt(CronToolSchema as Record<string, unknown>, "patch")).toEqual(
52+
[
53+
"agentId",
54+
"deleteAfterRun",
55+
"delivery",
56+
"description",
57+
"enabled",
58+
"failureAlert",
59+
"name",
60+
"payload",
61+
"schedule",
62+
"sessionKey",
63+
"sessionTarget",
64+
"wakeMode",
65+
].toSorted(),
66+
);
67+
});
68+
69+
it("job.schedule exposes kind, at, everyMs, anchorMs, expr, tz, staggerMs", () => {
70+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job.schedule")).toEqual(
71+
["anchorMs", "at", "everyMs", "expr", "kind", "staggerMs", "tz"].toSorted(),
72+
);
73+
});
74+
75+
it("marks staggerMs as cron-only in both job and patch schedule schemas", () => {
76+
const jobStagger = propertyAt(
77+
CronToolSchema as Record<string, unknown>,
78+
"job.schedule.staggerMs",
79+
);
80+
const patchStagger = propertyAt(
81+
CronToolSchema as Record<string, unknown>,
82+
"patch.schedule.staggerMs",
83+
);
84+
85+
expect(jobStagger?.description).toBe("Random jitter in ms (kind=cron)");
86+
expect(patchStagger?.description).toBe("Random jitter in ms (kind=cron)");
87+
});
88+
89+
it("job.delivery exposes mode, channel, to, bestEffort, accountId, failureDestination", () => {
90+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job.delivery")).toEqual(
91+
["accountId", "bestEffort", "channel", "failureDestination", "mode", "to"].toSorted(),
92+
);
93+
});
94+
95+
it("job.payload exposes kind, text, message, model, thinking and extras", () => {
96+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job.payload")).toEqual(
97+
[
98+
"allowUnsafeExternalContent",
99+
"fallbacks",
100+
"kind",
101+
"lightContext",
102+
"message",
103+
"model",
104+
"text",
105+
"thinking",
106+
"toolsAllow",
107+
"timeoutSeconds",
108+
].toSorted(),
109+
);
110+
});
111+
112+
it("job.payload includes fallbacks", () => {
113+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job.payload")).toContain("fallbacks");
114+
});
115+
116+
it("patch.payload exposes agentTurn fallback overrides", () => {
117+
expect(keysAt(CronToolSchema as Record<string, unknown>, "patch.payload")).toEqual(
118+
[
119+
"allowUnsafeExternalContent",
120+
"fallbacks",
121+
"kind",
122+
"lightContext",
123+
"message",
124+
"model",
125+
"text",
126+
"thinking",
127+
"toolsAllow",
128+
"timeoutSeconds",
129+
].toSorted(),
130+
);
131+
});
132+
133+
it("job.failureAlert exposes after, channel, to, cooldownMs, mode, accountId", () => {
134+
expect(keysAt(CronToolSchema as Record<string, unknown>, "job.failureAlert")).toEqual(
135+
["accountId", "after", "channel", "cooldownMs", "mode", "to"].toSorted(),
136+
);
137+
});
138+
139+
it("job.failureAlert also allows boolean false", () => {
140+
const root = (CronToolSchema as Record<string, unknown>).properties as
141+
| Record<string, { properties?: Record<string, unknown>; type?: unknown }>
142+
| undefined;
143+
const jobProps = root?.job?.properties as
144+
| Record<string, { type?: unknown; not?: { const?: unknown } }>
145+
| undefined;
146+
const schema = jobProps?.failureAlert;
147+
expect(schema?.type).toEqual(["object", "boolean"]);
148+
expect(schema?.not?.const).toBe(true);
149+
});
150+
151+
it("job.agentId and job.sessionKey accept null for clear/keep-unset flows", () => {
152+
const root = (CronToolSchema as Record<string, unknown>).properties as
153+
| Record<string, { properties?: Record<string, unknown> }>
154+
| undefined;
155+
const jobProps = root?.job?.properties as Record<string, { type?: unknown }> | undefined;
156+
157+
expect(jobProps?.agentId?.type).toEqual(["string", "null"]);
158+
expect(jobProps?.sessionKey?.type).toEqual(["string", "null"]);
159+
});
160+
161+
it("patch.payload.toolsAllow accepts null for clear flows", () => {
162+
const root = (CronToolSchema as Record<string, unknown>).properties as
163+
| Record<string, { properties?: Record<string, unknown> }>
164+
| undefined;
165+
const patchProps = root?.patch?.properties as
166+
| Record<string, { properties?: Record<string, { type?: unknown }> }>
167+
| undefined;
168+
169+
expect(patchProps?.payload?.properties?.toolsAllow?.type).toEqual(["array", "null"]);
170+
});
171+
});

0 commit comments

Comments
 (0)