Files
ln-bi/src/server/routes/asset-assistant/experience.test.ts
T
2026-09-23 17:52:33 +08:00

773 lines
26 KiB
TypeScript

import assert from "node:assert/strict";
import test, { type TestContext } from "node:test";
import { Hono } from "hono";
import router from "./index.js";
import { vehicleRepository } from "../vehicles/repository.js";
import { gapStore } from "./gaps.js";
import type { AuthUser } from "../../auth/types.js";
import type { Vehicle } from "../../types.js";
import type {
AssistantRequest,
AssistantResult,
GapReason,
QueryPlan,
} from "../../../shared/assets/assistant.js";
// End-to-end route tests use synthetic business data and mock every model call.
// They validate integration contracts, not the real model's language accuracy.
let sequence = 0;
const plan = (overrides: Partial<QueryPlan> = {}): QueryPlan => ({
kind: "assets",
category: "inventory",
groupBy: "city",
view: "table",
filters: {},
analysis: "distribution",
...overrides,
});
type Proposal = {
kind?: "query" | "answer" | "clarify";
plan: QueryPlan;
reply?: string;
reason?: GapReason;
suggestions?: string[];
baseResultId?: string;
};
function setup(t: TestContext) {
const user: AuthUser = {
userId: `experience-${++sequence}`,
userName: "体验验收",
loginName: "experience",
depCode: "experience-department",
depName: "测试部门",
permissionLevel: "personal",
roles: [],
};
const rows = [
{ id: "jx-stock", city: "嘉兴市", status: "Inventory", type: "4.5T" },
{ id: "jx-abnormal", city: "嘉兴市", status: "Abnormal", type: "18T" },
{ id: "hz-pending", city: "杭州市", status: "Pending", type: "18T" },
{ id: "hz-stock", city: "杭州市", status: "Inventory", type: "4.5T" },
{
id: "gz-stock",
city: "广州市",
province: "广东省",
status: "Inventory",
type: "4.5T",
},
{
id: "jx-running",
city: "嘉兴市",
status: "Operating",
type: "4.5T",
operationStatus: "1",
},
{
id: "not-authorized",
city: "嘉兴市",
status: "Inventory",
type: "18T",
managerId: "another-user",
},
].map(
(v) =>
({
province: "浙江省",
model: "测试型号",
subjectOrg: "测试公司",
plateNumber: `测试车-${v.id}`,
managerId: user.userId,
departmentName: user.depName,
operationStatus: "3",
...v,
...(v.status === "Abnormal"
? {
abnormalMove: {
type: "维修",
targetCompletionTime: "2020-01-01 12:00:00",
actualCompletionTime: null,
},
}
: {}),
}) as Vehicle,
);
t.mock.method(vehicleRepository, "getVehicles", async () => rows);
t.mock.method(vehicleRepository, "getFlowDetailRows", async () => []);
const recorded: { demand: string; reason: GapReason }[] = [];
t.mock.method(
gapStore,
"record",
async (_user: string, demand: string, reason: GapReason) => {
recorded.push({ demand, reason });
},
);
const originalKey = process.env.DEEPSEEK_API_KEY;
const originalEnabled = process.env.ASSET_AI_EXTERNAL_ENABLED;
process.env.DEEPSEEK_API_KEY = "synthetic-test-key";
process.env.ASSET_AI_EXTERNAL_ENABLED = "1";
t.after(() => {
if (originalKey === undefined) delete process.env.DEEPSEEK_API_KEY;
else process.env.DEEPSEEK_API_KEY = originalKey;
if (originalEnabled === undefined)
delete process.env.ASSET_AI_EXTERNAL_ENABLED;
else process.env.ASSET_AI_EXTERNAL_ENABLED = originalEnabled;
});
// These scenarios assert planning behavior. A minimal valid presentation must
// not consume the queued planning response for the next user turn.
const requests: string[] = [];
const presentationRequests: string[] = [];
const responses: (Proposal | Error | string)[] = [];
t.mock.method(
globalThis,
"fetch",
async (_url: unknown, init?: RequestInit) => {
const body = JSON.parse(String(init?.body));
const context = JSON.parse(body.messages.find((message: { role: string }) => message.role === "user").content);
if (context.stage === "presentation") {
presentationRequests.push(String(init?.body));
const presentation = { layout: "stack", results: context.results.map((result: { id: string }) => ({ id: result.id, blocks: [{ id: "summary", kind: "summary" }], interactions: [] })) };
return new Response(JSON.stringify({ choices: [{ message: { content: JSON.stringify(presentation) }, finish_reason: "stop" }] }));
}
requests.push(String(init?.body));
assert.ok(
responses.length,
"Unexpected model request: all responses must be explicit",
);
const response = responses.shift()!;
if (response instanceof Error) throw response;
const content =
typeof response === "string"
? response
: JSON.stringify({
...response,
kind:
response.kind ||
(response.plan.kind === "clarify" ? "clarify" : "query"),
reply: response.reply || response.plan.clarification,
});
return new Response(
JSON.stringify({
choices: [
{
message: {
content,
},
finish_reason: "stop",
},
],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
},
);
const app = new Hono<{ Variables: { user: AuthUser } }>();
app.use("*", async (c, next) => {
c.set("user", user);
await next();
});
app.route("/assistant", router);
async function ask(input: Partial<AssistantRequest>) {
const response = await app.request("/assistant/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subject: null, ...input }),
});
assert.equal(response.status, 200);
const events = (await response.text())
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => JSON.parse(line.slice(5)));
const failure = events.find((event) => event.type === "error");
assert.equal(failure, undefined, failure?.message);
const result = events.find((event) => event.type === "result")?.result;
assert.ok(result, "Expected completed assistant result");
return result as AssistantResult;
}
return { ask, responses, requests, presentationRequests, recorded, rows };
}
const based = (result: AssistantResult) => ({
sessionId: result.sessionId,
baseResultId: result.resultId,
});
const ids = (result: AssistantResult) =>
result.rows.map((row) => row.id).sort();
test("natural questions reach the model and its business filters are really executed", async (t) => {
const { ask, responses, requests } = setup(t);
const message = "帮我瞅瞅嘉兴现在还有几台闲着的车,做成城市统计就行";
responses.push({ plan: plan({ filters: { city: ["嘉兴市"] } }) });
const result = await ask({ message });
assert.equal(
requests.length,
1,
"Natural wording must not be rejected by a closed vocabulary",
);
assert.ok(requests[0].includes(message));
assert.equal(result.plan.category, "inventory");
assert.deepEqual(result.plan.filters, { city: ["嘉兴市"] });
assert.deepEqual(ids(result), ["jx-abnormal", "jx-stock"]);
assert.equal(result.metrics[0].value, 2);
assert.equal(result.gap, undefined);
});
test("five-turn analysis sends conversation context and supports correction, detail and reset", async (t) => {
const { ask, responses, requests } = setup(t);
const firstQuestion = "先帮我看一下华东的库存,后面我们再逐个城市研究";
responses.push({ plan: plan({ filters: { region: ["华东"] } }) });
const root = await ask({ message: firstQuestion });
assert.equal(root.rows.length, 4);
responses.push({
plan: plan({ filters: { region: ["华东"], city: ["嘉兴市"] } }),
});
const city = await ask({ ...based(root), message: "其中嘉兴呢?" });
assert.equal(city.rows.length, 2);
responses.push({
plan: plan({ filters: { region: ["华东"], city: ["杭州市"] } }),
});
const corrected = await ask({
...based(city),
message: "不对,我想看杭州,不是嘉兴",
});
assert.deepEqual(ids(corrected), ["hz-pending", "hz-stock"]);
responses.push({
plan: plan({
category: "pending",
view: "list",
filters: { region: ["华东"], city: ["杭州市"] },
}),
});
const detail = await ask({
...based(corrected),
message: "这里面待交车的展开看看",
});
assert.deepEqual(ids(detail), ["hz-pending"]);
responses.push({ plan: plan() });
const reset = await ask({ ...based(detail), message: "重新看全部库存" });
assert.equal(reset.rows.length, 5);
assert.deepEqual(reset.plan.filters, {});
const fourthBody = JSON.stringify(JSON.parse(requests[3]));
assert.ok(
fourthBody.includes(firstQuestion),
"Recent conversation must reach the model",
);
assert.ok(fourthBody.includes("不对,我想看杭州,不是嘉兴"));
assert.ok(fourthBody.includes("杭州市"));
});
test("clarification preserves the selected analysis for the answer that follows", async (t) => {
const { ask, responses, requests, recorded } = setup(t);
const scoped = plan({ filters: { city: ["嘉兴市"] } });
responses.push({ plan: scoped });
const root = await ask({ message: "看看嘉兴库存" });
const clarification = "你说的到期,是异动预计完成时间,还是合同到期?";
responses.push({
plan: { ...scoped, kind: "clarify", clarification },
reply: clarification,
suggestions: ["异动预计完成时间"],
});
const question = await ask({ ...based(root), message: "其中到期的有哪些?" });
assert.equal(question.plan.kind, "clarify");
assert.equal(
recorded.length,
0,
"Normal clarification is not an unmet capability",
);
responses.push({
plan: plan({
category: "abnormal",
exception: "overdue",
view: "list",
filters: scoped.filters,
}),
});
const answer = await ask({
...based(question),
message: "我指的是异动预计完成时间,已经超过的",
});
assert.deepEqual(answer.plan.filters, scoped.filters);
assert.deepEqual(ids(answer), ["jx-abnormal"]);
assert.equal(answer.rows[0].abnormalType, "维修");
assert.equal(answer.rows[0].targetCompletionTime, "2020-01-01 12:00:00");
assert.ok(
requests.at(-1)?.includes(clarification),
"A clarification must appear in conversational context",
);
});
test("help and valid zero-match answers do not pollute the unmet-demand inbox", async (t) => {
const { ask, responses, recorded } = setup(t);
responses.push({
kind: "answer",
plan: plan({
kind: "clarify",
clarification:
"可以分析库存、运营、异动和交退换车,也可以继续下钻到车辆明细。",
}),
reply: "可以分析库存、运营、异动和交退换车,也可以继续下钻到车辆明细。",
suggestions: ["各区域库存有多少", "看看异动情况"],
});
const help = await ask({ message: "你能帮我分析些什么?" });
assert.equal(help.gap, undefined);
assert.equal(recorded.length, 0);
responses.push({
plan: plan({ category: "pending", filters: { city: ["嘉兴市"] } }),
});
const empty = await ask({ ...based(help), message: "嘉兴有待交车吗?" });
assert.equal(empty.plan.kind, "assets");
assert.equal(empty.rows.length, 0);
assert.equal(empty.metrics[0].value, 0);
assert.equal(empty.gap, undefined);
assert.equal(recorded.length, 0);
});
test("unsupported business requests retain useful demand wording and are recorded", async (t) => {
const { ask, responses, recorded } = setup(t);
const message = "能按每台车的租赁收入减掉维修成本算毛利吗?";
responses.push({
plan: plan({
kind: "clarify",
clarification:
"目前缺少租赁收入和维修成本,不能可靠计算单车毛利。可以先查看车辆与异动分布。",
}),
reason: "unsupported",
suggestions: ["查看异动车辆"],
});
const result = await ask({ message });
assert.equal(result.plan.kind, "clarify");
assert.equal(result.gap?.reason, "unsupported");
assert.equal(result.gap.recorded, true);
assert.equal(recorded.length, 1);
assert.match(recorded[0].demand, /毛利/);
assert.match(recorded[0].demand, /租赁|收入/);
});
test("a model-selected historical result branches from that result rather than the latest city", async (t) => {
const { ask, responses, requests } = setup(t);
responses.push({ plan: plan({ filters: { region: ["华东"] } }) });
const root = await ask({ message: "看华东库存" });
responses.push({
plan: plan({ filters: { region: ["华东"], city: ["嘉兴市"] } }),
});
const city = await ask({ ...based(root), message: "只看嘉兴" });
responses.push({
plan: plan({ groupBy: "type", filters: { region: ["华东"] } }),
baseResultId: root.resultId,
});
const branch = await ask({
...based(city),
message: "回到第一张图,按车型拆开",
});
assert.ok(
requests.at(-1)?.includes(root.resultId!),
"Model must receive selectable historical result identifiers",
);
assert.deepEqual(branch.plan.filters, { region: ["华东"] });
assert.equal(branch.rows.length, 4);
assert.equal(branch.plan.groupBy, "type");
assert.equal(branch.breadcrumbs?.at(-2)?.resultId, root.resultId);
assert.ok(
!branch.breadcrumbs?.some((item) => item.resultId === city.resultId),
);
});
test("model outage and malformed output use an honest scoped fallback", async (t) => {
const { ask, responses } = setup(t);
for (const failedResponse of [
new Error("Synthetic model outage"),
"{not valid JSON",
]) {
responses.push(failedResponse);
const result = await ask({ message: "统计嘉兴市库存" });
assert.deepEqual(result.plan.filters, { city: ["嘉兴市"] });
assert.deepEqual(ids(result), ["jx-abnormal", "jx-stock"]);
assert.match(result.warning || "", /规则|降级|暂不可用|连接|模型/);
assert.ok(!JSON.stringify(result).includes("Synthetic model outage"));
}
});
test("ranking, exclusions and their removal change results without distorting totals", async (t) => {
const { ask, responses } = setup(t);
responses.push({
plan: plan({ topN: 1, sortBy: "count", sortOrder: "asc" }),
});
const ranked = await ask({ message: "库存最少的那个城市是哪一个?" });
assert.equal(ranked.groups.length, 1);
assert.equal(ranked.groups[0].name, "广州市");
assert.equal(ranked.groups[0].count, 1);
assert.equal(
ranked.metrics[0].value,
5,
"Top N must not replace the full-scope total",
);
assert.equal(ranked.groups[0].share, 20);
responses.push({
plan: plan({
filters: { region: ["华东"] },
excludeFilters: { city: ["杭州市"] },
}),
});
const excluded = await ask({
...based(ranked),
message: "改看华东库存,杭州先不要",
});
assert.deepEqual(ids(excluded), ["jx-abnormal", "jx-stock"]);
assert.equal(excluded.plan.topN, undefined);
responses.push({ plan: plan({ filters: { region: ["华东"] } }) });
const restored = await ask({
...based(excluded),
message: "还是把杭州加回来吧",
});
assert.equal(restored.rows.length, 4);
assert.equal(restored.plan.excludeFilters, undefined);
});
test("a specific vehicle can be selected without broadening the user's authorized data", async (t) => {
const { ask, responses } = setup(t);
responses.push({
plan: plan({
category: "all",
groupBy: "none",
view: "list",
vehicleIds: ["jx-abnormal"],
}),
});
const result = await ask({
message: "帮我看看测试车-jx-abnormal现在的维修情况和预计结束时间",
});
assert.deepEqual(ids(result), ["jx-abnormal"]);
assert.equal(result.rows[0].abnormalType, "维修");
assert.equal(result.rows[0].targetCompletionTime, "2020-01-01 12:00:00");
assert.ok(!ids(result).includes("not-authorized"));
});
test("clearing a filter and resetting the scope are deterministic and do not need another planning call", async (t) => {
const { ask, responses, requests } = setup(t);
responses.push({
plan: plan({
filters: { region: ["华东"], city: ["嘉兴市"] },
excludeFilters: { city: ["杭州市"] },
}),
});
const root = await ask({ message: "看华东嘉兴库存" });
assert.equal(root.rows.length, 2);
const cleared = await ask({
...based(root),
action: { type: "clear-filter", dimension: "city" },
});
assert.deepEqual(cleared.plan.filters, { region: ["华东"] });
assert.equal(cleared.plan.excludeFilters?.city, undefined);
assert.equal(cleared.rows.length, 4);
const ranked = await ask({
...based(cleared),
action: { type: "sort", sortBy: "count", sortOrder: "desc", topN: 1 },
});
assert.equal(ranked.groups.length, 1);
const reset = await ask({
...based(ranked),
action: { type: "reset-filters" },
});
assert.deepEqual(reset.plan.filters, {});
assert.equal(reset.plan.excludeFilters, undefined);
assert.equal(reset.plan.topN, undefined);
assert.equal(reset.rows.length, 5);
assert.equal(requests.length, 1);
});
test("upcoming and missing due-date analysis selects actual abnormal records", async (t) => {
const { ask, responses, rows } = setup(t);
const abnormal = rows.find((row) => row.id === "jx-abnormal")!;
const future = new Date(Date.now() + 2 * 86400000).toISOString();
rows.push(
{
...abnormal,
id: "due-soon",
abnormalMove: { ...abnormal.abnormalMove!, targetCompletionTime: future },
},
{
...abnormal,
id: "already-completed",
abnormalMove: {
...abnormal.abnormalMove!,
targetCompletionTime: future,
actualCompletionTime: new Date().toISOString(),
},
},
{
...abnormal,
id: "far-future",
abnormalMove: {
...abnormal.abnormalMove!,
targetCompletionTime: new Date(
Date.now() + 30 * 86400000,
).toISOString(),
},
},
{
...abnormal,
id: "missing-due-date",
abnormalMove: { ...abnormal.abnormalMove!, targetCompletionTime: null },
},
);
responses.push({
plan: plan({
category: "abnormal",
view: "list",
exception: "dueSoon",
filters: { city: ["嘉兴市"] },
}),
});
const upcoming = await ask({ message: "嘉兴的异动哪些未来七天就该结束了?" });
assert.deepEqual(ids(upcoming), ["due-soon"]);
responses.push({
plan: plan({
category: "abnormal",
view: "list",
exception: "missingDueDate",
filters: { city: ["嘉兴市"] },
}),
});
const missing = await ask({
...based(upcoming),
message: "那些连预计结束时间都没填的呢?",
});
assert.deepEqual(ids(missing), ["missing-due-date"]);
assert.equal(missing.rows[0].targetCompletionTime, undefined);
const reset = await ask({
...based(missing),
action: { type: "reset-filters" },
});
assert.equal(reset.plan.exception, undefined);
assert.deepEqual(reset.plan.filters, {});
assert.equal(
reset.plan.category,
"abnormal",
"Reset filters retains the selected business category",
);
});
test("model outage must not answer Chinese historical-stock questions with current inventory", async (t) => {
const { ask, responses } = setup(t);
for (const message of [
"2020年8月31日嘉兴库存有多少?",
"去年年底的库存是多少?",
]) {
responses.push(new Error("Synthetic model outage"));
const result = await ask({ message });
assert.equal(result.plan.kind, "clarify", message);
assert.equal(
result.rows.length,
0,
"Current vehicles are not historical inventory evidence",
);
assert.equal(result.gap?.reason, "unsupported");
assert.match(result.summary, /历史|快照|尚未|暂不/);
}
});
test("historical flow period survives display changes and day drill but clears on current-assets switch", async (t) => {
const { ask, responses, requests } = setup(t);
t.mock.method(
vehicleRepository,
"getFlowDetailRows",
async (start: string, end: string) =>
[
{
id: "flow-a",
truck_id: "jx-stock",
stat_date: "2020-08-10",
type: "delivered",
type_label: "交车",
},
{
id: "flow-b",
truck_id: "jx-abnormal",
stat_date: "2020-08-11",
type: "replaced",
type_label: "替换",
},
{
id: "flow-c",
truck_id: "hz-stock",
stat_date: "2020-08-11",
type: "returned",
type_label: "还车",
},
{
id: "flow-d",
truck_id: "not-authorized",
stat_date: "2020-08-11",
type: "delivered",
type_label: "交车",
},
].filter((event) => event.stat_date >= start && event.stat_date <= end),
);
responses.push({
plan: plan({
kind: "flow",
category: "all",
groupBy: "none",
start: "2020-08-10",
end: "2020-08-12",
comparePrevious: true,
filters: { city: ["嘉兴市"] },
}),
});
const flow = await ask({
message: "嘉兴2020年8月10日至12日的交退换,和上一等长周期对比",
});
assert.deepEqual(ids(flow), ["flow-a", "flow-b"]);
assert.deepEqual(flow.comparisonPeriod, {
start: "2020-08-07",
end: "2020-08-09",
});
const table = await ask({
...based(flow),
action: { type: "view", view: "table" },
});
assert.equal(table.plan.start, "2020-08-10");
assert.equal(table.plan.end, "2020-08-12");
assert.equal(table.plan.comparePrevious, true);
const day = await ask({
...based(table),
action: { type: "day", date: "2020-08-11" },
});
assert.equal(day.plan.start, "2020-08-11");
assert.equal(day.plan.end, "2020-08-11");
assert.equal(day.plan.comparePrevious, false);
assert.deepEqual(ids(day), ["flow-b"]);
assert.equal(
day.metrics[0].value,
1,
"Replacement delivery is included exactly once",
);
assert.equal(day.metrics[2].value, 1);
const current = await ask({
...based(day),
action: { type: "category", category: "inventory" },
});
assert.equal(current.plan.kind, "assets");
assert.equal(current.plan.start, undefined);
assert.equal(current.plan.end, undefined);
assert.equal(current.plan.comparePrevious, undefined);
assert.equal(current.comparison, undefined);
assert.deepEqual(current.plan.filters, { city: ["嘉兴市"] });
assert.deepEqual(ids(current), ["jx-abnormal", "jx-stock"]);
assert.equal(requests.length, 1);
});
test("visible interpretation includes exception scope and clears it after a category change", async (t) => {
const { ask, responses } = setup(t);
responses.push({
plan: plan({
category: "abnormal",
exception: "overdue",
filters: { region: ["华东"] },
excludeFilters: { city: ["杭州市"] },
}),
});
const overdue = await ask({
message: "华东除了杭州,已经超过预计完成时间的异动车有哪些?",
});
assert.match(overdue.interpretation || "", /华东/);
assert.match(overdue.interpretation || "", /排除城市:杭州市/);
assert.match(overdue.interpretation || "", /超期|逾期|超过|已超/);
const pending = await ask({
...based(overdue),
action: { type: "category", category: "pending" },
});
assert.equal(pending.plan.exception, undefined);
assert.doesNotMatch(pending.interpretation || "", /超期|逾期|超过|已超/);
assert.equal(
pending.gap,
undefined,
"A valid empty subset is not a demand gap",
);
});
test("unmet follow-up demands keep enough prior scope to be actionable outside the chat", async (t) => {
const { ask, responses, recorded } = setup(t);
responses.push({ plan: plan({ filters: { city: ["嘉兴市"] } }) });
const current = await ask({ message: "嘉兴库存按车型拆开" });
responses.push({
kind: "clarify",
plan: plan({
kind: "clarify",
clarification: "目前没有历史库存快照,暂时无法计算去年同期。",
}),
reason: "unsupported",
});
const missing = await ask({ ...based(current), message: "去年同期呢?" });
assert.equal(missing.gap?.recorded, true);
assert.equal(recorded.length, 1);
assert.match(recorded[0].demand, /去年同期/);
assert.match(recorded[0].demand, /嘉兴/);
assert.match(recorded[0].demand, /库存/);
});
test("clarification between a result and an unsupported follow-up keeps business context in the demand inbox", async (t) => {
const { ask, responses, recorded } = setup(t);
responses.push({ plan: plan({ filters: { city: ["嘉兴市"] } }) });
const current = await ask({ message: "嘉兴库存有多少?" });
responses.push({
kind: "clarify",
plan: plan({ kind: "clarify", clarification: "你想对比哪个时间段?" }),
});
const clarification = await ask({
...based(current),
message: "跟以前比一下",
});
responses.push({
kind: "clarify",
plan: plan({
kind: "clarify",
clarification: "缺少历史库存快照,暂时无法计算去年同期。",
}),
reason: "unsupported",
});
await ask({ ...based(clarification), message: "去年同期呢?" });
assert.equal(recorded.length, 1);
assert.match(recorded[0].demand, /去年同期/);
assert.match(recorded[0].demand, /嘉兴/);
assert.match(recorded[0].demand, /库存/);
});
test("references to ranked results receive the selected result's aggregate evidence", async (t) => {
const { ask, responses, requests } = setup(t);
responses.push({
plan: plan({
groupBy: "region",
topN: 2,
sortBy: "count",
sortOrder: "desc",
}),
});
const ranked = await ask({ message: "哪些区域库存最多?" });
assert.equal(ranked.groups[0].name, "华东");
responses.push({
plan: plan({ filters: { region: ["华东"] }, groupBy: "city" }),
});
const detail = await ask({
...based(ranked),
message: "数量最多的那个区域,再按城市拆开",
});
const request = JSON.parse(requests.at(-1)!);
const context = JSON.parse(
[...request.messages]
.reverse()
.find((message: { role: string }) => message.role === "user").content,
);
assert.ok(
context.resultContext,
"Ranking references need the actual result, not just its query plan",
);
assert.ok(
context.resultContext.groups.some(
(group: { name: string; count: number }) =>
group.name === "华东" && group.count === 4,
),
);
assert.deepEqual(detail.plan.filters, { region: ["华东"] });
assert.equal(detail.rows.length, 4);
});