fix(cron): accept null fallbacks in update patch payload (#100707) (#100801)

* fix(cron): accept null fallbacks in cron update patch payload

createCronPatchObjectSchema passes nullableStringArraySchema for model and
toolsAllow, but fallbacks was still hardcoded as a non-nullable array in
cronPayloadObjectSchema, rejecting null clears before they reach the
Gateway service layer.

Switch fallbacks to a parameter so the patch context can pass the nullable
variant, matching the existing pattern for model and toolsAllow.

Fixes #100707

Co-Authored-By: Claude <noreply@anthropic.com>

* test(cron): cover model+fallbacks null clear in patch schema test

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
SunnyShu
2026-07-06 21:48:00 +08:00
committed by GitHub
parent e35c7a3c95
commit a98a64649e
8 changed files with 71 additions and 29 deletions

View File

@@ -231,6 +231,21 @@ describe("createCronToolSchema", () => {
).toBe(true);
});
it("accepts payload.model and payload.fallbacks null in patch (clear-to-inherit)", () => {
expect(
Value.Check(createCronToolSchema(), {
action: "update",
jobId: "job-1",
patch: {
payload: {
model: null,
fallbacks: null,
},
},
}),
).toBe(true);
});
it("job.agentId and job.sessionKey project to plain string type for OpenAPI 3.0 compat", () => {
const root = providerSchemaRecord.properties as
| Record<string, { properties?: Record<string, unknown> }>

View File

@@ -109,7 +109,11 @@ function failureDestinationModeSchema(params: { nullableClears: boolean }) {
return Type.Optional(Type.Union(variants));
}
function cronPayloadObjectSchema(params: { model: TSchema; toolsAllow: TSchema }) {
function cronPayloadObjectSchema(params: {
model: TSchema;
toolsAllow: TSchema;
fallbacks: TSchema;
}) {
return Type.Object(
{
kind: optionalStringEnum(CRON_PAYLOAD_KINDS, { description: "Payload kind" }),
@@ -120,7 +124,7 @@ function cronPayloadObjectSchema(params: { model: TSchema; toolsAllow: TSchema }
timeoutSeconds: optionalFiniteNumberSchema({ minimum: 0 }),
lightContext: Type.Optional(Type.Boolean()),
allowUnsafeExternalContent: Type.Optional(Type.Boolean()),
fallbacks: Type.Optional(Type.Array(Type.String(), { description: "Fallback models" })),
fallbacks: params.fallbacks,
toolsAllow: params.toolsAllow,
},
{ additionalProperties: true },
@@ -161,6 +165,7 @@ function createCronPayloadSchema(): TSchema {
cronPayloadObjectSchema({
model: Type.Optional(Type.String({ description: "Model override" })),
toolsAllow: Type.Optional(Type.Array(Type.String(), { description: "Allowed tools" })),
fallbacks: Type.Optional(Type.Array(Type.String(), { description: "Fallback models" })),
}),
);
}
@@ -313,6 +318,7 @@ function createCronPatchObjectSchema(): TSchema {
cronPayloadObjectSchema({
model: nullableStringSchema("Model override, or null to clear"),
toolsAllow: nullableStringArraySchema("Allowed tool ids, or null to clear"),
fallbacks: nullableStringArraySchema("Fallback models, or null to clear"),
}),
),
delivery: createCronDeliveryPatchSchema(),

View File

@@ -904,11 +904,18 @@
"type": "boolean"
},
"fallbacks": {
"description": "Fallback models",
"items": {
"type": "string"
},
"type": "array"
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"description": "Fallback models, or null to clear"
},
"kind": {
"description": "Payload kind",

View File

@@ -900,11 +900,18 @@
"type": "boolean"
},
"fallbacks": {
"description": "Fallback models",
"items": {
"type": "string"
},
"type": "array"
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"description": "Fallback models, or null to clear"
},
"kind": {
"description": "Payload kind",

View File

@@ -900,11 +900,18 @@
"type": "boolean"
},
"fallbacks": {
"description": "Fallback models",
"items": {
"type": "string"
},
"type": "array"
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"description": "Fallback models, or null to clear"
},
"kind": {
"description": "Payload kind",

View File

@@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 51703,
"roughTokens": 12926
"chars": 51940,
"roughTokens": 12985
},
"openClawDeveloperInstructions": {
"chars": 3045,
@@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6893
},
"totalWithDynamicToolsJson": {
"chars": 79275,
"roughTokens": 19819
"chars": 79512,
"roughTokens": 19878
},
"userInputText": {
"chars": 1442,

View File

@@ -227,8 +227,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 51392,
"roughTokens": 12848
"chars": 51629,
"roughTokens": 12908
},
"openClawDeveloperInstructions": {
"chars": 1936,
@@ -239,8 +239,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6513
},
"totalWithDynamicToolsJson": {
"chars": 77446,
"roughTokens": 19362
"chars": 77683,
"roughTokens": 19421
},
"userInputText": {
"chars": 1033,

View File

@@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 52682,
"roughTokens": 13171
"chars": 52919,
"roughTokens": 13230
},
"openClawDeveloperInstructions": {
"chars": 1955,
@@ -240,8 +240,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6749
},
"totalWithDynamicToolsJson": {
"chars": 79679,
"roughTokens": 19920
"chars": 79916,
"roughTokens": 19979
},
"userInputText": {
"chars": 1271,