WRULE1 实现企微审批链校验器 POC

这个提交包含在:
2026-05-21 00:35:10 +08:00
父节点 1440cfe9a2
当前提交 c119183da6
共修改 14 个文件,包含 1225 行新增和 29 行删除
@@ -0,0 +1,188 @@
const {
executeApprovalRuleValidator,
parseArgs,
} = require("../../../src/scripts/check-wecom-approval-rule-validator");
function employee(id, overrides = {}) {
const { roles = ["employee"], ...rest } = overrides;
return {
id,
employee_no: rest.employee_no || `EMP-${id}`,
status: rest.status || "active",
roles: roles.map((role_code) => ({ role_code })),
wecom_identity_binding: rest.bound === false ? null : { external_user_id: `wecom-${id}` },
direct_manager: rest.direct_manager || null,
...rest,
};
}
function archiveRow() {
const manager = employee("manager", { employee_no: "SMOKE101", roles: ["manager"] });
return {
id: "archive-1",
sp_no: "SP_SCRIPT_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK",
template_id: "TPL_SCRIPT_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK",
template_type: "leave",
normalized_status: "approved",
applicant: employee("applicant", {
employee_no: "SMOKE002",
direct_manager: manager,
}),
participants: [{
role: "approver",
node_index: 1,
employee: manager,
}],
actions: [],
};
}
describe("check-wecom-approval-rule-validator", () => {
it("parses templates, manual gates and rule options", () => {
const options = parseArgs([
"--format=json",
"--template=leave",
"--manual-gate=all:A",
"--approval-role=hr",
"--fallback-employee-no=HYD000006",
"--limit=20",
]);
expect(options).toMatchObject({
format: "json",
templates: ["leave"],
manualGates: {
leave: "A",
missed_punch: "A",
outing: "A",
overtime: "A",
},
approvalRoles: ["hr"],
fallbackEmployeeNo: "HYD000006",
limit: 20,
});
expect(() => parseArgs(["--template=expense"])).toThrow(/Unsupported --template/);
expect(() => parseArgs(["--manual-gate=leave:E"])).toThrow(/Unsupported manual gate value/);
});
it("returns PASS_RULE_MATCH when archived WeCom approver matches OA rule", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--manual-gate=leave:A",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("PASS_RULE_MATCH");
expect(report.scope.wecomApiCalls).toBe(false);
expect(report.checks.ruleValidation.count).toBe(1);
});
it("pauses when WAPPR1 manual gates are missing", async () => {
const report = await executeApprovalRuleValidator(parseArgs(["--template=leave"]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("PAUSED_MANUAL_CONFIRMATION");
expect(report.checks.manualGateGaps).toHaveLength(1);
});
it("blocks when a selected template has no archived approval rows", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--template=outing",
"--manual-gate=leave:A",
"--manual-gate=outing:A",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.checks.templateDataGaps).toEqual([{
templateType: "outing",
status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
reason: "no_wecom_approval_archive_rows_for_template_scope",
}]);
});
it("blocks applicant-chosen manual gates even when that template has no archive rows", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--template=outing",
"--manual-gate=leave:A",
"--manual-gate=outing:B",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("FAIL_APPLICANT_CAN_CHOOSE");
expect(report.checks.manualGateFindings).toContainEqual({
templateType: "outing",
status: "FAIL_APPLICANT_CAN_CHOOSE",
reason: "manual_wappr1_approver_can_be_changed",
});
});
it("classifies manual D gates as external config blockers", async () => {
const report = await executeApprovalRuleValidator(parseArgs([
"--template=leave",
"--manual-gate=leave:D",
]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => ({
archives: [archiveRow()],
fallbackEmployee: null,
approvalRoleEmployees: [],
})),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.checks.manualGateFindings[0]).toMatchObject({
templateType: "leave",
status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG",
});
});
it("classifies DB read failures as external blockers without leaking long secrets", async () => {
const report = await executeApprovalRuleValidator(parseArgs(["--template=leave", "--manual-gate=leave:A"]), {
generatedAt: "2026-05-20T00:00:00.000Z",
prisma: {},
readApprovalRuleInputsImpl: vi.fn(async () => {
throw new Error("database down secret-like-token-123456789012345678901234567890");
}),
});
expect(report.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG");
expect(report.mainBlockers[0]).toMatchObject({
id: "approval_rule_db_read",
reason: "oa_archive_or_org_rule_read_failed",
errorCode: "unknown",
});
expect(JSON.stringify(report)).not.toContain("secret-like-token-123456789012345678901234567890");
});
});