Files
flow-center-front/tests/flow-error.test.ts
T
chenxing bed57846f4
build / build (push) Successful in 1m31s
feat(flow): 落地 FL2.0 原型——部门选人抽屉 / 授权管理页 / 退回重提入口(基线 origin/main)
在 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)与依赖目录
2026-10-30 10:15:47 +08:00

111 lines
5.5 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 { test } from "node:test";
import assert from "node:assert/strict";
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 的口径是"失败 ≠ 未生效":服务端明确拒绝与"请求已发出但结果未知"必须分开说。
*/
function bizError(message: string, bizCode = 2): Error {
return Object.assign(new Error(message), { bizCode });
}
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(bizError("该待办缺少流程推进记录,需管理员回填后办理")),
"该待办缺少流程推进记录,需管理员回填后办理",
);
assert.equal(isDefiniteBusinessRejection(bizError("流程线不存在:g1")), true);
});
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(bizError(raw, 500));
assert.equal(out, FLOW_GENERIC_FALLBACK, raw);
assert.ok(!/java\.|Exception|SQL|Bearer|<html>/i.test(out), raw);
}
});
test("网络/登录态失败:按网络口径提示,不与业务失败混说", () => {
assert.equal(describeFlowError("网络错误"), 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("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("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);
});