import { describe, expect, it } from "vitest"; import { applySessionStreamEvent, toAgentChatMessages, toAgentUiMessages } from "./agent-session-message-state"; describe("task context in persisted answers", () => { it("restores submitted answer cards only from snapshots bound to their user message", () => { const reply = { version: "agent-interaction-request@1", interaction_id: "question", message_id: "source", turn_id: "turn", call_id: "call", status: "answered", revision: 1, draft_revision: 0, draft: [], created_at: "2026-10-28T10:00:00Z", questions: [{ question_id: "q", title: "核查时段", body: "请选择", mode: "single", required: true, allow_custom: false, options: [{ id: "a", label: "前一天" }] }], answers: [{ question_id: "q", selected_option_ids: ["a"], selected_options: [{ id: "a", label: "前一天" }], custom_text: "" }], delivery: { native_message_id: "reply", revision: 2, status: "delivered" } }; const parts = [{ type: "text", text: "核查时段: 前一天" }, { type: "data-interaction_reply", data: reply }]; const messages = toAgentChatMessages(toAgentUiMessages([ { id: "reply", role: "user", parts }, { id: "other", role: "user", parts }, { id: "reply", role: "assistant", parts }, { id: "pending", role: "user", parts: [{ type: "data-interaction_reply", data: { ...reply, status: "pending", delivery: { ...reply.delivery, native_message_id: "pending" } } }] } ]), {}, {}); expect(messages[0].interactionReply).toEqual(reply); expect(messages.slice(1).every((message) => message.interactionReply === undefined)).toBe(true); }); it("inserts consumed user input once before the active assistant in live and resumed streams", () => { const messages = toAgentUiMessages([ { id: "original", role: "user", parts: [{ type: "text", text: "原问题" }] }, { id: "answer", role: "assistant", parts: [{ type: "text", text: "处理中" }] } ]); const data = { id: "new-input", text: "追加线索", delivery: "steer", status: "pending" }; expect(applySessionStreamEvent(messages, { type: "user_input", data })).toBe(messages); const delivered = { type: "user_input" as const, data: { ...data, status: "delivered" } }; const next = applySessionStreamEvent(messages, delivered); expect(next.map(({ id }) => id)).toEqual(["original", "new-input", "answer"]); expect(applySessionStreamEvent(next, delivered)).toBe(next); }); it("keeps the report reference of each answer when history is restored", () => { const reference = { task_id: "22222222-2222-4222-8222-222222222222", execution_generation: 1, report_revision: 3 }; const stored = [ { id: "answer-1", role: "assistant", parts: [ { type: "data-task_context", data: reference }, { type: "text", text: "该判断仅覆盖投诉时段的上游记录。" } ] } ]; const messages = toAgentChatMessages(toAgentUiMessages(stored), {}, {}); expect(messages[0]?.taskContext).toEqual(reference); expect(messages[0]?.content).toBe("该判断仅覆盖投诉时段的上游记录。"); }); it("keeps the answer readable when a report reference is invalid", () => { const messages = toAgentChatMessages( toAgentUiMessages([ { id: "answer-1", role: "assistant", parts: [ { type: "data-task_context", data: { task_id: "unknown", execution_generation: -1, report_revision: 0 } }, { type: "text", text: "报告引用未记录。" } ] } ]), {}, {} ); expect(messages[0]?.taskContext).toBeUndefined(); expect(messages[0]?.content).toBe("报告引用未记录。"); }); }); describe("rich answer replay", () => { const block = { version: "answer-ui@1", block_id: "reading", fallback_text: "演练读数", spec: { root: "metric", elements: { metric: { type: "Metric", props: { label: "压力", value: "0.3", unit: "MPa", source: "演练数据" }, children: [] } } } }; it("restores anchored final content without process UI, duplicate blocks or internal reference markers", () => { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [ { type: "data-answer_ui", data: { ...block, block_id: "query", phase: "process", fallback_text: "查询过程资料" } }, { type: "data-answer_ui", data: { ...block, phase: "final" } }, { type: "data-answer_ui", data: { ...block, phase: "final", fallback_text: "更新后的最终读数" } }, { type: "data-final_answer", data: { content: "最终说明\n\n[[ui:reading]]\n\n后续解读\n\n[[ui:query]]", ui_block_ids: ["reading", "query"] } }, ] }]); const restored = toAgentChatMessages(messages, {}, {})[0]; expect(restored.answerParts?.filter((part) => part.type === "answer-ui")).toHaveLength(2); expect(restored.content).toContain("更新后的最终读数"); expect(restored.content).not.toContain("查询过程资料"); expect(restored.content).not.toContain("[[ui:"); expect(restored.content.indexOf("更新后的最终读数")).toBeLessThan(restored.content.indexOf("后续解读")); }); it("keeps text, component and summary in their original order across history restoration", () => { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [ { type: "text", text: "下面是演练数据。" }, { type: "data-answer_ui", id: "reading", data: block }, { type: "text", text: "需要现场核验。" }, { type: "data-final_answer", data: { content: "结论仍待确认。", ui_block_ids: ["reading"] } }, ] }]); const restored = toAgentChatMessages(messages, {}, {})[0]; expect(restored.answerParts?.map((part) => part.type)).toEqual(["text", "answer-ui", "text"]); expect(restored.content).toContain("演练读数"); expect(restored.answerParts).toContainEqual({ type: "text", text: "需要现场核验。" }); expect(restored.content).not.toContain("需要现场核验。"); }); it("updates a block once and appends later tokens after it without duplicating final text", () => { let messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [{ type: "text", text: "演练:" }] }]); messages = applySessionStreamEvent(messages, { type: "answer_ui", data: block }); messages = applySessionStreamEvent(messages, { type: "answer_ui", data: { ...block, fallback_text: "新读数" } }); messages = applySessionStreamEvent(messages, { type: "token", data: { content: "仍待核验。", ui_block_ids: ["reading"] } }); messages = applySessionStreamEvent(messages, { type: "final_answer", data: { content: "仍待核验。", ui_block_ids: ["reading"] } }); const answer = toAgentChatMessages(messages, {}, {})[0]; expect(answer.answerParts?.map((part) => part.type)).toEqual(["text", "answer-ui", "text"]); expect(answer.content.match(/新读数/g)).toHaveLength(1); expect(answer.content.match(/仍待核验/g)).toHaveLength(1); }); it("falls back to text when an old or malformed UI block cannot be rendered", () => { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [{ type: "data-answer_ui", data: { ...block, version: "unknown" } }] }]); expect(toAgentChatMessages(messages, {}, {})[0].answerParts).toEqual([{ type: "text", text: "演练读数" }]); }); }); it("keeps a malformed persisted data payload from crashing the conversation", () => { for (const data of [null, undefined, "invalid"]) { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [{ type: "data-answer_ui", data }] }]); expect(toAgentChatMessages(messages, {}, {})[0].content).toBe("这段结构化内容无法显示。"); } }); it("restores an aborted run's terminal state from canonical history", () => { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [ { type: "text", text: "已核查的部分结果" }, { type: "data-progress", data: { id: "query", title: "查询", status: "running" } }, { type: "data-run_status", data: { status: "aborted", message: "请求已中断" } } ] }]); expect(toAgentChatMessages(messages, {}, {})[0]).toMatchObject({ content: "已核查的部分结果", progress: [{ status: "cancelled" }] }); }); describe("terminal tool progress", () => { const parts = [ { type: "text", text: "已核查的部分结果" }, { type: "data-progress", data: { id: "tool", phase: "tool", title: "正在处理", status: "running", tool: { name: "query", input: { id: 12 } } } }, { type: "data-todo_update", data: { session_id: "s", todos: [{ id: "todo", content: "核查", status: "in_progress" }] } }, { type: "data-permission_request", data: { request_id: "permission", session_id: "s", permission: "read", patterns: [] } }, { type: "data-question_request", data: { request_id: "question", session_id: "s", questions: [{ question: "继续吗?", options: [] }] } } ]; const initial = () => toAgentUiMessages([{ id: "answer", role: "assistant", parts }]); it.each(["done", "error", "aborted"] as const)("settles running tool rows after resumed %s", (type) => { const messages = applySessionStreamEvent(initial(), { type, data: { message: "模型请求失败" } }); const answer = toAgentChatMessages(messages, {}, {})[0]; expect(answer.progress?.[0]).toMatchObject({ status: type === "done" ? "completed" : type === "aborted" ? "cancelled" : "error", tool: { name: "query", input: { id: 12 } } }); expect(answer.todos?.todos[0].status).toBe("cancelled"); expect(answer.content).toBe("已核查的部分结果"); if (type === "error") expect(answer.runError).toBe("模型请求失败"); }); it("preserves the reason when reopening a failed answer with partial structured output", () => { const messages = toAgentUiMessages([{ id: "answer", role: "assistant", parts: [...parts, { type: "data-final_answer", data: { content: "已核查的部分结果" } }, { type: "data-run_status", data: { status: "error", message: "模型请求失败" } } ] }]); const answer = toAgentChatMessages(messages, {}, {})[0]; expect(answer).toMatchObject({ content: "已核查的部分结果", runError: "模型请求失败", progress: [{ status: "error", detail: "模型请求失败" }] }); }); it("does not let an old reply override revive stopped cards", () => { const messages = applySessionStreamEvent(initial(), { type: "run_status", data: { status: "aborted" } }); const answer = toAgentChatMessages(messages, { permission: { status: "error", error: "late HTTP response" } }, { question: { status: "submitting" } })[0]; expect(answer.permissions?.[0].status).toBe("aborted"); expect(answer.questions?.[0].status).toBe("rejected"); const stopped = applySessionStreamEvent(messages, { type: "error", data: { message: "请求已中断" } }); expect(toAgentChatMessages(stopped, {}, {})[0].runError).toBeUndefined(); expect(toAgentChatMessages(stopped, {}, {})[0].progress?.[0].status).toBe("cancelled"); }); it("settles legacy history using the session status while leaving active tools running", () => { for (const status of ["completed", "aborted", "error", "waiting_input", "running"] as const) { const messages = toAgentUiMessages(initial(), { runStatus: status, isStreaming: status === "running" }); expect(toAgentChatMessages(messages, {}, {})[0].progress?.[0].status) .toBe(status === "running" ? "running" : status === "error" ? "error" : status === "aborted" ? "cancelled" : "completed"); } const state = applySessionStreamEvent(initial(), { type: "state", sessionId: "s", isStreaming: false, messages: initial() }); expect(toAgentChatMessages(state, {}, {})[0].progress?.[0].status).toBe("completed"); }); it("keeps a saved failure authoritative when a legacy session defaults to completed", () => { const saved = applySessionStreamEvent(initial(), { type: "error", data: { message: "历史错误" } }); const answer = toAgentChatMessages(toAgentUiMessages(saved, { runStatus: "completed", isStreaming: false }), {}, {})[0]; expect(answer.runError).toBe("历史错误"); }); it("routes historical question replies to the owning assistant without changing the current turn", () => { const messages = [...initial(), ...toAgentUiMessages([{ id: "current", role: "assistant", parts: [{ type: "text", text: "新一轮" }] }])]; const updated = applySessionStreamEvent(messages, { type: "question_response", data: { request_id: "question", rejected: true } }); const answers = toAgentChatMessages(updated, {}, {}); expect(answers[0].questions?.[0].status).toBe("rejected"); expect(updated[1]).toEqual(messages[1]); }); });