ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
226 lines
6.8 KiB
TypeScript
226 lines
6.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
copyResultText,
|
|
resultScope,
|
|
resultDisplay,
|
|
queryPath,
|
|
compactAnalysisPath,
|
|
} from "./result-presentation.js";
|
|
import type { AssistantResult } from "../../../shared/assets/assistant.js";
|
|
|
|
const result: AssistantResult = {
|
|
plan: {
|
|
kind: "assets",
|
|
category: "available",
|
|
groupBy: "region",
|
|
view: "table",
|
|
filters: { type: ["4.5T"] },
|
|
},
|
|
title: "可运营库存 · 按区域统计",
|
|
summary: "当前范围共有135台可运营库存。",
|
|
source: "资产台账",
|
|
fetchedAt: "2026-09-23T08:00:00Z",
|
|
groups: [],
|
|
rows: [],
|
|
metrics: [{ label: "可运营库存", value: 135, unit: "台" }],
|
|
scope: {
|
|
company: "演示公司",
|
|
permission: "当前账号可见车辆",
|
|
filters: ["4.5T", "可运营库存"],
|
|
grouping: "区域",
|
|
period: "当前资产台账",
|
|
definitions: ["车型按吨位汇总,4.5T包含普货与冷链"],
|
|
sourceUpdatedAt: null,
|
|
},
|
|
};
|
|
|
|
test("copy preserves facts, scope, definitions and distinguishes unknown source update from query time", () => {
|
|
const text = copyResultText(result, null);
|
|
for (const expected of [
|
|
"135台",
|
|
"公司:演示公司",
|
|
"权限范围:当前账号可见车辆",
|
|
"筛选条件:4.5T;可运营库存",
|
|
"分组:区域",
|
|
"4.5T包含普货与冷链",
|
|
"源数据更新时间:未提供",
|
|
"查询时间:",
|
|
])
|
|
assert.ok(text.includes(expected), expected);
|
|
});
|
|
|
|
test("partial briefing copies missing answers and inventory evidence without inventing a source timestamp", () => {
|
|
const partial: AssistantResult = {
|
|
...result,
|
|
outcome: "partial",
|
|
unanswered: [
|
|
{ question: "闲置损失多少钱", reason: "未接入成本数据,不估算金额。" },
|
|
],
|
|
inventoryBreakdown: {
|
|
title: "库存集中情况",
|
|
summary: "4.5T冷链库存最多。",
|
|
groups: [
|
|
{ name: "4.5T冷链", count: 155, operating: 0, rate: 0, share: 53.3 },
|
|
],
|
|
},
|
|
};
|
|
const text = copyResultText(partial, null);
|
|
assert.match(text, /4.5T冷链:155台,占库存53.3%/);
|
|
assert.match(text, /尚未回答:[\s\S]*未接入成本数据,不估算金额/);
|
|
assert.match(text, /源数据更新时间:未提供/);
|
|
});
|
|
|
|
test("copying a revoked result keeps the revocation explicit", () => {
|
|
const text = copyResultText(
|
|
result,
|
|
null,
|
|
"上一条回答的是待运营,不能用于判断待交车。",
|
|
);
|
|
assert.match(
|
|
text,
|
|
/【此回答已撤回】上一条回答的是待运营,不能用于判断待交车/,
|
|
);
|
|
});
|
|
|
|
test("fallback scope keeps exclusion and selected vehicle filters", () => {
|
|
const fallback = resultScope(
|
|
{
|
|
...result,
|
|
scope: undefined,
|
|
plan: {
|
|
...result.plan,
|
|
excludeFilters: { city: ["嘉兴市"] },
|
|
vehicleIds: ["example-1"],
|
|
exception: "overdue",
|
|
},
|
|
},
|
|
"演示公司",
|
|
);
|
|
assert.deepEqual(fallback.filters, [
|
|
"可运营库存",
|
|
"车型:4.5T",
|
|
"排除城市:嘉兴市",
|
|
"指定车辆:1台",
|
|
"已超过预计完成时间",
|
|
]);
|
|
assert.equal(fallback.sourceUpdatedAt, null);
|
|
});
|
|
|
|
test("copy includes regional facts and daily flow without duplicating groups or copying vehicle details", () => {
|
|
const regional: AssistantResult = {
|
|
...result,
|
|
groups: [
|
|
{
|
|
name: "华东",
|
|
count: 99,
|
|
operating: 0,
|
|
rate: 0,
|
|
share: 73.3,
|
|
abnormal: 1,
|
|
pending: 0,
|
|
},
|
|
],
|
|
insights: ["华东占当前可运营库存73.3%。"],
|
|
rows: [
|
|
{
|
|
id: "private-vehicle",
|
|
plate: "测试车牌不应复制",
|
|
vehicleClass: "4.5T普货",
|
|
type: "4.5T",
|
|
model: "演示型号",
|
|
region: "华东",
|
|
city: "嘉兴市",
|
|
department: "演示部门",
|
|
status: "可运营",
|
|
},
|
|
],
|
|
};
|
|
const regionalText = copyResultText(regional, null);
|
|
assert.match(regionalText, /华东:99台,占比73.3%/);
|
|
assert.match(regionalText, /异动1台,待交车0台/);
|
|
assert.match(regionalText, /分析发现:[\s\S]*华东占当前可运营库存73.3%/);
|
|
assert.ok(!regionalText.includes("测试车牌不应复制"));
|
|
|
|
const flowText = copyResultText(
|
|
{
|
|
...regional,
|
|
plan: {
|
|
...regional.plan,
|
|
kind: "flow",
|
|
start: "2026-09-23",
|
|
end: "2026-09-23",
|
|
},
|
|
daily: [
|
|
{
|
|
date: "2026-09-23",
|
|
delivered: 5,
|
|
returned: 2,
|
|
replaced: 1,
|
|
total: 7,
|
|
},
|
|
],
|
|
},
|
|
null,
|
|
);
|
|
assert.match(
|
|
flowText,
|
|
/2026-09-23:交车5车次,还车2车次,其中替换交车1车次,合计7车次/,
|
|
);
|
|
assert.match(flowText, /替换交车包含在交车中,不重复相加/);
|
|
assert.ok(!flowText.includes("华东:99台"));
|
|
assert.ok(!flowText.includes("测试车牌不应复制"));
|
|
});
|
|
|
|
test("single totals use metric cards while comparisons, trends and explicit records retain their display", () => {
|
|
const total = {
|
|
...result.plan,
|
|
groupBy: "none" as const,
|
|
view: "chart" as const,
|
|
};
|
|
assert.equal(resultDisplay(total), "metrics");
|
|
assert.equal(resultDisplay({ ...total, groupBy: "region" }), "chart");
|
|
assert.equal(resultDisplay({ ...total, secondaryGroupBy: "type" }), "chart");
|
|
assert.equal(resultDisplay({ ...total, kind: "flow" }), "chart");
|
|
assert.equal(resultDisplay({ ...total, view: "list" }), "list");
|
|
assert.equal(resultDisplay({ ...total, view: "table" }), "table");
|
|
});
|
|
|
|
test("current path retains every active slice and highlights the current grouping", () => {
|
|
const path = queryPath({
|
|
...result.plan,
|
|
filters: { region: ["华东"], city: ["嘉兴市"] },
|
|
excludeFilters: { model: ["演示型号"] },
|
|
groupBy: "vehicleClass",
|
|
exception: "overdue",
|
|
});
|
|
assert.deepEqual(path, [
|
|
"可运营库存",
|
|
"区域:华东",
|
|
"城市:嘉兴市",
|
|
"排除型号:演示型号",
|
|
"已超期",
|
|
"按车型细分统计",
|
|
]);
|
|
});
|
|
|
|
test("analysis path collapses display-only steps and preserves the latest node of each scope", () => {
|
|
const path = compactAnalysisPath([
|
|
{ resultId: "all", label: "全部库存" },
|
|
{ resultId: "east-chart", label: "库存 · 华东" },
|
|
{ resultId: "east-table", label: "库存 · 华东" },
|
|
{ resultId: "jiaxing", label: "库存 · 华东 / 嘉兴" },
|
|
]);
|
|
assert.deepEqual(
|
|
path.map((crumb) => crumb.resultId),
|
|
["all", "east-table", "jiaxing"],
|
|
);
|
|
assert.equal(path.at(-2)?.label, "库存 · 华东");
|
|
});
|
|
|
|
test("vehicle status navigation identifies a vehicle answer instead of a total", () => {
|
|
const path = queryPath({ kind: "assets", category: "all", groupBy: "none", view: "list", filters: {}, vehicleIds: ["v1"], responseMode: "vehicle-status" });
|
|
assert.ok(path.includes("车辆状态"));
|
|
assert.ok(!path.includes("总量"));
|
|
});
|