import { describe, expect, it } from "bun:test"; import { buildPromptWithLearningContext, extractLatestFrontendTurn, generateSessionTitle, shouldRestoreConversationForRuntime, shouldGenerateSessionTitle, } from "../../src/routes/chatSession.js"; import { type SessionTurnRecord } from "../../src/sessions/transcriptStore.js"; import { type MemoryStore } from "../../src/memory/store.js"; import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; describe("shouldGenerateSessionTitle", () => { it("allows auto-title generation for the first turn when the title was not edited", () => { expect( shouldGenerateSessionTitle({ recentTurnCount: 0, isTitleManuallyEdited: false, }), ).toBe(true); }); it("blocks auto-title generation after the user edits the title manually", () => { expect( shouldGenerateSessionTitle({ recentTurnCount: 0, isTitleManuallyEdited: true, }), ).toBe(false); }); it("only allows auto-title generation during the first two turns", () => { expect( shouldGenerateSessionTitle({ recentTurnCount: 1, isTitleManuallyEdited: false, }), ).toBe(true); expect( shouldGenerateSessionTitle({ recentTurnCount: 2, isTitleManuallyEdited: false, }), ).toBe(false); }); }); describe("generateSessionTitle", () => { it("uses the current user and assistant turn instead of reading wrapped runtime context", async () => { let titlePrompt = ""; const runtime = { createSession: async () => ({ id: "title-session" }), prompt: async (_sessionId: string, prompt: string) => { titlePrompt = prompt; }, waitForSessionIdle: async () => undefined, messages: async () => [ { info: { role: "assistant" }, parts: [{ type: "text", text: "标题:泵站压力异常排查。" }], }, ], abortSession: async () => undefined, } as unknown as OpencodeRuntimeAdapter; const title = await generateSessionTitle(runtime, { sessionId: "chat-session", latestUserMessage: "检查一下三号泵站最近压力波动的原因", latestAssistantMessage: "三号泵站压力波动主要与夜间阀门开度变化有关。", fallbackTitle: "新对话", }); expect(title).toBe("泵站压力异常排查"); expect(titlePrompt).toContain("用户:检查一下三号泵站最近压力波动的原因"); expect(titlePrompt).toContain("助手:三号泵站压力波动主要与夜间阀门开度变化有关。"); }); }); describe("buildPromptWithLearningContext", () => { const memoryStore = { buildPromptSnapshot: async () => "", } as unknown as MemoryStore; it("prefers persisted frontend messages so aborted turns remain in restored context", async () => { const prompt = await buildPromptWithLearningContext( memoryStore, "actor-1", "project-1", { recentTurns: [], persistedMessages: [ { role: "user", content: "先分析 3 号泵站夜间压力波动" }, { role: "assistant", content: "已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。", isError: true, }, { role: "assistant", content: "⚠️ **请求已中断**", isError: true }, ], message: "继续刚才的分析,并补充相邻支路影响", }, ); expect(prompt).toContain("用户:先分析 3 号泵站夜间压力波动"); expect(prompt).toContain( "助手:已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。", ); expect(prompt).not.toContain("⚠️ **请求已中断**"); expect(prompt).toContain("[Current user request]\n继续刚才的分析,并补充相邻支路影响"); }); it("falls back to history turns when frontend state is unavailable", async () => { const recentTurns: SessionTurnRecord[] = [ { id: "turn-1", userMessage: "检查 DMA-2 夜间漏损异常", assistantMessage: "DMA-2 在 02:00-04:00 出现持续最小夜流抬升。", timestamp: new Date().toISOString(), toolCallCount: 1, }, ]; const prompt = await buildPromptWithLearningContext( memoryStore, "actor-1", "project-1", { recentTurns, message: "继续给出排查建议", }, ); expect(prompt).toContain("用户:检查 DMA-2 夜间漏损异常"); expect(prompt).toContain("助手:DMA-2 在 02:00-04:00 出现持续最小夜流抬升。"); }); it("skips restored conversation injection when reusing an existing opencode session", async () => { const prompt = await buildPromptWithLearningContext( memoryStore, "actor-1", "project-1", { recentTurns: [ { id: "turn-1", userMessage: "上一轮问题", assistantMessage: "上一轮回答", timestamp: new Date().toISOString(), toolCallCount: 0, }, ], persistedMessages: [ { role: "user", content: "旧问题" }, { role: "assistant", content: "旧回答" }, ], message: "基于刚才结果继续分析", restoreConversation: false, }, ); expect(prompt).not.toContain("[Previous conversation context]"); expect(prompt).toBe("基于刚才结果继续分析"); }); it("restores copied fork context when metadata exists but runtime has no conversation", () => { expect( shouldRestoreConversationForRuntime({ hadExistingSessionRecord: true, runtimeHasConversation: false, }), ).toBe(true); expect( shouldRestoreConversationForRuntime({ hadExistingSessionRecord: true, runtimeHasConversation: true, }), ).toBe(false); }); }); describe("extractLatestFrontendTurn", () => { it("extracts the latest valid frontend user and assistant turn", () => { const turn = extractLatestFrontendTurn([ { role: "user", content: "检查 DMA-2 漏损" }, { role: "assistant", content: "DMA-2 夜间最小流量持续抬升。", progress: [{ id: "tool-dma", phase: "tool" }], }, { role: "user", content: "继续分析相邻分区" }, { role: "assistant", content: "⚠️ **请求已中断**", isError: true }, ]); expect(turn).toEqual({ assistantMessage: "DMA-2 夜间最小流量持续抬升。", toolCallCount: 1, userMessage: "检查 DMA-2 漏损", }); }); });