murashit_codex-panel/tests/thread-naming.test.ts
2026-05-13 00:05:56 +09:00

211 lines
7.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
findThreadNamingContext,
firstNamingContextFromDisplayItems,
namingRuntime,
namingContextFromDisplayItems,
namingContextFromTurn,
normalizeGeneratedTitle,
titleFromNamingTurn,
validatedNamingRuntime,
} from "../src/panel/thread-naming";
import type { Model } from "../src/generated/app-server/v2/Model";
import type { Turn } from "../src/generated/app-server/v2/Turn";
describe("thread naming", () => {
it("extracts the first user request and final assistant response from a completed turn", () => {
expect(
namingContextFromTurn(
turn([
{ type: "userMessage", id: "u1", content: [{ type: "text", text: "Codex Panelに自動命名を付けたい", text_elements: [] }] },
{ type: "agentMessage", id: "a1", text: "実装方針をまとめました。", phase: "final_answer", memoryCitation: null },
]),
),
).toEqual({
userRequest: "Codex Panelに自動命名を付けたい",
assistantResponse: "実装方針をまとめました。",
});
});
it("does not build naming context for failed or incomplete turns", () => {
expect(
namingContextFromTurn(
turn([{ type: "userMessage", id: "u1", content: [{ type: "text", text: "hello", text_elements: [] }] }], {
status: "failed",
}),
),
).toBeNull();
});
it("extracts naming context from streamed display items when completed turn items are not loaded", () => {
expect(
namingContextFromDisplayItems("turn", [
{ id: "u1", kind: "message", role: "user", text: "自動命名を直したい", turnId: "turn", markdown: true },
{ id: "a1", kind: "message", role: "assistant", text: "原因を直しました。", turnId: "turn", markdown: false },
]),
).toEqual({
userRequest: "自動命名を直したい",
assistantResponse: "原因を直しました。",
});
});
it("uses the first usable displayed turn as a resumed-history fallback", () => {
expect(
firstNamingContextFromDisplayItems([
{ id: "u1", kind: "message", role: "user", text: "本文だけのturn", turnId: "turn-1", markdown: true },
{ id: "u2", kind: "message", role: "user", text: "履歴から命名したい", turnId: "turn-2", markdown: true },
{ id: "a2", kind: "message", role: "assistant", text: "表示済み履歴から候補を作ります。", turnId: "turn-2", markdown: false },
{ id: "u3", kind: "message", role: "user", text: "後続turn", turnId: "turn-3", markdown: true },
{ id: "a3", kind: "message", role: "assistant", text: "後続応答", turnId: "turn-3", markdown: false },
]),
).toEqual({
userRequest: "履歴から命名したい",
assistantResponse: "表示済み履歴から候補を作ります。",
});
});
it("scans older thread pages until it finds a usable naming context", async () => {
const calls: Array<{ cursor: string | null; limit: number; sortDirection: string }> = [];
const context = await findThreadNamingContext({
threadId: "thread",
pageLimit: 2,
maxPages: 3,
readTurns: async (_threadId, cursor, limit, sortDirection) => {
calls.push({ cursor, limit, sortDirection });
if (cursor === null) {
return {
data: [
turn([{ type: "userMessage", id: "u1", content: [{ type: "text", text: "本文だけ", text_elements: [] }] }], {
id: "turn-1",
}),
],
nextCursor: "cursor-2",
};
}
return {
data: [
turn(
[
{ type: "userMessage", id: "u2", content: [{ type: "text", text: "古い履歴から命名したい", text_elements: [] }] },
{ type: "agentMessage", id: "a2", text: "古いturnを使って候補を作ります。", phase: "final_answer", memoryCitation: null },
],
{ id: "turn-2" },
),
],
nextCursor: null,
};
},
});
expect(context).toEqual({
userRequest: "古い履歴から命名したい",
assistantResponse: "古いturnを使って候補を作ります。",
});
expect(calls).toEqual([
{ cursor: null, limit: 2, sortDirection: "asc" },
{ cursor: "cursor-2", limit: 2, sortDirection: "asc" },
]);
});
it("falls back to visible display items after bounded history scanning", async () => {
await expect(
findThreadNamingContext({
threadId: "thread",
pageLimit: 1,
maxPages: 1,
readTurns: async () => ({ data: [], nextCursor: "ignored" }),
fallbackDisplayItems: [
{ id: "u1", kind: "message", role: "user", text: "表示済み履歴から命名したい", turnId: "visible", markdown: true },
{ id: "a1", kind: "message", role: "assistant", text: "表示済み履歴を使います。", turnId: "visible", markdown: false },
],
}),
).resolves.toEqual({
userRequest: "表示済み履歴から命名したい",
assistantResponse: "表示済み履歴を使います。",
});
});
it("parses structured title responses", () => {
expect(
titleFromNamingTurn(
turn([
{
type: "agentMessage",
id: "a1",
text: '```json\n{"title":"Codex Panelの自動命名"}\n```',
phase: "final_answer",
memoryCitation: null,
},
]),
),
).toBe("Codex Panelの自動命名");
});
it("normalizes generated titles", () => {
expect(normalizeGeneratedTitle(' ## "Codex Panelの自動命名"\n')).toBe("Codex Panelの自動命名");
expect(normalizeGeneratedTitle("")).toBeNull();
expect(normalizeGeneratedTitle("x".repeat(80))).toHaveLength(40);
});
it("uses explicit naming runtime settings", () => {
expect(namingRuntime({ threadNamingModel: "gpt-5.4-mini", threadNamingEffort: "minimal" })).toEqual({
model: "gpt-5.4-mini",
effort: "minimal",
});
});
it("omits naming runtime overrides that are set to Codex default", () => {
expect(namingRuntime({ threadNamingModel: null, threadNamingEffort: null })).toEqual({});
});
it("omits an explicit naming effort when the selected model does not support it", () => {
expect(
validatedNamingRuntime({ threadNamingModel: "gpt-5.4-mini", threadNamingEffort: "minimal" }, [
model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]),
]),
).toEqual({ model: "gpt-5.4-mini" });
});
it("keeps an explicit naming effort when the selected model supports it", () => {
expect(
validatedNamingRuntime({ threadNamingModel: "gpt-5.4-mini", threadNamingEffort: "low" }, [
model("gpt-5.4-mini", ["low", "medium", "high", "xhigh"]),
]),
).toEqual({ model: "gpt-5.4-mini", effort: "low" });
});
});
function turn(items: Turn["items"], overrides: Partial<Turn> = {}): Turn {
return {
id: "turn",
items,
itemsView: "full",
status: "completed",
error: null,
startedAt: 1,
completedAt: 2,
durationMs: 1,
...overrides,
};
}
function model(name: string, efforts: Model["supportedReasoningEfforts"][number]["reasoningEffort"][]): Model {
return {
id: name,
model: name,
upgrade: null,
upgradeInfo: null,
availabilityNux: null,
displayName: name,
description: "",
hidden: false,
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort, description: "" })),
defaultReasoningEffort: efforts[0] ?? "low",
inputModalities: ["text"],
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
isDefault: false,
};
}