Files
openclaw/src/agents/live-model-switch.test.ts
2026-03-27 12:01:55 +00:00

130 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../test/helpers/import-fresh.js";
const state = vi.hoisted(() => ({
abortEmbeddedPiRunMock: vi.fn(),
requestEmbeddedRunModelSwitchMock: vi.fn(),
consumeEmbeddedRunModelSwitchMock: vi.fn(),
resolveDefaultModelForAgentMock: vi.fn(),
loadSessionStoreMock: vi.fn(),
resolveStorePathMock: vi.fn(),
}));
vi.mock("./pi-embedded.js", () => ({
abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args),
}));
vi.mock("./pi-embedded-runner/runs.js", () => ({
requestEmbeddedRunModelSwitch: (...args: unknown[]) =>
state.requestEmbeddedRunModelSwitchMock(...args),
consumeEmbeddedRunModelSwitch: (...args: unknown[]) =>
state.consumeEmbeddedRunModelSwitchMock(...args),
}));
vi.mock("./model-selection.js", () => ({
resolveDefaultModelForAgent: (...args: unknown[]) =>
state.resolveDefaultModelForAgentMock(...args),
}));
vi.mock("../config/sessions.js", () => ({
loadSessionStore: (...args: unknown[]) => state.loadSessionStoreMock(...args),
resolveStorePath: (...args: unknown[]) => state.resolveStorePathMock(...args),
}));
async function loadModule() {
return await importFreshModule<typeof import("./live-model-switch.js")>(
import.meta.url,
`./live-model-switch.js?scope=${Math.random().toString(36).slice(2)}`,
);
}
describe("live model switch", () => {
beforeEach(() => {
vi.resetModules();
state.abortEmbeddedPiRunMock.mockReset().mockReturnValue(false);
state.requestEmbeddedRunModelSwitchMock.mockReset();
state.consumeEmbeddedRunModelSwitchMock.mockReset();
state.resolveDefaultModelForAgentMock
.mockReset()
.mockReturnValue({ provider: "anthropic", model: "claude-opus-4-6" });
state.loadSessionStoreMock.mockReset().mockReturnValue({});
state.resolveStorePathMock.mockReset().mockReturnValue("/tmp/session-store.json");
});
afterEach(() => {
vi.clearAllMocks();
});
it("resolves persisted session overrides ahead of agent defaults", async () => {
state.loadSessionStoreMock.mockReturnValue({
main: {
providerOverride: "openai",
modelOverride: "gpt-5.4",
authProfileOverride: "profile-gpt",
authProfileOverrideSource: "user",
},
});
const { resolveLiveSessionModelSelection } = await loadModule();
expect(
resolveLiveSessionModelSelection({
cfg: { session: { store: "/tmp/custom-store.json" } },
sessionKey: "main",
agentId: "reply",
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
}),
).toEqual({
provider: "openai",
model: "gpt-5.4",
authProfileId: "profile-gpt",
authProfileIdSource: "user",
});
expect(state.resolveDefaultModelForAgentMock).toHaveBeenCalledWith({
cfg: { session: { store: "/tmp/custom-store.json" } },
agentId: "reply",
});
expect(state.resolveStorePathMock).toHaveBeenCalledWith("/tmp/custom-store.json", {
agentId: "reply",
});
});
it("queues a live switch only when an active run was aborted", async () => {
state.abortEmbeddedPiRunMock.mockReturnValue(true);
const { requestLiveSessionModelSwitch } = await loadModule();
expect(
requestLiveSessionModelSwitch({
sessionEntry: { sessionId: "session-1" },
selection: { provider: "openai", model: "gpt-5.4", authProfileId: "profile-gpt" },
}),
).toBe(true);
expect(state.abortEmbeddedPiRunMock).toHaveBeenCalledWith("session-1");
expect(state.requestEmbeddedRunModelSwitchMock).toHaveBeenCalledWith("session-1", {
provider: "openai",
model: "gpt-5.4",
authProfileId: "profile-gpt",
});
});
it("treats auth-profile-source changes as no-op when no auth profile is selected", async () => {
const { hasDifferentLiveSessionModelSelection } = await loadModule();
expect(
hasDifferentLiveSessionModelSelection(
{
provider: "openai",
model: "gpt-5.4",
authProfileIdSource: "auto",
},
{
provider: "openai",
model: "gpt-5.4",
},
),
).toBe(false);
});
});