Files
next-tjwater-frontend/src/features/agent/answer-ui/contract.test.ts
T

119 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { parseAnswerUiBlock } from "./contract";
const block = {
version: "answer-ui@1",
block_id: "reading",
fallback_text: "仍待核对",
spec: {
root: "note",
elements: { note: { type: "Text", props: { text: "仍待核对", tone: "neutral" }, children: [] } }
}
};
describe("untrusted answer components", () => {
it("accepts a line with seven sparse history series while enforcing sample timestamp alignment", () => {
const x_data = Array.from({ length: 13 }, (_, index) => String(index));
const series = [{ name: "当前", type: "line", data: x_data.map(() => 31) },
...Array.from({ length: 7 }, (_, index) => ({ name: `前${index + 1}日`, type: "scatter",
data: [...Array(10).fill(null), 33, 34, 35],
sample_times: [...Array(10).fill(null), ...[30, 45, 59].map((minute) => `2026-06-${String(13 - index).padStart(2, "0")}T11:${minute}:00+08:00`)] }))];
const props = { title: "压力(m)", source: "合成样例", chart_type: "line", x_data, series };
const make = (value: unknown) => ({ ...block, spec: { root: "chart", elements: { chart: { type: "Chart", props: value, children: [] } } } });
expect(parseAnswerUiBlock(make(props))).not.toBeNull();
expect(parseAnswerUiBlock(make({ ...props, series: [...series, series[0]] }))).toBeNull();
expect(parseAnswerUiBlock(make({ ...props, series: [{ ...series[1], sample_times: [null] }] }))).toBeNull();
expect(parseAnswerUiBlock(make({ ...props, series: [{ ...series[1], sample_times: Array(13).fill("2026-06-13T12:00:00+08:00") }] }))).toBeNull();
});
it("keeps explicit UI phases and rejects invalid phase values", () => {
expect(parseAnswerUiBlock(block)?.phase).toBeUndefined();
expect(parseAnswerUiBlock({ ...block, phase: "process" })?.phase).toBe("process");
expect(parseAnswerUiBlock({ ...block, phase: "final" })?.phase).toBe("final");
expect(parseAnswerUiBlock({ ...block, phase: "other" })).toBeNull();
expect(parseAnswerUiBlock({ ...block, phase: null })).toBeNull();
});
it("accepts controlled task semantics and result states without accepting arbitrary presentation code", () => {
const result = { type: "Result", props: { title: "定位结果", summary: "3 根管道已定位,高亮未确认。", status: "partial", source: "地图工具回执" }, children: [] };
const spec = {
root: "root", elements: {
root: { type: "Stack", props: { columns: "one", gap: "normal", intent: "response", density: "compact" }, children: ["section"] },
section: { type: "Section", props: { title: "本次结果", description: null, role: "result" }, children: ["result"] },
result
}
};
expect(parseAnswerUiBlock({ ...block, spec })).not.toBeNull();
for (const props of [
{ ...result.props, status: "running" },
{ ...result.props, css: "display:none" },
{ ...result.props, onClick: "fetch('/write')" }
]) expect(parseAnswerUiBlock({ ...block, spec: { ...spec, elements: { ...spec.elements, result: { ...result, props } } } })).toBeNull();
expect(parseAnswerUiBlock({ ...block, spec: { root: "note", elements: { note: { ...block.spec.elements.note, props: { text: "高亮操作未提供。", tone: "warning", title: "能力限制" } } } } })).not.toBeNull();
});
it("accepts registered text and rejects arbitrary code, URLs and unknown components", () => {
expect(parseAnswerUiBlock(block)).not.toBeNull();
for (const node of [
{ type: "iframe", props: { src: "https://example.com" }, children: [] },
{
...block.spec.elements.note,
props: { ...block.spec.elements.note.props, script: "alert(1)" }
},
{ ...block.spec.elements.note, on: { click: { action: "fetch" } } }
])
expect(
parseAnswerUiBlock({ ...block, spec: { root: "note", elements: { note: node } } })
).toBeNull();
});
it("rejects missing, duplicate, cyclic, unreachable and excessive trees", () => {
const stack = { type: "Stack", props: { columns: "one", gap: "normal" }, children: ["note"] };
for (const children of [["missing"], ["note", "note"], ["root"]])
expect(
parseAnswerUiBlock({
...block,
spec: { root: "root", elements: { root: { ...stack, children }, ...block.spec.elements } }
})
).toBeNull();
expect(
parseAnswerUiBlock({
...block,
spec: {
...block.spec,
elements: { ...block.spec.elements, unused: block.spec.elements.note }
}
})
).toBeNull();
});
it("requires aligned, finite chart values and unambiguous form fields", () => {
const chart = {
type: "Chart",
props: {
title: "演练压力",
chart_type: "line",
source: "演练数据",
x_data: ["08:00"],
series: [{ name: "压力", data: [0.3] }]
},
children: []
};
const withNode = (node: unknown) => ({
...block,
spec: { root: "note", elements: { note: node } }
});
expect(parseAnswerUiBlock(withNode(chart))).not.toBeNull();
for (const data of [[0.3, 0.2], [Infinity], [NaN]])
expect(
parseAnswerUiBlock(
withNode({ ...chart, props: { ...chart.props, series: [{ name: "压力", data }] } })
)
).toBeNull();
const field = { name: "location", label: "位置", kind: "text", required: true };
expect(
parseAnswerUiBlock(
withNode({
type: "FollowUpForm",
props: { title: "核验", description: "用户补充", fields: [field, field] },
children: []
})
)
).toBeNull();
});
});