import { beforeEach } from 'node:test'; import { questionLog } from './question-log.js'; beforeEach(t => { if (!('mock' in t)) throw new Error('Expected test context'); t.mock.method(questionLog, 'start', async () => 'synthetic-question'); t.mock.method(questionLog, 'finish', async () => {}); }); import assert from "node:assert/strict"; import test from "node:test"; import { Hono } from "hono"; import router from "./index.js"; import { vehicleRepository } from "../vehicles/repository.js"; import { gapStore } from "./gaps.js"; import { projectQuestion, externalContext } from "./privacy.js"; import { initialPlan, localPlan, modelPlan } from "./planner.js"; import { assetResult, validatePlan } from "./model.js"; import { execute, applyAction } from "./execute.js"; import { acquire, getSession, addNode, getNode } from "./session.js"; import type { Vehicle } from "../../types.js"; import type { AuthUser } from "../../auth/types.js"; import type { AssistantResult, AssistantRequest, GapReason, } from "../../../shared/assets/assistant.js"; process.env.ASSET_AI_EXTERNAL_ENABLED = "0"; const user: AuthUser = { userId: "analysis", userName: "测试", loginName: "test", depCode: "D", depName: "测试部门", permissionLevel: "personal", roles: [], }; const vehicles = [ { id: "1", status: "Inventory", city: "嘉兴市", type: "4.5T" }, { id: "2", status: "Abnormal", city: "嘉兴市", type: "18T" }, { id: "3", status: "Pending", city: "杭州市", type: "18T" }, { id: "4", status: "Inventory", city: "广州市", type: "4.5T", province: "广东省", }, { id: "5", status: "Operating", city: "嘉兴市", type: "4.5T", operationStatus: "1", }, { id: "6", status: "Inventory", city: "嘉兴市", type: "18T", managerId: "other", }, ].map( (v) => ({ province: "浙江省", model: "保密型号甲", subjectOrg: "保密公司甲", plateNumber: `测试车牌${v.id}`, managerId: user.userId, departmentName: "保密部门甲", operationStatus: "3", ...v, ...(v.status === "Abnormal" ? { abnormalMove: { type: "维修", targetCompletionTime: "2020-01-01 12:00:00", actualCompletionTime: null, }, } : {}), }) as Vehicle, ); function appFor(u: AuthUser = user) { const app = new Hono<{ Variables: { user: AuthUser } }>(); app.use("*", async (c, next) => { c.set("user", u); await next(); }); app.route("/ai", router); return app; } async function ask( app: ReturnType, input: Partial, ) { const response = await app.request("/ai/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((s) => s.startsWith("data:")) .map((s) => JSON.parse(s.slice(5))); const error = events.find((e) => e.type === "error"); assert.equal(error, undefined, error?.message); return events.find((e) => e.type === "result").result as AssistantResult; } const based = (r: AssistantResult) => ({ sessionId: r.sessionId, baseResultId: r.resultId, }); test("natural question and relevant catalog remain available for semantic planning", () => { const question = "只看嘉兴市的库存,再解释一下有哪些值得关注"; const input = projectQuestion(question, vehicles); const context = externalContext(input); assert.equal(context.message, question); assert.ok(context.catalog.city?.includes("嘉兴市")); assert.equal( projectQuestion("查询天狼星客户的利润", vehicles).demand, "查询天狼星客户的利润", ); assert.ok( !projectQuestion("sk-testsecret123456", vehicles).demand.includes( "testsecret", ), ); }); test("multi-level clicks and follow-ups preserve exact scope, branching and reconcile groups", async (t) => { t.mock.method(vehicleRepository, "getVehicles", async () => vehicles); t.mock.method(gapStore, "record", async () => {}); const app = appFor(); const root = await ask(app, { message: "各区域的库存车辆有多少?按数量排序。", }); assert.equal(root.rows.length, 5); assert.equal(root.plan.category, "inventory"); const region = await ask(app, { ...based(root), action: { type: "drill", keys: { region: "华东" } }, }); assert.deepEqual(region.plan.filters, { region: ["华东"] }); assert.equal(region.plan.groupBy, "city"); assert.equal(region.rows.length, 4); const city = await ask(app, { ...based(region), action: { type: "drill", keys: { city: "嘉兴市" } }, }); assert.deepEqual(city.plan.filters, { region: ["华东"], city: ["嘉兴市"] }); assert.equal(city.rows.length, 3); const types = await ask(app, { ...based(city), message: "再按车型拆分" }); assert.deepEqual(types.plan.filters, city.plan.filters); assert.equal(types.plan.groupBy, "type"); const subsets = await ask(app, { ...based(types), message: "这些车里面有多少异动和待交车", }); assert.equal(subsets.plan.category, "inventory"); assert.deepEqual(subsets.plan.filters, city.plan.filters); assert.equal(subsets.metrics.find((m) => m.label === "其中异动")?.value, 1); assert.equal(subsets.metrics.find((m) => m.label === "其中待交车")?.value, 0); const detail = await ask(app, { ...based(subsets), message: "显示异动车辆明细", }); assert.equal(detail.rows.length, 1); assert.equal(detail.rows[0].abnormalType, "维修"); assert.equal(detail.rows[0].targetCompletionTime, "2020-01-01 12:00:00"); const branch = await ask(app, { ...based(root), action: { type: "restore" }, }); assert.deepEqual(branch.plan.filters, {}); assert.equal(branch.rows.length, 5); assert.equal(detail.breadcrumbs?.length, 6); for (const r of [root, region, city, types, subsets, detail]) assert.equal( r.groups.reduce((n, g) => n + g.count, 0), r.rows.length, ); }); test("cross dimensions, exact values, anomalies and comparisons are computed internally", () => { const plan = localPlan( projectQuestion("库存按区域和车型交叉统计", vehicles), ).plan; assert.equal(plan.secondaryGroupBy, "type"); const result = assetResult(vehicles, plan); assert.equal( result.groups.reduce((n, g) => n + g.count, 0), 5, ); assert.equal( result.exceptions?.find((e) => e.label === "异动已超过预计完成时间")?.count, 1, ); assert.equal( assetResult(vehicles, { ...plan, filters: { city: ["嘉兴"] } }).rows.length, 0, ); const comparison = assetResult(vehicles, { ...plan, analysis: "compare" }); assert.ok(comparison.insights?.some((s) => s.includes("不代表历史增长"))); assert.throws(() => applyAction( plan, { type: "drill", keys: { region: "未授权地区" } }, result, ), ); assert.throws(() => validatePlan({ ...plan, filters: { sql: ["select"] } })); }); test("same-duration flow comparisons include replacements once and handle zero baseline", async (t) => { t.mock.method( vehicleRepository, "getFlowDetailRows", async (start: string) => start === "2026-09-10" ? [ { id: "a", truck_id: "1", stat_date: "2026-09-10", type: "replaced", type_label: "替换", }, { id: "b", truck_id: "2", stat_date: "2026-09-10", type: "returned", type_label: "还车", }, { id: "c", truck_id: "unauthorized", stat_date: "2026-09-10", type: "delivered", type_label: "交车", }, ] : [], ); const result = await execute(vehicles, { ...initialPlan(), kind: "flow", groupBy: "none", start: "2026-09-10", end: "2026-09-12", comparePrevious: true, }); assert.equal(result.rows.length, 2); assert.equal(result.metrics[0].value, 1); assert.deepEqual(result.comparisonPeriod, { start: "2026-09-07", end: "2026-09-09", }); assert.equal(result.comparison?.[0].rate, null); const follow = localPlan( projectQuestion("改成表格", vehicles, result.plan), result.plan, ).plan; assert.equal(follow.start, "2026-09-10"); assert.equal(follow.comparePrevious, true); }); test("sessions bind user, company and permission fingerprint; exports reapply current visibility", async (t) => { let current = vehicles; t.mock.method(vehicleRepository, "getVehicles", async () => current); const app = appFor({ ...user, userId: "owner", permissionLevel: "full" }); const result = await ask(app, { message: "统计库存" }); const other = appFor({ ...user, userId: "intruder" }); for (const target of [other, app]) { const r = await target.request("/ai/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...based(result), subject: target === app ? "另一家公司" : null, message: "显示明细", }), }); assert.equal(r.status, 409); } current = vehicles.slice(0, 1); const exported = await app.request("/ai/export", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: result.sessionId, resultId: result.resultId, subject: null, }), }); assert.equal(exported.status, 200); assert.ok(!(await exported.text()).includes("测试车牌2")); const s = getSession(user, null), n = addNode(s, { plan: initialPlan(), label: "root", demand: "库存" }); assert.throws(() => getSession({ ...user, permissionLevel: "full" }, null, s.id), ); assert.equal(getNode(s, n.id)?.id, n.id); }); test("unsupported demands preserve intent and local mode records feedback once", async (t) => { t.mock.method(vehicleRepository, "getVehicles", async () => vehicles); const recorded: { demand: string; reason: GapReason }[] = []; t.mock.method( gapStore, "record", async (_u: string, demand: string, reason: GapReason) => { recorded.push({ demand, reason }); }, ); const fetch = t.mock.method(globalThis, "fetch", async () => { throw new Error("No network expected"); }); const app = appFor({ ...user, userId: "gaps", permissionLevel: "full" }); for (const [message, reason] of [ ["预测库存", "unsupported"], ["统计天狼星的利润", "unsupported"], ] as const) { const r = await ask(app, { message }); assert.equal(r.gap?.reason, reason); assert.equal(r.gap.recorded, true); assert.equal(r.plan.kind, "clarify"); } assert.equal(fetch.mock.callCount(), 0); assert.equal(recorded.length, 2); assert.ok(recorded.some((r) => r.demand.includes("天狼星"))); const result = await ask(app, { message: "库存统计" }); const feedback = () => app.request("/ai/feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: result.sessionId, resultId: result.resultId, subject: null, }), }); assert.equal((await feedback()).status, 200); assert.equal((await feedback()).status, 200); assert.equal(recorded.filter((r) => r.reason === "unsatisfied").length, 1); assert.equal((await app.request("/ai/gaps?scope=all")).status, 403); assert.equal( ( await app.request("/ai/gaps/1", { method: "PATCH", body: JSON.stringify({ status: "resolved" }), headers: { "Content-Type": "application/json" }, }) ).status, 403, ); }); test("storage failure is visible and must never claim a requirement was saved", async (t) => { t.mock.method(vehicleRepository, "getVehicles", async () => vehicles); t.mock.method(gapStore, "record", async () => { throw new Error("sensitive database diagnostic"); }); const result = await ask(appFor({ ...user, userId: "failure" }), { message: "预测库存", }); assert.equal(result.gap?.recorded, false); assert.ok(!JSON.stringify(result).includes("sensitive database")); }); test("model can choose a full plan using natural question and contextual catalog", async (t) => { const previous = { ...initialPlan(), filters: { city: ["嘉兴市"] } }; const safe = projectQuestion("改为杭州市库存", vehicles, previous); let body = ""; t.mock.method( globalThis, "fetch", async (_url: unknown, init?: RequestInit) => { body = String(init?.body); return new Response( JSON.stringify({ choices: [ { message: { content: JSON.stringify({ kind: "query", plan: { ...initialPlan(), category: "inventory", filters: { city: ["杭州市"] }, }, }), }, }, ], }), ); }, ); const plan = await modelPlan( safe, previous, initialPlan(), new AbortController().signal, ); assert.deepEqual(plan.filters.city, ["杭州市"]); assert.ok(body.includes("改为杭州市库存")); assert.ok(body.includes("嘉兴市")); }); test("only concurrent duplicate work is limited, ordinary long analysis is not quota-blocked", () => { const release = acquire("rate-test"); assert.ok(release); assert.equal(acquire("rate-test"), null); release(); for (let i = 1; i < 20; i++) { const r = acquire("rate-test"); assert.ok(r); r(); } const next = acquire("rate-test"); assert.ok(next); next(); }); test("overdue action returns only unfinished, overdue abnormal vehicles and retains scope", () => { const plan = { ...initialPlan(), category: "inventory" as const, filters: { region: ["华东"] }, }; const next = applyAction( plan, { type: "exception", exception: "overdue" }, assetResult(vehicles, plan), ); assert.equal(next.exception, "overdue"); assert.equal(next.view, "list"); const result = assetResult(vehicles, next); assert.deepEqual( result.rows.map((r) => r.id), ["2"], ); assert.deepEqual(next.filters, plan.filters); const cleared = applyAction( next, { type: "category", category: "pending" }, result, ); assert.equal(cleared.exception, undefined); }); test("malformed partial planner output cannot become an executable query", async (t) => { const safe = projectQuestion("统计库存", vehicles); t.mock.method( globalThis, "fetch", async () => new Response( JSON.stringify({ choices: [ { message: { content: JSON.stringify({ groupBy: "region", filters: { region: ["secret"] }, url: "https://invalid.example", }), }, }, ], }), ), ); await assert.rejects(() => modelPlan(safe, undefined, initialPlan(), new AbortController().signal), ); });