import { afterEach, describe, expect, it, vi } from "bun:test"; import * as ai from "@oh-my-pi/pi-ai"; import { type Api, getBundledModel, type Model } from "@oh-my-pi/pi-ai"; import { generateSessionTitle } from "../src/utils/title-generator"; function getModelOrThrow(id: string): Model { const model = getBundledModel("anthropic", id); if (!model) throw new Error(`Expected model ${id}`); return model; } function createSettings(model: Model, tinyModel = "online") { return { get(path: string) { if (path === "providers.tinyModel") return tinyModel; return undefined; }, getModelRole(role: string) { return role === "smol" ? `${model.provider}/${model.id}` : undefined; }, getStorage() { return undefined; }, } as never; } function createRegistry(model: Model) { return { getAvailable: () => [model], getApiKey: async () => "test-key", } as never; } afterEach(() => { vi.restoreAllMocks(); }); describe("title generator", () => { it("returns the title from a forced set_title tool call", async () => { const model = getModelOrThrow("claude-sonnet-4-5"); const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "stop", content: [ { type: "toolCall", id: "call-title", name: "set_title", arguments: { title: "Structured Title" }, }, ], } as never); const title = await generateSessionTitle( "Investigate the resolver", createRegistry(model), createSettings(model), ); expect(title).toBe("Structured Title"); expect(completeSimpleMock.mock.calls[0]?.[1]).toMatchObject({ tools: [expect.objectContaining({ name: "set_title" })], }); expect(completeSimpleMock.mock.calls[0]?.[2]).toMatchObject({ disableReasoning: true, toolChoice: { type: "tool", name: "set_title" }, }); }); it("falls back to text content when no set_title tool call is returned", async () => { const model = getModelOrThrow("claude-sonnet-4-5"); vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "stop", content: [{ type: "text", text: "Text Title" }], } as never); const title = await generateSessionTitle( "Investigate the resolver", createRegistry(model), createSettings(model), ); expect(title).toBe("Text Title"); }); it("uses a reasoning-safe output budget for reasoning models", async () => { const model = getModelOrThrow("claude-sonnet-4-5"); const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "stop", content: [ { type: "toolCall", id: "call-title", name: "set_title", arguments: { title: "Budget Title" }, }, ], } as never); const title = await generateSessionTitle( "Investigate the resolver", createRegistry(model), createSettings(model), ); const maxTokens = (completeSimpleMock.mock.calls[0]?.[2] as { maxTokens?: number } | undefined)?.maxTokens; expect(title).toBe("Budget Title"); expect(maxTokens).toBeGreaterThanOrEqual(1024); }); it("strips code blocks from the message sent to the model", async () => { const model = getModelOrThrow("claude-sonnet-4-5"); const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({ stopReason: "stop", content: [{ type: "toolCall", id: "call-title", name: "set_title", arguments: { title: "Setup Screen" } }], } as never); await generateSessionTitle( "plan a setup screen\n```\nWelcome to Claude Code v2.1.158\n```\npick provider then theme", createRegistry(model), createSettings(model), ); const sentMessages = (completeSimpleMock.mock.calls[0]?.[1] as { messages?: Array<{ content?: string }> }) ?.messages; const userContent = sentMessages?.[0]?.content ?? ""; expect(userContent).not.toContain("Claude Code v2.1.158"); expect(userContent).toContain("pick provider then theme"); }); });