Files
ln-bi/src/modules/assets/assistant/generative-presentation.test.ts
T
2026-09-23 17:52:33 +08:00

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, /&lt;script&gt;不能执行&lt;\/script&gt;/);
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,/不应插入的固定城市路径/);
});