mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:00:42 +00:00
refactor: share cron model formatting assertions
This commit is contained in:
@@ -24,6 +24,8 @@ function lastEmbeddedCall(): { provider?: string; model?: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MESSAGE = "do it";
|
const DEFAULT_MESSAGE = "do it";
|
||||||
|
const DEFAULT_PROVIDER = "anthropic";
|
||||||
|
const DEFAULT_MODEL = "claude-opus-4-5";
|
||||||
|
|
||||||
type TurnOptions = {
|
type TurnOptions = {
|
||||||
cfgOverrides?: Parameters<typeof makeCfg>[2];
|
cfgOverrides?: Parameters<typeof makeCfg>[2];
|
||||||
@@ -73,6 +75,50 @@ async function runTurn(home: string, options: TurnOptions = {}) {
|
|||||||
return { res, call: lastEmbeddedCall() };
|
return { res, call: lastEmbeddedCall() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectSelectedModel(
|
||||||
|
call: { provider?: string; model?: string },
|
||||||
|
params: { provider: string; model: string },
|
||||||
|
) {
|
||||||
|
expect(call.provider).toBe(params.provider);
|
||||||
|
expect(call.model).toBe(params.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectDefaultSelectedModel(call: { provider?: string; model?: string }) {
|
||||||
|
expectSelectedModel(call, { provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCronSessionOverrideStore(
|
||||||
|
overrides: Record<string, unknown>,
|
||||||
|
sessionId = "existing-session",
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
"agent:main:cron:job-1": {
|
||||||
|
sessionId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectTurnModel(
|
||||||
|
home: string,
|
||||||
|
options: TurnOptions,
|
||||||
|
expected: { provider: string; model: string },
|
||||||
|
) {
|
||||||
|
const { res, call } = await runTurn(home, options);
|
||||||
|
expect(res.status).toBe("ok");
|
||||||
|
expectSelectedModel(call, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectInvalidModel(home: string, model: string) {
|
||||||
|
const { res } = await runErrorTurn(home, {
|
||||||
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe("error");
|
||||||
|
expect(res.error).toMatch(/invalid model/i);
|
||||||
|
expect(vi.mocked(runEmbeddedPiAgent)).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -99,16 +145,17 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
|
|
||||||
it("handles leading/trailing whitespace in model string", async () => {
|
it("handles leading/trailing whitespace in model string", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { res, call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
jobPayload: {
|
home,
|
||||||
kind: "agentTurn",
|
{
|
||||||
message: DEFAULT_MESSAGE,
|
jobPayload: {
|
||||||
model: " openai/gpt-4.1-mini ",
|
kind: "agentTurn",
|
||||||
|
message: DEFAULT_MESSAGE,
|
||||||
|
model: " openai/gpt-4.1-mini ",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{ provider: "openai", model: "gpt-4.1-mini" },
|
||||||
expect(res.status).toBe("ok");
|
);
|
||||||
expect(call.provider).toBe("openai");
|
|
||||||
expect(call.model).toBe("gpt-4.1-mini");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,38 +176,29 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
|
|
||||||
it("rejects model with trailing slash (empty model name)", async () => {
|
it("rejects model with trailing slash (empty model name)", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { res } = await runErrorTurn(home, {
|
await expectInvalidModel(home, "openai/");
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "openai/" },
|
|
||||||
});
|
|
||||||
expect(res.status).toBe("error");
|
|
||||||
expect(res.error).toMatch(/invalid model/i);
|
|
||||||
expect(vi.mocked(runEmbeddedPiAgent)).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects model with leading slash (empty provider)", async () => {
|
it("rejects model with leading slash (empty provider)", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { res } = await runErrorTurn(home, {
|
await expectInvalidModel(home, "/gpt-4.1-mini");
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "/gpt-4.1-mini" },
|
|
||||||
});
|
|
||||||
expect(res.status).toBe("error");
|
|
||||||
expect(res.error).toMatch(/invalid model/i);
|
|
||||||
expect(vi.mocked(runEmbeddedPiAgent)).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes provider casing", async () => {
|
it("normalizes provider casing", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { res, call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
jobPayload: {
|
home,
|
||||||
kind: "agentTurn",
|
{
|
||||||
message: DEFAULT_MESSAGE,
|
jobPayload: {
|
||||||
model: "OpenAI/gpt-4.1-mini",
|
kind: "agentTurn",
|
||||||
|
message: DEFAULT_MESSAGE,
|
||||||
|
model: "OpenAI/gpt-4.1-mini",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
{ provider: "openai", model: "gpt-4.1-mini" },
|
||||||
expect(res.status).toBe("ok");
|
);
|
||||||
expect(call.provider).toBe("openai");
|
|
||||||
expect(call.model).toBe("gpt-4.1-mini");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -217,43 +255,39 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
// No model in job payload. Session store has openai override.
|
// No model in job payload. Session store has openai override.
|
||||||
// Provider must be openai, not the default anthropic.
|
// Provider must be openai, not the default anthropic.
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
home,
|
||||||
storeEntries: {
|
{
|
||||||
"agent:main:cron:job-1": {
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
sessionId: "existing-session",
|
storeEntries: createCronSessionOverrideStore({
|
||||||
updatedAt: Date.now(),
|
|
||||||
providerOverride: "openai",
|
providerOverride: "openai",
|
||||||
modelOverride: "gpt-4.1-mini",
|
modelOverride: "gpt-4.1-mini",
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
});
|
{ provider: "openai", model: "gpt-4.1-mini" },
|
||||||
expect(call.provider).toBe("openai");
|
);
|
||||||
expect(call.model).toBe("gpt-4.1-mini");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("job payload model wins over conflicting session override", async () => {
|
it("job payload model wins over conflicting session override", async () => {
|
||||||
// Job payload says anthropic. Session says openai. Job must win.
|
// Job payload says anthropic. Session says openai. Job must win.
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
jobPayload: {
|
home,
|
||||||
kind: "agentTurn",
|
{
|
||||||
message: DEFAULT_MESSAGE,
|
jobPayload: {
|
||||||
model: "anthropic/claude-sonnet-4-5",
|
kind: "agentTurn",
|
||||||
deliver: false,
|
message: DEFAULT_MESSAGE,
|
||||||
},
|
model: "anthropic/claude-sonnet-4-5",
|
||||||
storeEntries: {
|
deliver: false,
|
||||||
"agent:main:cron:job-1": {
|
},
|
||||||
sessionId: "existing-session",
|
storeEntries: createCronSessionOverrideStore({
|
||||||
updatedAt: Date.now(),
|
|
||||||
providerOverride: "openai",
|
providerOverride: "openai",
|
||||||
modelOverride: "gpt-4.1-mini",
|
modelOverride: "gpt-4.1-mini",
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
});
|
{ provider: "anthropic", model: "claude-sonnet-4-5" },
|
||||||
expect(call.provider).toBe("anthropic");
|
);
|
||||||
expect(call.model).toBe("claude-sonnet-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -262,9 +296,7 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
const { call } = await runTurn(home, {
|
const { call } = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
});
|
});
|
||||||
// makeCfg default is anthropic/claude-opus-4-5
|
expectDefaultSelectedModel(call);
|
||||||
expect(call.provider).toBe("anthropic");
|
|
||||||
expect(call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -293,17 +325,12 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
mockAgentPayloads([{ text: "ok" }]);
|
mockAgentPayloads([{ text: "ok" }]);
|
||||||
const step2 = await runTurn(home, {
|
const step2 = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
storeEntries: {
|
storeEntries: createCronSessionOverrideStore({
|
||||||
"agent:main:cron:job-1": {
|
providerOverride: "openai",
|
||||||
sessionId: "existing-session",
|
modelOverride: "gpt-4.1-mini",
|
||||||
updatedAt: Date.now(),
|
}),
|
||||||
providerOverride: "openai",
|
|
||||||
modelOverride: "gpt-4.1-mini",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(step2.call.provider).toBe("openai");
|
expectSelectedModel(step2.call, { provider: "openai", model: "gpt-4.1-mini" });
|
||||||
expect(step2.call.model).toBe("gpt-4.1-mini");
|
|
||||||
|
|
||||||
// Step 3: Job payload says anthropic, session store still says openai
|
// Step 3: Job payload says anthropic, session store still says openai
|
||||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||||
@@ -315,17 +342,12 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-5",
|
||||||
deliver: false,
|
deliver: false,
|
||||||
},
|
},
|
||||||
storeEntries: {
|
storeEntries: createCronSessionOverrideStore({
|
||||||
"agent:main:cron:job-1": {
|
providerOverride: "openai",
|
||||||
sessionId: "existing-session",
|
modelOverride: "gpt-4.1-mini",
|
||||||
updatedAt: Date.now(),
|
}),
|
||||||
providerOverride: "openai",
|
|
||||||
modelOverride: "gpt-4.1-mini",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(step3.call.provider).toBe("anthropic");
|
expectSelectedModel(step3.call, { provider: "anthropic", model: "claude-opus-4-5" });
|
||||||
expect(step3.call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,8 +371,7 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
const r2 = await runTurn(home, {
|
const r2 = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
});
|
});
|
||||||
expect(r2.call.provider).toBe("anthropic");
|
expectDefaultSelectedModel(r2.call);
|
||||||
expect(r2.call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -363,19 +384,20 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
// The stored modelOverride/providerOverride must still be read and applied
|
// The stored modelOverride/providerOverride must still be read and applied
|
||||||
// (resolveCronSession spreads ...entry before overriding core fields).
|
// (resolveCronSession spreads ...entry before overriding core fields).
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
home,
|
||||||
storeEntries: {
|
{
|
||||||
"agent:main:cron:job-1": {
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
sessionId: "old-session-id",
|
storeEntries: createCronSessionOverrideStore(
|
||||||
updatedAt: Date.now(),
|
{
|
||||||
providerOverride: "openai",
|
providerOverride: "openai",
|
||||||
modelOverride: "gpt-4.1-mini",
|
modelOverride: "gpt-4.1-mini",
|
||||||
},
|
},
|
||||||
|
"old-session-id",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
{ provider: "openai", model: "gpt-4.1-mini" },
|
||||||
expect(call.provider).toBe("openai");
|
);
|
||||||
expect(call.model).toBe("gpt-4.1-mini");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,16 +405,9 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
const { call } = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
storeEntries: {
|
storeEntries: createCronSessionOverrideStore({}, "old-session-id"),
|
||||||
"agent:main:cron:job-1": {
|
|
||||||
sessionId: "old-session-id",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
// No providerOverride or modelOverride
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(call.provider).toBe("anthropic");
|
expectDefaultSelectedModel(call);
|
||||||
expect(call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -405,8 +420,7 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
const { call } = await runTurn(home, {
|
const { call } = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: " " },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: " " },
|
||||||
});
|
});
|
||||||
expect(call.provider).toBe("anthropic");
|
expectDefaultSelectedModel(call);
|
||||||
expect(call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -415,8 +429,7 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
const { call } = await runTurn(home, {
|
const { call } = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "" },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "" },
|
||||||
});
|
});
|
||||||
expect(call.provider).toBe("anthropic");
|
expectDefaultSelectedModel(call);
|
||||||
expect(call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -424,18 +437,13 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
const { call } = await runTurn(home, {
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
storeEntries: {
|
storeEntries: createCronSessionOverrideStore(
|
||||||
"agent:main:cron:job-1": {
|
{ providerOverride: "openai", modelOverride: " " },
|
||||||
sessionId: "old",
|
"old",
|
||||||
updatedAt: Date.now(),
|
),
|
||||||
providerOverride: "openai",
|
|
||||||
modelOverride: " ",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
// Whitespace modelOverride should be ignored → default
|
// Whitespace modelOverride should be ignored → default
|
||||||
expect(call.provider).toBe("anthropic");
|
expectDefaultSelectedModel(call);
|
||||||
expect(call.model).toBe("claude-opus-4-5");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -445,35 +453,39 @@ describe("cron model formatting and precedence edge cases", () => {
|
|||||||
describe("config model format variations", () => {
|
describe("config model format variations", () => {
|
||||||
it("default model as string 'provider/model'", async () => {
|
it("default model as string 'provider/model'", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
cfgOverrides: {
|
home,
|
||||||
agents: {
|
{
|
||||||
defaults: {
|
cfgOverrides: {
|
||||||
model: "openai/gpt-4.1",
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "openai/gpt-4.1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
},
|
},
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
{ provider: "openai", model: "gpt-4.1" },
|
||||||
});
|
);
|
||||||
expect(call.provider).toBe("openai");
|
|
||||||
expect(call.model).toBe("gpt-4.1");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default model as object with primary field", async () => {
|
it("default model as object with primary field", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const { call } = await runTurn(home, {
|
await expectTurnModel(
|
||||||
cfgOverrides: {
|
home,
|
||||||
agents: {
|
{
|
||||||
defaults: {
|
cfgOverrides: {
|
||||||
model: { primary: "openai/gpt-4.1" },
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "openai/gpt-4.1" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
||||||
},
|
},
|
||||||
jobPayload: { kind: "agentTurn", message: DEFAULT_MESSAGE, deliver: false },
|
{ provider: "openai", model: "gpt-4.1" },
|
||||||
});
|
);
|
||||||
expect(call.provider).toBe("openai");
|
|
||||||
expect(call.model).toBe("gpt-4.1");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user