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", "500 Internal Server Error", "内部服务错误", ]; for (const raw of leaks) { const out = describeFlowError(bizError(raw, 500)); assert.equal(out, FLOW_GENERIC_FALLBACK, raw); assert.ok(!/java\.|Exception|SQL|Bearer|/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); });