refactor: share cron model formatting assertions

This commit is contained in:
Peter Steinberger
2026-03-13 20:39:25 +00:00
parent 0836bf844b
commit 6d0e4c76ac

View File

@@ -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");
}); });
}); });