254 lines
7.6 KiB
TypeScript
254 lines
7.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { GenerativeResultView } from "./GenerativeResultView";
|
|
import { CompoundResultView } from "./CompoundResultView";
|
|
import {
|
|
presentationAction,
|
|
presentationData,
|
|
presentationFields,
|
|
presentationMetrics,
|
|
} from "./generative-presentation";
|
|
import type {
|
|
AssistantResult,
|
|
PresentationInteraction,
|
|
} from "../../../shared/assets/assistant";
|
|
|
|
const result: AssistantResult = {
|
|
resultId: "generated-result",
|
|
sessionId: "demo",
|
|
plan: {
|
|
kind: "assets",
|
|
category: "inventory",
|
|
groupBy: "region",
|
|
view: "chart",
|
|
filters: {},
|
|
},
|
|
title: "库存分布",
|
|
summary: "华东库存12台,华南8台。",
|
|
source: "演示台账",
|
|
fetchedAt: "2026-09-23T08:00:00Z",
|
|
rows: [],
|
|
metrics: [
|
|
{ label: "总库存", value: 20, unit: "台" },
|
|
{ label: "异动", value: 2, unit: "台" },
|
|
],
|
|
groups: [
|
|
{
|
|
name: "华东",
|
|
count: 12,
|
|
operating: 0,
|
|
rate: 0,
|
|
keys: { region: "华东" },
|
|
},
|
|
{ name: "华南", count: 8, operating: 0, rate: 0, keys: { region: "华南" } },
|
|
],
|
|
daily: [
|
|
{ date: "2026-09-23", delivered: 1, returned: 2, replaced: 0, total: 3 },
|
|
],
|
|
presentation: {
|
|
title: "库存情况",
|
|
blocks: [
|
|
{ id: "conclusion", kind: "summary" },
|
|
{ id: "selected", kind: "metrics", metrics: ["异动", "总库存"] },
|
|
{ id: "narrative", kind: "narrative", text: "<script>不能执行</script>" },
|
|
{
|
|
id: "regions",
|
|
kind: "table",
|
|
dataset: "groups",
|
|
columns: [
|
|
{ field: "count", label: "库存数量" },
|
|
{ field: "name", label: "所属区域" },
|
|
],
|
|
interactionId: "cities",
|
|
},
|
|
],
|
|
interactions: [
|
|
{
|
|
id: "cities",
|
|
label: "查看城市分布",
|
|
kind: "group",
|
|
plan: {
|
|
kind: "assets",
|
|
category: "inventory",
|
|
groupBy: "city",
|
|
view: "chart",
|
|
filters: {},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const callbacks = {
|
|
ask: () => {},
|
|
operate: () => {},
|
|
restore: () => {},
|
|
activate: () => {},
|
|
showCorrection: () => {},
|
|
active: true,
|
|
subject: "演示公司",
|
|
disabled: false,
|
|
};
|
|
|
|
test("model-selected metrics and columns preserve their order and never invent values", () => {
|
|
assert.deepEqual(
|
|
presentationMetrics(result, ["异动", "不存在", "总库存"]).map(
|
|
(item) => item.value,
|
|
),
|
|
[2, 20],
|
|
);
|
|
assert.deepEqual(
|
|
presentationFields({
|
|
id: "table",
|
|
kind: "table",
|
|
dataset: "groups",
|
|
columns: [
|
|
{ field: "count", label: "数量" },
|
|
{ field: "secret", label: "无此字段" },
|
|
],
|
|
}),
|
|
[{ field: "count", label: "数量" }],
|
|
);
|
|
assert.deepEqual(presentationData(result, "groups")[0].keys, {
|
|
region: "华东",
|
|
});
|
|
assert.deepEqual(presentationData(result, "comparison"), []);
|
|
});
|
|
test("exploration uses stored interaction ID and actual clicked keys or date", () => {
|
|
const group = result.presentation!.interactions[0];
|
|
assert.deepEqual(
|
|
presentationAction(group, presentationData(result, "groups")[1]),
|
|
{ type: "explore", interactionId: "cities", keys: { region: "华南" } },
|
|
);
|
|
assert.equal(presentationAction(group, { name: "华东" }), undefined);
|
|
const point: PresentationInteraction = { ...group, id: "day", kind: "point" };
|
|
assert.deepEqual(
|
|
presentationAction(point, presentationData(result, "daily")[0]),
|
|
{ type: "explore", interactionId: "day", date: "2026-09-23" },
|
|
);
|
|
assert.equal(presentationAction(point, { name: "昨天" }), undefined);
|
|
assert.deepEqual(presentationAction({ ...group, kind: "followup" }), {
|
|
type: "explore",
|
|
interactionId: "cities",
|
|
});
|
|
});
|
|
test("semantic render respects block order, real table fields, plain text, scope and answer anchors", () => {
|
|
const html = renderToStaticMarkup(
|
|
createElement(GenerativeResultView, { ...callbacks, result }),
|
|
);
|
|
assert.ok(
|
|
html.indexOf('data-block-id="selected"') <
|
|
html.indexOf('data-block-id="regions"'),
|
|
);
|
|
assert.ok(html.indexOf(">异动</span>") < html.indexOf(">总库存</span>"));
|
|
assert.ok(html.indexOf(">库存数量</th>") < html.indexOf(">所属区域</th>"));
|
|
assert.match(html, /<script>不能执行<\/script>/);
|
|
assert.doesNotMatch(html, /<script>/);
|
|
for (const expected of [
|
|
"aq-query-generated-result",
|
|
"aq-answer-generated-result",
|
|
"查看城市分布",
|
|
"源数据更新时间:",
|
|
"未提供",
|
|
"演示公司",
|
|
"华东",
|
|
"复制完整结果",
|
|
])
|
|
assert.ok(html.includes(expected), expected);
|
|
});
|
|
test("stack layout renders each real section while tabs expand only the active section", () => {
|
|
const compound: AssistantResult = {
|
|
...result,
|
|
resultId: "parent",
|
|
layout: "stack",
|
|
presentation: undefined,
|
|
sections: [
|
|
{
|
|
id: "first",
|
|
title: "库存分析",
|
|
question: "库存",
|
|
display: "chart",
|
|
result,
|
|
},
|
|
{
|
|
id: "second",
|
|
title: "异动分析",
|
|
question: "异动",
|
|
display: "table",
|
|
result: { ...result, resultId: "second-child", summary: "异动2台。" },
|
|
},
|
|
{
|
|
id: "failed",
|
|
title: "成本分析",
|
|
question: "成本",
|
|
display: "metrics",
|
|
error: "缺少成本数据",
|
|
},
|
|
],
|
|
};
|
|
const stacked = renderToStaticMarkup(
|
|
createElement(CompoundResultView, {
|
|
...callbacks,
|
|
result: compound,
|
|
activeResultId: "second-child",
|
|
}),
|
|
);
|
|
assert.ok(stacked.includes('id="aq-query-generated-result"'));
|
|
assert.ok(stacked.includes('id="aq-query-second-child"'));
|
|
assert.ok(stacked.includes('data-section-id="first"'));
|
|
assert.match(stacked, /缺少成本数据/);
|
|
assert.doesNotMatch(stacked, /role="tablist"/);
|
|
const tabbed = renderToStaticMarkup(
|
|
createElement(CompoundResultView, {
|
|
...callbacks,
|
|
result: { ...compound, layout: "tabs" },
|
|
activeResultId: "second-child",
|
|
}),
|
|
);
|
|
assert.ok(!tabbed.includes('id="aq-query-generated-result"'));
|
|
assert.ok(tabbed.includes('id="aq-query-second-child"'));
|
|
assert.match(tabbed, /role="tablist"/);
|
|
});
|
|
|
|
test("AI summary text is honored and metrics-only layouts do not receive an unsolicited visible summary", () => {
|
|
const summary = renderToStaticMarkup(
|
|
createElement(GenerativeResultView, {
|
|
...callbacks,
|
|
result: {
|
|
...result,
|
|
presentation: {
|
|
blocks: [{ id: "s", kind: "summary", text: "异动2台,应先处理。" }],
|
|
interactions: [],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
assert.match(summary, /异动2台,应先处理。/);
|
|
const metricsOnly = renderToStaticMarkup(
|
|
createElement(GenerativeResultView, {
|
|
...callbacks,
|
|
result: {
|
|
...result,
|
|
presentation: {
|
|
blocks: [{ id: "m", kind: "metrics", metrics: ["总库存"] }],
|
|
interactions: [],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
assert.ok(
|
|
metricsOnly.indexOf(result.summary) >
|
|
metricsOnly.indexOf('<details class="aq-result-definitions">'),
|
|
);
|
|
assert.ok(!metricsOnly.includes('class="aq-answer"'));
|
|
assert.ok(metricsOnly.includes('id="aq-query-generated-result"'));
|
|
});
|
|
|
|
test("generated exploration does not add unrelated fixed followup suggestions", () => {
|
|
const markup=renderToStaticMarkup(createElement(GenerativeResultView,{
|
|
...callbacks,result:{...result,suggestions:["不应插入的固定城市路径"],presentation:{blocks:[{id:"m",kind:"metrics",metrics:["总库存"]}],interactions:[]}},
|
|
}));
|
|
assert.doesNotMatch(markup,/不应插入的固定城市路径/);
|
|
});
|