import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import test from "node:test"; import { Hono } from "hono"; import type { AuthUser } from "../../auth/types.js"; import { registerNotifyRoutes, fetchActiveNotificationMap, fetchRecentInterventionCount } from "./notify.js"; import { registerSuggestionsRoutes } from "./suggestions.js"; import type { Database } from "./repository.js"; interface Call { via: "query" | "execute"; sql: string; params: unknown[]; } const normalize = (sql: string) => sql.replace(/\s+/g, " ").trim(); /** 依次弹出预设结果;未预设则返回空数组。 */ function createMockDb(resultSets: unknown[] = []): { db: Database; calls: Call[] } { const calls: Call[] = []; const queue = [...resultSets]; const mk = (via: "query" | "execute") => async (sql: string, params?: any[]) => { calls.push({ via, sql: normalize(sql), params: (params ?? []) as unknown[] }); return [queue.shift() ?? [], []] as never; }; return { db: { query: mk("query"), execute: mk("execute") }, calls }; } const USER: AuthUser = { userId: "u-1", userName: "张三", loginName: "zhangsan", depCode: "", depName: "", permissionLevel: "full", roles: ["BI-SCHEDULE-OPT"], }; function appWith(register: (app: any, deps: any) => void, db: Database, extra: Record = {}) { const app = new Hono<{ Variables: { user: AuthUser } }>(); app.use("*", async (c, next) => { c.set("user", USER); await next(); }); register(app, { db, ...extra }); return app; } /** 建议列表依赖的跨域数据源:测试中替成空实现,避免触达真实库/外部 API。 */ const STUB_DEPS = { loadVehicleInfoMap: async () => new Map(), loadOneOsMileageDates: async () => new Map(), }; const NOTIFICATION_ROW = { id: 5, suggestion_id: "sug-1", current_plate: "粤A1", candidate_plate: "粤B2", operator_id: "u-1", operator_name: "张三", status: "sent", created_at: "2026-07-01 10:00:00", updated_at: "2026-07-01 10:00:00", executed_at: null, notes: null, before_mileage: null, after_mileage: null, }; test("干预登记:先查是否已有生效记录,再 INSERT 并回读", async () => { const { db, calls } = createMockDb([[], { insertId: 5 }, [NOTIFICATION_ROW]]); const app = appWith(registerNotifyRoutes, db); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ suggestionId: "sug-1", currentPlate: "粤A1", candidatePlate: "粤B2" }), }); assert.equal(res.status, 200); const payload = await res.json() as { success: boolean; record: Record }; assert.equal(payload.success, true); assert.equal(payload.record.candidatePlate, "粤B2"); assert.equal(calls.length, 3); assert.equal( calls[0].sql, "SELECT id, candidate_plate FROM tab_scheduling_notifications WHERE suggestion_id = ? AND status != 'cancelled' LIMIT 1", ); assert.deepEqual(calls[0].params, ["sug-1"]); assert.equal(calls[0].via, "execute"); assert.equal( calls[1].sql, "INSERT INTO tab_scheduling_notifications (suggestion_id, current_plate, candidate_plate, operator_id, operator_name, status) VALUES (?, ?, ?, ?, ?, 'sent')", ); assert.deepEqual(calls[1].params, ["sug-1", "粤A1", "粤B2", "u-1", "张三"]); assert.equal(calls[2].sql, "SELECT * FROM tab_scheduling_notifications WHERE id = ?"); assert.deepEqual(calls[2].params, [5]); }); test("干预登记:已有生效记录返回 409 且不写库", async () => { const { db, calls } = createMockDb([[{ id: 9, candidate_plate: "粤C3" }]]); const app = appWith(registerNotifyRoutes, db); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ suggestionId: "sug-1", currentPlate: "粤A1", candidatePlate: "粤B2" }), }); assert.equal(res.status, 409); assert.equal(calls.length, 1, "被阻断后不应再 INSERT"); const payload = await res.json() as { message: string }; assert.equal(payload.message.includes("粤C3"), true); }); test("干预登记:缺少必要参数返回 400 且不查库", async () => { const { db, calls } = createMockDb(); const app = appWith(registerNotifyRoutes, db); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ suggestionId: "sug-1" }), }); assert.equal(res.status, 400); assert.equal(calls.length, 0); }); test("历史列表:状态过滤与条数上限,SQL 与参数顺序锁定", async () => { const { db, calls } = createMockDb([[]]); const app = appWith(registerNotifyRoutes, db); await app.request("/?status=sent&limit=9999"); assert.equal(calls.length, 1); assert.equal(calls[0].via, "query", "列表沿用 query(非 execute)"); assert.equal( calls[0].sql, "SELECT * FROM tab_scheduling_notifications WHERE status = ? ORDER BY created_at DESC LIMIT ?", ); assert.deepEqual(calls[0].params, ["sent", 500], "limit 上限 500"); calls.length = 0; await app.request("/"); assert.equal(calls[0].sql, "SELECT * FROM tab_scheduling_notifications ORDER BY created_at DESC LIMIT ?"); assert.deepEqual(calls[0].params, [200], "无过滤时默认 200"); }); test("状态更新:UPDATE 字段顺序与回读 SQL 保持", async () => { const { db, calls } = createMockDb([{}, [NOTIFICATION_ROW]]); const app = appWith(registerNotifyRoutes, db); const res = await app.request("/5", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "executed" }), }); assert.equal(res.status, 200); assert.equal(calls[0].via, "execute"); assert.equal(calls[0].sql.startsWith("UPDATE tab_scheduling_notifications SET "), true); assert.equal(calls[0].sql.endsWith("WHERE id = ?"), true); assert.equal(calls[0].params[calls[0].params.length - 1], 5); assert.equal(calls[1].sql, "SELECT * FROM tab_scheduling_notifications WHERE id = ?"); }); test("状态更新:非法 id / 缺 status / 非法 status 一律 400 且不查库", async () => { const { db, calls } = createMockDb(); const app = appWith(registerNotifyRoutes, db); const cases: Array<[string, unknown]> = [ ["/0", { status: "executed" }], ["/5", {}], ["/5", { status: "nope" }], ]; for (const [url, body] of cases) { const res = await app.request(url, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); assert.equal(res.status, 400, `${url} ${JSON.stringify(body)}`); } assert.equal(calls.length, 0); }); test("状态更新:记录不存在返回 404", async () => { const { db } = createMockDb([{}, []]); const app = appWith(registerNotifyRoutes, db); const res = await app.request("/5", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "cancelled" }), }); assert.equal(res.status, 404); }); test("活跃映射与近 7 天计数:SQL 与映射键保持", async () => { const { db, calls } = createMockDb([ [{ id: 1, suggestion_id: "sug-1", candidate_plate: "粤B2", status: "sent", created_at: "2026-07-01 10:00:00" }], [{ cnt: "4" }], ]); const map = await fetchActiveNotificationMap(db); assert.equal(map.get("sug-1::粤B2")?.id, 1); assert.equal( calls[0].sql, "SELECT id, suggestion_id, candidate_plate, status, created_at FROM tab_scheduling_notifications WHERE status != 'cancelled' ORDER BY created_at DESC", ); assert.equal(await fetchRecentInterventionCount(db), 4); assert.equal( calls[1].sql, "SELECT COUNT(*) AS cnt FROM tab_scheduling_notifications WHERE status != 'cancelled' AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)", ); }); test("建议列表:五条基础查询的 SQL 与顺序保持(不查外部 API 的路径)", async () => { const { db, calls } = createMockDb([[], [], [], [], []]); const app = appWith(registerSuggestionsRoutes, db, STUB_DEPS); // targetId 让响应可预测;OneOS 段在无车牌时不会触发 const res = await app.request("/?targetId=1"); assert.equal(res.status, 200); const sqls = calls.map(c => c.sql); assert.equal(sqls[0], "SELECT id, target_name, annual_mileage_per_vehicle FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id"); assert.equal(sqls[1].startsWith("SELECT target_id, plate_number, today_mileage, vehicle_total_mileage,"), true); assert.equal(sqls[1].includes("FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0"), true); assert.equal(sqls[2].includes("FROM vehicle_info vi LEFT JOIN vehicle_status vs"), true); assert.equal(sqls[2].includes("COALESCE(vs.operation_status, '') <> '5'"), true); assert.equal(sqls[3], "SELECT plate_number, province, city FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL"); assert.equal(sqls[4].includes("COALESCE(vs.operation_status, '') IN ('3','4')"), true, "库存车辆条件保持"); }); test("分层:调度域的路由文件不再直接书写 SQL", () => { const dir = path.dirname(fileURLToPath(import.meta.url)); for (const file of ["notify.ts", "suggestions.ts"]) { const source = readFileSync(path.join(dir, file), "utf8"); const offenders = source.match(/\b(SELECT|INSERT INTO|UPDATE |DELETE FROM)\b/g) ?? []; assert.deepEqual(offenders, [], `${file} 的 SQL 必须留在 repository.ts`); } });