Files
oh-my-pi/packages/coding-agent/test/agent-session-force-tool-choice.test.ts
T
can1357 1b9d9d0851 refactor(catalog)!: split model catalog from pi-ai
Move bundled models, model cache/manager, thinking metadata, effort helpers,
provider descriptors/discovery, wire constants, and model identity utilities
into the new @oh-my-pi/pi-catalog package.

Update pi-ai to keep provider runtime/auth concerns, move catalog provider
metadata into CATALOG_PROVIDERS, and migrate coding-agent, agent, stats, docs,
and tests to import catalog values from pi-catalog.

Split coding-agent model registry helpers into discovery, roles, and models
config modules while preserving registry orchestration.

BREAKING CHANGE: @oh-my-pi/pi-ai no longer exports catalog subpaths such as
/models, /model-cache, /model-manager, /model-thinking, /effort,
/provider-models*, discovery helpers, and provider wire constants; use the
matching @oh-my-pi/pi-catalog subpaths instead.
2026-06-10 04:06:57 +02:00

93 lines
3.1 KiB
TypeScript

import { afterEach, beforeEach, expect, it } from "bun:test";
import * as path from "node:path";
import { Agent, type AgentTool } from "@oh-my-pi/pi-agent-core";
import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
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 { convertToLlm } from "@oh-my-pi/pi-coding-agent/session/messages";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TempDir } from "@oh-my-pi/pi-utils";
import * as z from "zod/v4";
let tempDir: TempDir;
let authStorage: AuthStorage | undefined;
let session: AgentSession;
beforeEach(async () => {
tempDir = TempDir.createSync("@pi-agent-session-force-tool-");
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected claude-sonnet-4-5 model to exist");
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"));
const settings = Settings.isolated({ "compaction.enabled": false });
const sessionManager = SessionManager.inMemory(tempDir.path());
const bashTool: AgentTool = {
name: "bash",
label: "Bash",
description: "Mock bash tool",
parameters: z.object({}),
execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
};
const writeTool: AgentTool = {
name: "write",
label: "Write",
description: "Mock write tool",
parameters: z.object({}),
execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
};
const agent = new Agent({
getApiKey: () => "test-key",
initialState: {
model,
systemPrompt: ["Test"],
tools: [bashTool, writeTool],
messages: [],
},
convertToLlm,
streamFn: () => new AssistantMessageEventStream(),
});
session = new AgentSession({
agent,
sessionManager,
settings,
modelRegistry,
toolRegistry: new Map([
[bashTool.name, bashTool],
[writeTool.name, writeTool],
]),
});
});
afterEach(async () => {
await session.dispose();
authStorage?.close();
authStorage = undefined;
tempDir.removeSync();
});
it("forces specific tool, then transitions to none, then clears", () => {
session.setForcedToolChoice("write");
const first = session.nextToolChoice();
const second = session.nextToolChoice();
const third = session.nextToolChoice();
expect(first).toEqual({ type: "tool", name: "write" });
// After the forced call, "none" prevents the loop from making more tool calls
expect(second).toBe("none");
// After "none" is consumed, override clears entirely
expect(third).toBeUndefined();
});
it("throws when forcing a non-active tool", () => {
expect(() => session.setForcedToolChoice("read")).toThrow('Tool "read" is not currently active.');
});