Files
oh-my-pi/packages/coding-agent/test/agent-session-model-persistence.test.ts
T
can1357 346ae48b0c fix(session): prevented runtime model switches from persisting default role
- Restricted `setModel` to persist settings only when `persist: true` is passed; all runtime switches (Ctrl+P, `--model`, `/model`, model picker temp selections) no longer overwrite `modelRoles.default`.
- Changed `cycleRoleModels` to accept a direction ("forward"/"backward") instead of a `temporary` flag; both directions now use `applyRoleModel` without persisting.
- Added `persist: true` exclusively to the model picker's "Set as default" action in `SelectorController`.
- Added test suite covering persistence behavior for `setModel`, `cycleRoleModels`, and `cycleModel`.
2026-05-31 06:21:29 +02:00

191 lines
6.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as path from "node:path";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { type Api, Effort, getBundledModel, type Model } from "@oh-my-pi/pi-ai";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TempDir } from "@oh-my-pi/pi-utils";
describe("AgentSession model persistence", () => {
let tempDir: TempDir;
let session: AgentSession | undefined;
let sessionSettings: Settings;
const authStorages: AuthStorage[] = [];
beforeEach(() => {
tempDir = TempDir.createSync("@pi-model-persistence-");
});
afterEach(async () => {
if (session) {
await session.dispose();
session = undefined;
}
for (const authStorage of authStorages.splice(0)) {
authStorage.close();
}
tempDir.removeSync();
});
function getAnthropicModelOrThrow(id: string): Model<Api> {
const model = getBundledModel("anthropic", id);
if (!model) throw new Error(`Expected anthropic model ${id} to exist`);
return model;
}
function modelValue(model: Model<Api>): string {
return `${model.provider}/${model.id}`;
}
async function createSession(options?: {
initialModel?: Model<Api>;
selectInitialModel?: (availableModels: Model<Api>[]) => Model<Api>;
modelRoles?: Record<string, string>;
}): Promise<{ modelRegistry: ModelRegistry; settings: Settings; session: AgentSession }> {
const authStorage = await AuthStorage.create(path.join(tempDir.path(), `testauth-${authStorages.length}.db`));
authStorages.push(authStorage);
authStorage.setRuntimeApiKey("anthropic", "test-key");
const modelRegistry = new ModelRegistry(
authStorage,
path.join(tempDir.path(), `models-${authStorages.length}.yml`),
);
const model =
options?.initialModel ??
options?.selectInitialModel?.(modelRegistry.getAvailable()) ??
getAnthropicModelOrThrow("claude-sonnet-4-5");
const agent = new Agent({
initialState: {
model,
systemPrompt: ["Test"],
tools: [],
messages: [],
thinkingLevel: Effort.Medium,
},
});
sessionSettings = Settings.isolated();
const modelRoles = options?.modelRoles;
if (modelRoles) {
for (const role in modelRoles) {
const modelRoleValue = modelRoles[role];
if (modelRoleValue !== undefined) {
sessionSettings.setModelRole(role, modelRoleValue);
}
}
}
session = new AgentSession({
agent,
sessionManager: SessionManager.inMemory(),
settings: sessionSettings,
modelRegistry,
});
return { modelRegistry, settings: sessionSettings, session };
}
it("switches the active model without persisting by default", async () => {
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
const nextModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
const defaultRoleValue = modelValue(defaultModel);
const created = await createSession({
initialModel: defaultModel,
modelRoles: { default: defaultRoleValue },
});
await created.session.setModel(nextModel);
expect(created.session.model?.id).toBe(nextModel.id);
expect(created.settings.getModelRole("default")).toBe(defaultRoleValue);
});
it("persists the default role when explicitly requested", async () => {
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
const nextModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
const created = await createSession({
initialModel: defaultModel,
modelRoles: { default: modelValue(defaultModel) },
});
await created.session.setModel(nextModel, "default", { persist: true });
expect(created.session.model?.id).toBe(nextModel.id);
expect(created.settings.getModelRole("default")).toBe(modelValue(nextModel));
});
it("cycles role models without rewriting configured roles", async () => {
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
const defaultRoleValue = modelValue(defaultModel);
const slowRoleValue = `${modelValue(slowModel)}:high`;
const created = await createSession({
initialModel: defaultModel,
modelRoles: {
default: defaultRoleValue,
slow: slowRoleValue,
},
});
const result = await created.session.cycleRoleModels(["default", "slow"]);
expect(result?.role).toBe("slow");
expect(result?.model.id).toBe(slowModel.id);
expect(created.session.model?.id).toBe(slowModel.id);
expect(created.settings.getModelRole("default")).toBe(defaultRoleValue);
expect(created.settings.getModelRole("slow")).toBe(slowRoleValue);
});
it("cycles role models backward from the current role", async () => {
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
const defaultRoleValue = modelValue(defaultModel);
const slowRoleValue = modelValue(slowModel);
const created = await createSession({
initialModel: defaultModel,
modelRoles: {
default: defaultRoleValue,
slow: slowRoleValue,
},
});
const forward = await created.session.cycleRoleModels(["default", "slow"], "forward");
const backward = await created.session.cycleRoleModels(["default", "slow"], "backward");
expect(forward?.role).toBe("slow");
expect(backward?.role).toBe("default");
expect(created.session.model?.id).toBe(defaultModel.id);
expect(created.settings.getModelRole("default")).toBe(defaultRoleValue);
expect(created.settings.getModelRole("slow")).toBe(slowRoleValue);
});
it("cycles available models without persisting the default role", async () => {
const created = await createSession({
selectInitialModel: availableModels => {
if (availableModels.length <= 1 || !availableModels[0]) {
throw new Error("Expected at least two available models");
}
return availableModels[0];
},
});
const initialModel = created.session.model;
if (!initialModel) throw new Error("Expected initial model to be set");
const defaultRoleValue = modelValue(initialModel);
created.settings.setModelRole("default", defaultRoleValue);
const result = await created.session.cycleModel();
if (!result) throw new Error("Expected cycleModel to return a new model");
expect(modelValue(result.model)).not.toBe(defaultRoleValue);
const activeModel = created.session.model;
if (!activeModel) throw new Error("Expected active model after cycleModel");
expect(modelValue(activeModel)).toBe(modelValue(result.model));
expect(created.settings.getModelRole("default")).toBe(defaultRoleValue);
});
});