131 lines
5.5 KiB
TypeScript
131 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { AgentChatMessage } from "../types";
|
|
import { canExpandAnswer, getAnswerPresentation, getFinalAnswerTranscript } from "./answer-presentation";
|
|
import type { AnswerUiPart } from "./contract";
|
|
|
|
const ui = (id: string): AnswerUiPart => ({
|
|
type: "answer-ui",
|
|
block: {
|
|
version: "answer-ui@1",
|
|
block_id: id,
|
|
fallback_text: id,
|
|
spec: {
|
|
root: "text",
|
|
elements: { text: { type: "Text", props: { text: id, tone: "neutral" }, children: [] } }
|
|
}
|
|
}
|
|
});
|
|
const parts: AnswerUiPart[] = [
|
|
{ type: "text", text: "正在核对资料。" },
|
|
ui("report-chart"),
|
|
ui("temporary"),
|
|
{ type: "text", text: "查询结束。" }
|
|
];
|
|
const message: AgentChatMessage = {
|
|
id: "a",
|
|
role: "assistant",
|
|
content: "正在核对资料。",
|
|
answerParts: parts
|
|
};
|
|
describe("final report and collapsible process", () => {
|
|
it("keeps short warnings and location confirmations inline while retaining report readers", () => {
|
|
const note = ui("note");
|
|
if (note.type !== "answer-ui") throw new Error("invalid fixture");
|
|
note.block.spec.elements.text.props = { text: "当前工具不能确认高亮。", tone: "warning" };
|
|
expect(canExpandAnswer([{ type: "text", text: "已定位管道。" }, note])).toBe(false);
|
|
expect(canExpandAnswer([{ type: "text", text: "完整报告".repeat(300) }])).toBe(true);
|
|
for (const type of ["Table", "Chart", "FollowUpForm"]) {
|
|
note.block.spec.elements.text.type = type;
|
|
expect(canExpandAnswer([note])).toBe(true);
|
|
}
|
|
});
|
|
it("shows all incremental content while waiting for final_answer", () => {
|
|
expect(getAnswerPresentation(message)).toEqual({ report: parts, process: [], final: false });
|
|
});
|
|
it("keeps only the final report and explicitly selected components expanded", () => {
|
|
const view = getAnswerPresentation({
|
|
...message,
|
|
finalAnswer: {
|
|
content: "最后的报告内容。",
|
|
uiBlockIds: ["report-chart", "report-chart", "missing"]
|
|
}
|
|
});
|
|
expect(view.report).toEqual([{ type: "text", text: "最后的报告内容。" }, parts[1]]);
|
|
expect(view.process).toEqual([parts[0], parts[2], parts[3]]);
|
|
expect(view.final).toBe(true);
|
|
});
|
|
it("folds every UI block when the final report does not reference any", () => {
|
|
const view = getAnswerPresentation({
|
|
...message,
|
|
finalAnswer: { content: "报告", uiBlockIds: [] }
|
|
});
|
|
expect(view.report).toEqual([{ type: "text", text: "报告" }]);
|
|
expect(view.process).toEqual(parts);
|
|
});
|
|
});
|
|
|
|
function phasedUi(id: string, phase: "process" | "final"): AnswerUiPart {
|
|
const part = ui(id);
|
|
if (part.type === "answer-ui") {
|
|
part.block.phase = phase;
|
|
part.block.fallback_text = `摘要 ${id}`;
|
|
}
|
|
return part;
|
|
}
|
|
|
|
describe("final UI anchors", () => {
|
|
const chart = phasedUi("chart", "final");
|
|
const progress = phasedUi("progress", "process");
|
|
it("separates explicit process blocks from the report before a final answer arrives", () => {
|
|
const view = getAnswerPresentation({ ...message, answerParts: [parts[0], progress, chart, parts[3]] });
|
|
expect(view.report).toEqual([parts[0], chart, parts[3]]);
|
|
expect(view.process).toEqual([progress]);
|
|
expect(view.final).toBe(false);
|
|
});
|
|
const anchoredMessage = (content: string, uiBlockIds = ["chart", "progress"]): AgentChatMessage => ({
|
|
...message,
|
|
answerParts: [parts[0], progress, chart, parts[3]],
|
|
finalAnswer: { content, uiBlockIds }
|
|
});
|
|
|
|
it("places final UI between the original Markdown sections and mounts duplicates once", () => {
|
|
const content = "# 分析\n\n- 第一项\n\n[[ui:chart]]\n\n## 建议\n保留换行\n[[ui:chart]]\n";
|
|
const view = getAnswerPresentation(anchoredMessage(content));
|
|
expect(view.report).toEqual([
|
|
{ type: "text", text: "# 分析\n\n- 第一项\n\n" },
|
|
chart,
|
|
{ type: "text", text: "\n## 建议\n保留换行\n" }
|
|
]);
|
|
expect(view.process).toEqual([parts[0], progress, parts[3]]);
|
|
expect(getFinalAnswerTranscript(anchoredMessage(content))).toBe("# 分析\n\n- 第一项\n\n摘要 chart\n## 建议\n保留换行\n");
|
|
});
|
|
|
|
it("never includes process UI even when referenced and anchored", () => {
|
|
const value = anchoredMessage("结论\n[[ui:progress]]\n[[ui:missing]]\n[[ui:chart]]", ["progress", "missing"]);
|
|
const view = getAnswerPresentation(value);
|
|
expect(view.report.every((part) => part.type === "text")).toBe(true);
|
|
expect(view.process).toEqual([parts[0], progress, chart, parts[3]]);
|
|
const transcript = getFinalAnswerTranscript(value);
|
|
expect(transcript).not.toContain("progress");
|
|
expect(transcript).not.toContain("missing");
|
|
expect(transcript).not.toContain("chart");
|
|
expect(transcript).toContain("暂不可用");
|
|
});
|
|
|
|
it("preserves fenced, indented, and quoted markers as literal Markdown", () => {
|
|
const literal = "```md\n[[ui:chart]]\n```\n\n~~~\n[[ui:chart]]\n~~~\n\n [[ui:chart]]\n\n> [[ui:chart]]\n";
|
|
const view = getAnswerPresentation(anchoredMessage(literal, []));
|
|
expect(view.report).toEqual([{ type: "text", text: literal }]);
|
|
});
|
|
|
|
it("retains legacy references and appends final references without anchors", () => {
|
|
const view = getAnswerPresentation(anchoredMessage("报告"));
|
|
expect(view.report).toEqual([{ type: "text", text: "报告" }, chart]);
|
|
expect(getFinalAnswerTranscript(anchoredMessage("报告"))).toBe("报告\n\n摘要 chart");
|
|
});
|
|
|
|
it("does not expand a short answer because of marker identifiers", () => {
|
|
expect(canExpandAnswer([{ type: "text", text: `结论\n[[ui:${"x".repeat(1500)}]]` }])).toBe(false);
|
|
});
|
|
});
|