Files
oh-my-pi/packages/coding-agent/test/title-generator.test.ts
T
can1357 fb8496fd21 feat(tiny/text): added code block stripping for session title generation
- Added `stripCodeBlocks` to remove fenced code blocks before titling, preventing literal noise (e.g. version strings in UI mockups) from becoming the session title.
- Added `prepareTitleInput` composing strip and truncate steps, updated `formatTitleUserMessage` to use it.
- Added unit tests for stripping logic and an integration test verifying the model never receives code block contents.
2026-05-30 23:13:11 +02:00

130 lines
3.8 KiB
TypeScript

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<Api> {
const model = getBundledModel("anthropic", id);
if (!model) throw new Error(`Expected model ${id}`);
return model;
}
function createSettings(model: Model<Api>, 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<Api>) {
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");
});
});