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"); }); });