Files
ln-bi/src/server/routes/scheduling/routes.test.ts
T
dsh-agent 5196db5df7 refactor(stage12): 调度域拆出 repository,跨域数据源改为显式注入
改动
- 新增 scheduling/repository.ts:通知/干预与建议列表的全部 SQL(13 条)。
- notify.ts / suggestions.ts 改为 register*(app, deps) 可注入,保留默认导出与 create*Router 工厂。
- suggestions.ts 的两处跨域依赖(里程车辆信息、OneOS 里程)改为依赖注入:
  既让本域可独立测试,也让"建议依赖里程数据"在类型上可见,而不是藏在 import 里。
- 跨域引用修正:mapRegion 原本从 '../vehicles/routes.js' 引入(为一个纯函数把整个
  车辆路由拖进依赖图),改为直接从 '../vehicles/model.js' 引入。

契约测试(新增 10 个用例)
- 逐条锁定 SQL 与参数顺序:干预登记的三步(查重 → INSERT → 回读)、409 阻断、
  批量循环、历史列表的状态过滤与 limit 上限(500/默认 200)、状态更新的 UPDATE 形状、
  400/404 分支、活跃映射与近 7 天计数、建议列表五条基础查询的内容与顺序。
- 架构守护升级:不再只看 routes.ts,而是要求该域**除 repository.ts 外的任何非测试文件
  都不得含 SQL**;匹配用"语句形状"正则(如 update <table> set)而非裸关键字,
  避免把 UpdateNotification 或日志 "update error" 误判。已验证 6/6 个 repository 被识别、
  其余文件零误判。

等价性验证
- 把 notify.ts / suggestions.ts 改造前的实现从 git 取出,与改造后跑同一批请求
  (9 个场景,含建议列表整条链路 8 次查询),对比落库 SQL、参数、HTTP 状态与响应体:
  完全一致。
- 期间修正了两处**验证工具自身**的缺陷(旧文件误引用新 notify;跨域默认实现的调用
  被记到另一侧),修正后结论可信。

lint / test(191) / build 全绿,可达性 0 未引用文件。
2026-09-11 10:44:52 +08:00

240 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown> = {}) {
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<string, unknown> };
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`);
}
});