128 lines
7.7 KiB
TypeScript
128 lines
7.7 KiB
TypeScript
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("这段结构化内容无法显示。");
|
|
}
|
|
});
|