import assert from "node:assert/strict"; import test from "node:test"; import { Hono } from "hono"; import { assetResult, validatePlan, visibleVehicles } from "./model.js"; import router from "./index.js"; import { vehicleRepository } from "../vehicles/repository.js"; import type { Vehicle } from "../../types.js"; import type { AuthUser } from "../../auth/types.js"; const user: AuthUser = { userId: "u1", userName: "测试", loginName: "test", depName: "甲部门", depCode: "A", permissionLevel: "personal", roles: [], }; const plan = { kind: "assets", category: "inventory", groupBy: "region", view: "chart", filters: {}, }; const vehicles = ["Inventory", "Abnormal", "Pending", "Operating"].map( (status, index) => ({ id: String(index), status, operationStatus: index === 3 ? "1" : "3", managerId: index === 1 ? "u2" : "u1", departmentName: "甲部门", subjectOrg: index === 2 ? "乙公司" : "甲公司", province: "浙江省", city: "嘉兴市", plateNumber: `测试${index}`, type: "4.5T", model: "测试型号", }) as Vehicle, ); test("public inventory and company filters apply before aggregation and detail generation", () => { const visible = visibleVehicles(vehicles, user, "甲公司"); const result = assetResult(visible, validatePlan(plan)); assert.equal(result.metrics[0].value, 2); assert.equal(result.groups[0].count, result.rows.length); assert.deepEqual( result.rows.map((v) => v.id), ["0", "1"], ); }); test("Shanghai counts include districts and preserve permission and company scope", () => { const input = vehicles.map((v, index) => ({ ...v, province: "上海市", city: ["松江区", "嘉定区", "奉贤区", "上海市"][index] })); const query = validatePlan({ ...plan, category: "all", groupBy: "city", filters: { city: ["上海市"] } }); const all = assetResult(input, query); assert.equal(all.metrics[0].value, 4); assert.equal(all.rows.length, 4); assert.equal(all.groups.length, 1); assert.equal(all.groups[0].count, 4); const scoped = assetResult(visibleVehicles(input, user, "甲公司"), query); assert.equal(scoped.metrics[0].value, 3); assert.deepEqual(scoped.rows.map(v => v.id).sort(), ["0", "1", "3"]); assert.ok(scoped.rows.every(v => v.city === "上海市")); }); test("inventory includes abnormal and pending; group totals reconcile with detail", () => { const result = assetResult(vehicles, validatePlan(plan)); assert.equal(result.rows.length, 3); assert.equal( result.groups.reduce((n, g) => n + g.count, 0), 3, ); assert.equal(result.metrics[1].value, 3); }); test("unsafe or unsupported query plans, impossible dates and long ranges are rejected", () => { assert.throws(() => validatePlan({ ...plan, filters: { sql: "SELECT *" } })); assert.throws(() => validatePlan({ ...plan, category: "delete" })); assert.throws(() => validatePlan({ ...plan, start: "2026-01-01" })); assert.throws(() => validatePlan({ ...plan, kind: "flow", start: "2026-02-30", end: "2026-03-05", }), ); assert.throws(() => validatePlan({ ...plan, kind: "flow", start: "2026-01-01", end: "2026-09-01", }), ); }); test("assistant rejects unauthenticated direct calls before contacting a model", async () => { const response = await router.request("/ask", { method: "POST", body: "{}" }); assert.equal(response.status, 401); }); test("presentation receives authorized aggregates while vehicle rows stay in the internal response", async (t) => { const oldKey = process.env.DEEPSEEK_API_KEY; process.env.DEEPSEEK_API_KEY = "test-only"; const oldEnabled = process.env.ASSET_AI_EXTERNAL_ENABLED; process.env.ASSET_AI_EXTERNAL_ENABLED = "1"; t.after(() => { if (oldEnabled === undefined) delete process.env.ASSET_AI_EXTERNAL_ENABLED; else process.env.ASSET_AI_EXTERNAL_ENABLED = oldEnabled; if (oldKey === undefined) delete process.env.DEEPSEEK_API_KEY; else process.env.DEEPSEEK_API_KEY = oldKey; }); t.mock.method(vehicleRepository, "getVehicles", async () => vehicles); const prompts: string[] = []; t.mock.method( globalThis, "fetch", async (_url: unknown, init: RequestInit) => { prompts.push(String(init.body)); return new Response( JSON.stringify({ choices: [ { message: { content: prompts.length === 1 ? JSON.stringify({ kind: "query", plan, }) : JSON.stringify({results:[{id:"main",blocks:[{id:"summary",kind:"summary"}],interactions:[]}]}), }, finish_reason: "stop", }, ], }), { status: 200 }, ); }, ); const app = new Hono<{ Variables: { user: AuthUser } }>(); app.use("*", async (c, next) => { c.set("user", user); await next(); }); app.route("/assistant", router); const response = await app.request("/assistant/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: "统计库存" }), }); const events = (await response.text()) .split("\n") .filter((s) => s.startsWith("data:")) .map((s) => JSON.parse(s.slice(5))); const result = events.find((e) => e.type === "result").result; assert.deepEqual( result.rows.map((r: { id: string }) => r.id), ["0", "1", "2"], ); assert.equal(prompts.length, 2); const presentationInput = JSON.parse(JSON.parse(prompts[1]).messages[1].content); assert.equal(presentationInput.stage, "presentation"); assert.equal(presentationInput.results[0].metrics[0].value, 3); assert.equal(presentationInput.results[0].rows, undefined); assert.ok( prompts.every( (p) => !p.includes("测试0") && !p.includes("测试2") && !p.includes("plateNumber"), ), ); assert.equal(events.filter((e) => e.type === "step").length, 4); }); test("inventory subsets do not inherit parent inventory metric cards", () => { for (const category of ["abnormal", "pending", "available", "awaiting"] as const) { const r = assetResult(vehicles, {kind:"assets", category, groupBy:"none", view:"chart", filters:{}}); assert.equal(r.metrics.length, 1, category); assert.doesNotMatch(r.summary, /其中异动.*待交车/); } }); test("personal and department scopes include all current stock subsets but keep operating vehicles restricted", () => { const stock = vehicles.slice(0, 3).map(v => ({ ...v, subjectOrg: "甲公司", managerId: "other", departmentName: "乙部门" })); const ownOperating = vehicles[3]; const foreignOperating = { ...ownOperating, id: "foreign-operating", managerId: "other", departmentName: "乙部门" }; const input = [...stock, ownOperating, foreignOperating]; for (const permissionLevel of ["personal", "department"] as const) { const visible = visibleVehicles(input, { ...user, permissionLevel }, "甲公司"); assert.deepEqual(visible.map(v => v.id), ["0", "1", "2", "3"]); for (const [category, expected] of [["inventory", ["0", "1", "2"]], ["abnormal", ["1"]], ["pending", ["2"]], ["operating", ["3"]]] as const) { const result = assetResult(visible, validatePlan({ ...plan, category })); assert.deepEqual(result.rows.map(v => v.id), expected); } } });