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 => ({ 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", }, { // Inventory is public; use another owner's operating vehicle to test restricted data. id: "not-authorized", city: "嘉兴市", status: "Operating", operationStatus: "1", 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) { 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); }); test("department operating situation repairs an all-assets proposal and preserves scope on drill", async (t) => { const { ask, responses } = setup(t); for (const message of ["各部门车辆运营情况", "各部门车辆的运营情况"]) { responses.push({ plan: plan({ category: "all", groupBy: "department" }) }); const result = await ask({ message }); assert.equal(result.plan.category, "operating"); assert.equal(result.plan.groupBy, "department"); assert.deepEqual(result.rows.map(row => row.id), ["jx-running"]); assert.deepEqual(result.metrics, [{ label: "运营车辆", value: 1, unit: "台" }]); const detail = await ask({ ...based(result), action: { type: "drill", keys: result.groups[0].keys! } }); assert.equal(detail.plan.category, "operating"); assert.deepEqual(detail.rows.map(row => row.id), ["jx-running"]); } }); test("department operating rate retains all assets as its denominator", async (t) => { const { ask, responses } = setup(t); responses.push({ plan: plan({ category: "all", groupBy: "department" }) }); const result = await ask({ message: "各部门车辆运营情况和运营率" }); assert.equal(result.plan.category, "all"); assert.equal(result.rows.length, 6); assert.equal(result.metrics.find(metric => metric.label === "运营率")?.value, 16.7); });