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 { const model = getBundledModel("anthropic", id); if (!model) throw new Error(`Expected anthropic model ${id} to exist`); return model; } function modelValue(model: Model): string { return `${model.provider}/${model.id}`; } async function createSession(options?: { initialModel?: Model; selectInitialModel?: (availableModels: Model[]) => Model; modelRoles?: Record; }): 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); }); });