230 lines
7.9 KiB
TypeScript
230 lines
7.9 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 { Effort, getBundledModel } 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 role model thinking behavior", () => {
|
|
let tempDir: TempDir;
|
|
let session: AgentSession;
|
|
let sessionSettings: Settings;
|
|
|
|
beforeEach(() => {
|
|
tempDir = TempDir.createSync("@pi-role-thinking-");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (session) {
|
|
await session.dispose();
|
|
}
|
|
tempDir.removeSync();
|
|
});
|
|
|
|
function getAnthropicModelOrThrow(id: string) {
|
|
const model = getBundledModel("anthropic", id);
|
|
if (!model) throw new Error(`Expected anthropic model ${id} to exist`);
|
|
return model;
|
|
}
|
|
|
|
async function createSession(options: {
|
|
initialModelId: string;
|
|
initialThinkingLevel: Effort;
|
|
modelRoles: Record<string, string>;
|
|
}) {
|
|
const model = getAnthropicModelOrThrow(options.initialModelId);
|
|
const agent = new Agent({
|
|
initialState: {
|
|
model,
|
|
systemPrompt: "Test",
|
|
tools: [],
|
|
messages: [],
|
|
thinkingLevel: options.initialThinkingLevel,
|
|
},
|
|
});
|
|
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
|
|
authStorage.setRuntimeApiKey("anthropic", "test-key");
|
|
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml"));
|
|
|
|
sessionSettings = Settings.isolated();
|
|
for (const [role, modelRoleValue] of Object.entries(options.modelRoles)) {
|
|
sessionSettings.setModelRole(role, modelRoleValue);
|
|
}
|
|
session = new AgentSession({
|
|
agent,
|
|
sessionManager: SessionManager.inMemory(),
|
|
settings: sessionSettings,
|
|
modelRegistry,
|
|
});
|
|
}
|
|
|
|
it("re-applies explicit role thinking each time that role is selected", async () => {
|
|
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
|
const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
|
|
|
await createSession({
|
|
initialModelId: defaultModel.id,
|
|
initialThinkingLevel: Effort.High,
|
|
modelRoles: {
|
|
default: `${defaultModel.provider}/${defaultModel.id}`,
|
|
slow: `${slowModel.provider}/${slowModel.id}:off`,
|
|
},
|
|
});
|
|
|
|
const firstSwitch = await session.cycleRoleModels(["default", "slow"]);
|
|
expect(firstSwitch?.role).toBe("slow");
|
|
expect(firstSwitch?.model.id).toBe(slowModel.id);
|
|
expect(firstSwitch?.thinkingLevel).toBe("off");
|
|
expect(session.thinkingLevel).toBe("off");
|
|
|
|
session.setThinkingLevel(Effort.High);
|
|
expect(session.thinkingLevel).toBe(Effort.High);
|
|
|
|
const secondSwitch = await session.cycleRoleModels(["default", "slow"]);
|
|
expect(secondSwitch?.role).toBe("default");
|
|
expect(secondSwitch?.model.id).toBe(defaultModel.id);
|
|
expect(session.thinkingLevel).toBe(Effort.High);
|
|
|
|
const thirdSwitch = await session.cycleRoleModels(["default", "slow"]);
|
|
expect(thirdSwitch?.role).toBe("slow");
|
|
expect(thirdSwitch?.model.id).toBe(slowModel.id);
|
|
expect(thirdSwitch?.thinkingLevel).toBe("off");
|
|
expect(session.thinkingLevel).toBe("off");
|
|
});
|
|
|
|
it("preserves current thinking when switching into default/no-suffix role", async () => {
|
|
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
|
const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
|
|
|
await createSession({
|
|
initialModelId: defaultModel.id,
|
|
initialThinkingLevel: Effort.Low,
|
|
modelRoles: {
|
|
default: `${defaultModel.provider}/${defaultModel.id}`,
|
|
slow: `${slowModel.provider}/${slowModel.id}:high`,
|
|
},
|
|
});
|
|
|
|
const toSlow = await session.cycleRoleModels(["default", "slow"]);
|
|
expect(toSlow?.role).toBe("slow");
|
|
expect(toSlow?.thinkingLevel).toBe(Effort.High);
|
|
expect(session.thinkingLevel).toBe(Effort.High);
|
|
|
|
session.setThinkingLevel(Effort.Minimal);
|
|
expect(session.thinkingLevel).toBe(Effort.Minimal);
|
|
|
|
const toDefault = await session.cycleRoleModels(["default", "slow"]);
|
|
expect(toDefault?.role).toBe("default");
|
|
expect(toDefault?.model.id).toBe(defaultModel.id);
|
|
expect(toDefault?.thinkingLevel).toBe(Effort.Minimal);
|
|
expect(session.thinkingLevel).toBe(Effort.Minimal);
|
|
});
|
|
|
|
it("applies slow role thinking even when plan shares the same model", async () => {
|
|
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
|
const smolModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
|
const slowPlanModel = getAnthropicModelOrThrow("claude-opus-4-5");
|
|
|
|
await createSession({
|
|
initialModelId: defaultModel.id,
|
|
initialThinkingLevel: Effort.Medium,
|
|
modelRoles: {
|
|
default: `${defaultModel.provider}/${defaultModel.id}`,
|
|
smol: `${smolModel.provider}/${smolModel.id}:low`,
|
|
slow: `${slowPlanModel.provider}/${slowPlanModel.id}:high`,
|
|
plan: `${slowPlanModel.provider}/${slowPlanModel.id}:off`,
|
|
},
|
|
});
|
|
|
|
const toSmol = await session.cycleRoleModels(["slow", "default", "smol"]);
|
|
expect(toSmol?.role).toBe("smol");
|
|
expect(toSmol?.thinkingLevel).toBe(Effort.Low);
|
|
expect(session.thinkingLevel).toBe(Effort.Low);
|
|
|
|
const toSlow = await session.cycleRoleModels(["slow", "default", "smol"]);
|
|
expect(toSlow?.role).toBe("slow");
|
|
expect(toSlow?.model.id).toBe(slowPlanModel.id);
|
|
expect(toSlow?.thinkingLevel).toBe(Effort.High);
|
|
expect(session.thinkingLevel).toBe(Effort.High);
|
|
});
|
|
|
|
it("preserves explicit role thinking when updating default model despite unresolved previous model", async () => {
|
|
const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
|
const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
|
|
|
await createSession({
|
|
initialModelId: defaultModel.id,
|
|
initialThinkingLevel: Effort.High,
|
|
modelRoles: {
|
|
default: "anthropic/nonexistent-model:off",
|
|
},
|
|
});
|
|
|
|
await session.setModel(slowModel);
|
|
|
|
expect(sessionSettings.getModelRole("default")).toBe(`${slowModel.provider}/${slowModel.id}:off`);
|
|
});
|
|
|
|
it("clamps unsupported selections from model metadata", async () => {
|
|
const model = getAnthropicModelOrThrow("claude-sonnet-4-6");
|
|
const agent = new Agent({
|
|
initialState: {
|
|
model,
|
|
systemPrompt: "Test",
|
|
tools: [],
|
|
messages: [],
|
|
thinkingLevel: undefined,
|
|
},
|
|
});
|
|
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth-non-xhigh.db"));
|
|
authStorage.setRuntimeApiKey("anthropic", "test-key");
|
|
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models-non-xhigh.yml"));
|
|
|
|
sessionSettings = Settings.isolated();
|
|
session = new AgentSession({
|
|
agent,
|
|
sessionManager: SessionManager.inMemory(),
|
|
settings: sessionSettings,
|
|
modelRegistry,
|
|
});
|
|
|
|
session.setThinkingLevel(Effort.XHigh);
|
|
expect(session.thinkingLevel).toBe(Effort.High);
|
|
expect(session.getAvailableThinkingLevels()).not.toContain("xhigh");
|
|
});
|
|
|
|
it("cycles through off before returning to effort levels", async () => {
|
|
const model = getAnthropicModelOrThrow("claude-sonnet-4-5");
|
|
|
|
const agent = new Agent({
|
|
initialState: {
|
|
model,
|
|
systemPrompt: "Test",
|
|
tools: [],
|
|
messages: [],
|
|
thinkingLevel: Effort.High,
|
|
},
|
|
});
|
|
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth-cycle-thinking.db"));
|
|
authStorage.setRuntimeApiKey("anthropic", "test-key");
|
|
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models-cycle-thinking.yml"));
|
|
|
|
sessionSettings = Settings.isolated();
|
|
session = new AgentSession({
|
|
agent,
|
|
sessionManager: SessionManager.inMemory(),
|
|
settings: sessionSettings,
|
|
modelRegistry,
|
|
});
|
|
|
|
expect(session.cycleThinkingLevel()).toBe("off");
|
|
expect(session.thinkingLevel).toBe("off");
|
|
expect(session.cycleThinkingLevel()).toBe(Effort.Minimal);
|
|
expect(session.thinkingLevel).toBe(Effort.Minimal);
|
|
});
|
|
});
|