feat(flow): 落地 FL2.0 原型——部门选人抽屉 / 授权管理页 / 退回重提入口(基线 origin/main)
build / build (push) Successful in 1m31s
build / build (push) Successful in 1m31s
在 c93313c 之上增量落地原型与前端规范,保留既有流程图入口/失败提示/指派预览/线级停发:
- 审批/抄送抽屉按原型重构(422→1026 / 239→742 行):科室树+成员多选、SYSTEM/CUSTOM 分支、
CUSTOM 分类导航自动定位、跨节点导航重置、保存后重开回显
- 新增授权管理页 /admin/usage-grants(UsageGrants.vue,417 行):校域清单、按流程线筛选、
授予/撤销后回读,失败不宣称成功
- 路由与 API:新增 usage-grant(schoolList/summary/grant/revoke)、department(groups/options/resolve/members)、
instance/resubmit;TaskHandle 增加「退回待修改」入口(仅 status=50 显示)
- 错误处理沿用 flowError 统一出口,不透出后端原文
- 组件/契约测试:flow-error、flow2-contract、department-roundtrip、task-result
不含构建产物(dist-verify)与依赖目录
This commit is contained in:
+81
-26
@@ -1,55 +1,110 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { describeFlowError, flowFailTip, FLOW_GENERIC_FALLBACK, FLOW_NETWORK_FALLBACK } from "../src/utils/flowError";
|
||||
import {
|
||||
classifyFlowError,
|
||||
describeFlowError,
|
||||
flowFailTip,
|
||||
flowReloadFailTip,
|
||||
flowSubmitFailTip,
|
||||
isDefiniteBusinessRejection,
|
||||
FLOW_GENERIC_FALLBACK,
|
||||
FLOW_NETWORK_FALLBACK,
|
||||
FLOW_UNKNOWN_FALLBACK,
|
||||
} from "../src/utils/flowError";
|
||||
|
||||
/** 审批失败文案单测:口径=具体业务话原样透出,通用/异常形态换兜底话术且不外泄内部信息 */
|
||||
/**
|
||||
* 错误呈现与结果可信度单测(P0-1 附带项 + Codex 终审 F04/F05,2026-10-29)。
|
||||
*
|
||||
* F05 的口径不是"命中黑名单才挡",而是"拿不到可信凭据就不放行":
|
||||
* 只有带 bizCode 的业务信封原文才可能进 DOM;HTTP 状态文本、异常/SQL/Token 形态、
|
||||
* 以及无法判定来源的裸文本一律换成通用安全提示。
|
||||
*
|
||||
* F04 的口径是"失败 ≠ 未生效":服务端明确拒绝与"请求已发出但结果未知"必须分开说。
|
||||
*/
|
||||
|
||||
test("后端给出通用 code=6 占位文案:换成不误导的兜底话术,不猜原因", () => {
|
||||
assert.equal(describeFlowError("内部服务错误"), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError("内部服务错误!"), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError("业务错误"), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError(""), FLOW_GENERIC_FALLBACK);
|
||||
assert.ok(!FLOW_GENERIC_FALLBACK.includes("成功"));
|
||||
});
|
||||
function bizError(message: string, bizCode = 2): Error {
|
||||
return Object.assign(new Error(message), { bizCode });
|
||||
}
|
||||
|
||||
test("契约内的具体业务 msg 原样透出,不改写不误导", () => {
|
||||
assert.equal(describeFlowError("流程线不存在:8d07f049"), "流程线不存在:8d07f049");
|
||||
assert.equal(describeFlowError("该任务已被他人办理"), "该任务已被他人办理");
|
||||
function httpError(message: string, httpStatus = 500): Error {
|
||||
return Object.assign(new Error(message), { httpStatus });
|
||||
}
|
||||
|
||||
test("业务信封明确拒绝:具体业务话原样透出,不改写不误导", () => {
|
||||
assert.equal(describeFlowError(bizError("流程线不存在:8d07f049")), "流程线不存在:8d07f049");
|
||||
assert.equal(describeFlowError(bizError("该任务已被他人办理")), "该任务已被他人办理");
|
||||
assert.equal(
|
||||
describeFlowError("该待办缺少流程推进记录,需管理员回填后办理"),
|
||||
describeFlowError(bizError("该待办缺少流程推进记录,需管理员回填后办理")),
|
||||
"该待办缺少流程推进记录,需管理员回填后办理",
|
||||
);
|
||||
assert.equal(isDefiniteBusinessRejection(bizError("流程线不存在:g1")), true);
|
||||
});
|
||||
|
||||
test("异常/SQL/令牌形态不回显:堆栈与凭据不进页面", () => {
|
||||
test("F05:HTTP 500 的响应原文不回显(旧实现把任意非黑名单文本都当业务话)", () => {
|
||||
assert.equal(describeFlowError(Object.assign(httpError('private backend text'), { bizCode: 2 })), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(isDefiniteBusinessRejection(Object.assign(httpError('private backend text'), { bizCode: 2 })), false);
|
||||
assert.equal(describeFlowError(bizError('private backend text', 999)), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError(bizError('private backend text', 6)), FLOW_GENERIC_FALLBACK);
|
||||
for (const raw of ["Internal Server Error", "Bad Gateway", ""]) {
|
||||
const out = describeFlowError(httpError(raw));
|
||||
assert.equal(out, FLOW_GENERIC_FALLBACK, raw);
|
||||
assert.equal(isDefiniteBusinessRejection(httpError(raw)), false);
|
||||
}
|
||||
// 网关超时属网络口径:说"刷新重试"而不是把 "Gateway Timeout" 原文贴进页面
|
||||
assert.equal(describeFlowError(httpError("Gateway Timeout", 504)), FLOW_NETWORK_FALLBACK);
|
||||
});
|
||||
|
||||
test("F05:无法判定来源的裸文本走通用提示,不做黑名单后原样放行", () => {
|
||||
// 没有 bizCode 的字符串/Error 不可能是"可信业务文案"
|
||||
assert.equal(describeFlowError("Something went wrong at node 7"), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError(new Error("流程线不存在:g1")), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError(undefined), FLOW_GENERIC_FALLBACK);
|
||||
assert.equal(describeFlowError(null), FLOW_GENERIC_FALLBACK);
|
||||
});
|
||||
|
||||
test("F05:即使来自业务信封,异常/SQL/令牌形态也不回显", () => {
|
||||
const leaks = [
|
||||
"java.lang.IllegalStateException: D09 task has no proven activation; audited backfill required",
|
||||
"at com.yunhai.flowcenter.service.FlowTaskService.handle(FlowTaskService.java:100)",
|
||||
"Caused by: SQLException error code [1064]",
|
||||
"refresh token=Bearer abcdef123456",
|
||||
"<html><body>500 Internal Server Error</body></html>",
|
||||
"内部服务错误",
|
||||
];
|
||||
for (const raw of leaks) {
|
||||
const out = describeFlowError(raw);
|
||||
const out = describeFlowError(bizError(raw, 500));
|
||||
assert.equal(out, FLOW_GENERIC_FALLBACK, raw);
|
||||
assert.ok(!/java\.|Exception|SQL|Bearer|<html>/i.test(out));
|
||||
assert.ok(!/java\.|Exception|SQL|Bearer|<html>/i.test(out), raw);
|
||||
}
|
||||
});
|
||||
|
||||
test("网络/登录态失败:按网络口径提示,不与业务失败混说", () => {
|
||||
assert.equal(describeFlowError("网络错误"), FLOW_NETWORK_FALLBACK);
|
||||
assert.equal(describeFlowError("Failed to fetch"), FLOW_NETWORK_FALLBACK);
|
||||
assert.equal(describeFlowError("timeout of 20000ms exceeded"), FLOW_NETWORK_FALLBACK);
|
||||
assert.equal(describeFlowError(new Error("Failed to fetch")), FLOW_NETWORK_FALLBACK);
|
||||
assert.equal(classifyFlowError(httpError("timeout of 20000ms exceeded", 0)).kind, "network");
|
||||
});
|
||||
|
||||
test("Error 对象可直接传入(取 message)", () => {
|
||||
assert.equal(describeFlowError(new Error("流程线不存在:g1")), "流程线不存在:g1");
|
||||
assert.equal(describeFlowError(new Error("内部服务错误")), FLOW_GENERIC_FALLBACK);
|
||||
test("F04:明确拒绝说拒绝;超时/丢响应只说结果待确认,不断言未生效", () => {
|
||||
const rejected = flowSubmitFailTip("同意", bizError("仅任务办理人本人可办理"));
|
||||
assert.ok(rejected.startsWith("同意被拒绝:"), rejected);
|
||||
assert.ok(rejected.includes("仅任务办理人本人可办理"), rejected);
|
||||
|
||||
const lost = flowSubmitFailTip("同意", new TypeError("network failure"));
|
||||
assert.ok(lost.startsWith("同意结果待确认:"), lost);
|
||||
assert.ok(lost.includes(FLOW_UNKNOWN_FALLBACK) || lost.includes(FLOW_NETWORK_FALLBACK), lost);
|
||||
assert.ok(!lost.includes("未生效"), lost);
|
||||
assert.ok(!lost.includes("仍是你的待办"), lost);
|
||||
});
|
||||
|
||||
test("页面内联提示带动作名,明确「未生效」而不是成功", () => {
|
||||
const tip = flowFailTip("同意", new Error("内部服务错误"));
|
||||
assert.ok(tip.startsWith("同意未完成:"));
|
||||
assert.ok(tip.includes(FLOW_GENERIC_FALLBACK));
|
||||
assert.ok(!tip.includes("已同意"));
|
||||
test("F04:办理成功但后续刷新失败 → 说明已成功、只是没刷新,不改口成失败", () => {
|
||||
const tip = flowReloadFailTip("同意");
|
||||
assert.ok(tip.includes("已提交成功"), tip);
|
||||
assert.ok(tip.includes("刷新"), tip);
|
||||
});
|
||||
|
||||
test("只读动作失败的内联提示带动作名,不谎报成功", () => {
|
||||
const tip = flowFailTip("催办", bizError("已达催办次数上限(3 次)"));
|
||||
assert.ok(tip.startsWith("催办未完成:"), tip);
|
||||
assert.ok(tip.includes("已达催办次数上限(3 次)"), tip);
|
||||
assert.ok(!tip.includes("成功"), tip);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user