fix: scope cron payload.model fallback to disallowed refs

This commit is contained in:
Shakker
2026-02-25 23:24:31 +00:00
parent ccee9b1f8a
commit 714238b2ea
2 changed files with 63 additions and 13 deletions

View File

@@ -39,14 +39,18 @@ vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../../agents/model-selection.js", () => ({
getModelRefStatus: getModelRefStatusMock,
isCliProvider: isCliProviderMock,
resolveAllowedModelRef: resolveAllowedModelRefMock,
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
resolveHooksGmailModel: resolveHooksGmailModelMock,
resolveThinkingDefault: resolveThinkingDefaultMock,
}));
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: getModelRefStatusMock,
isCliProvider: isCliProviderMock,
resolveAllowedModelRef: resolveAllowedModelRefMock,
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
resolveHooksGmailModel: resolveHooksGmailModelMock,
resolveThinkingDefault: resolveThinkingDefaultMock,
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: vi.fn().mockResolvedValue({
@@ -417,6 +421,27 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
await expectPrimaryOverridePreservesDefaults({ primary: "anthropic/claude-sonnet-4-5" });
});
it("applies payload.model override when model is allowed", async () => {
resolveAllowedModelRefMock.mockReturnValueOnce({
ref: { provider: "anthropic", model: "claude-sonnet-4-6" },
});
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
payload: { kind: "agentTurn", message: "test", model: "anthropic/claude-sonnet-4-6" },
}),
}),
);
expect(result.status).toBe("ok");
expect(logWarnMock).not.toHaveBeenCalled();
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
const runParams = runWithModelFallbackMock.mock.calls[0][0];
expect(runParams.provider).toBe("anthropic");
expect(runParams.model).toBe("claude-sonnet-4-6");
});
it("falls back to agent defaults when payload.model is not allowed", async () => {
resolveAllowedModelRefMock.mockReturnValueOnce({
error: "model not allowed: anthropic/claude-sonnet-4-6",
@@ -449,5 +474,24 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
expect(model?.primary).toBe("openai-codex/gpt-5.3-codex");
expect(model?.fallbacks).toEqual(defaultFallbacks);
});
it("returns an error when payload.model is invalid", async () => {
resolveAllowedModelRefMock.mockReturnValueOnce({
error: "invalid model: openai/",
});
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
payload: { kind: "agentTurn", message: "test", model: "openai/" },
}),
}),
);
expect(result.status).toBe("error");
expect(result.error).toBe("invalid model: openai/");
expect(logWarnMock).not.toHaveBeenCalled();
expect(runWithModelFallbackMock).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,7 +1,3 @@
import type { CliDeps } from "../../cli/outbound-send-deps.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { AgentDefaultsConfig } from "../../config/types.js";
import type { CronJob, CronRunOutcome, CronRunTelemetry } from "../types.js";
import {
resolveAgentConfig,
resolveAgentDir,
@@ -34,11 +30,14 @@ import {
normalizeVerboseLevel,
supportsXHighThinking,
} from "../../auto-reply/thinking.js";
import type { CliDeps } from "../../cli/outbound-send-deps.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveSessionTranscriptPath,
setSessionRuntimeModel,
updateSessionStore,
} from "../../config/sessions.js";
import type { AgentDefaultsConfig } from "../../config/types.js";
import { registerAgentRunContext } from "../../infra/agent-events.js";
import { logWarn } from "../../logger.js";
import { buildAgentMainSessionKey, normalizeAgentId } from "../../routing/session-key.js";
@@ -49,6 +48,7 @@ import {
isExternalHookSession,
} from "../../security/external-content.js";
import { resolveCronDeliveryPlan } from "../delivery.js";
import type { CronJob, CronRunOutcome, CronRunTelemetry } from "../types.js";
import {
dispatchCronDelivery,
matchesMessagingToolDeliveryTarget,
@@ -198,7 +198,13 @@ export async function runCronIsolatedAgentTurn(params: {
defaultModel: resolvedDefault.model,
});
if ("error" in resolvedOverride) {
logWarn(`cron: payload.model '${modelOverride}' not allowed, falling back to agent defaults`);
if (resolvedOverride.error.startsWith("model not allowed:")) {
logWarn(
`cron: payload.model '${modelOverride}' not allowed, falling back to agent defaults`,
);
} else {
return { status: "error", error: resolvedOverride.error };
}
} else {
provider = resolvedOverride.ref.provider;
model = resolvedOverride.ref.model;