const { assertAlertSendAllowed, buildAlertMessage, executeOpsAlert, parseArgs, shouldSendForSummary, } = require("../../../src/scripts/send-wecom-ops-alert"); const SENSITIVE_USER_ID = ["ops", "alert", "full", "userid", "should", "not", "leak"].join("_"); const SENSITIVE_TOKEN = ["ops", "alert", "token", "should", "not", "leak"].join("-"); function baseOptions(overrides = {}) { return { summaryFile: "", recipientEmployeeNos: ["wangshunli", "HYD000006"], send: false, force: false, format: "json", reportDir: "/tmp/wecom-ops-alert-test", help: false, ...overrides, }; } function baseSummary(overrides = {}) { return { issueId: "WARCH3", status: "PASS_WITH_FINDINGS", findingCount: 1, reportDir: "/app/artifacts/qa-flow/warch3-ops-test", ...overrides, }; } function recipientRows(overrides = {}) { return [ { employeeNo: "wangshunli", status: "active", archived: false, wecomExternalUserId: SENSITIVE_USER_ID, }, { employeeNo: "HYD000006", status: "active", archived: false, wecomExternalUserId: SENSITIVE_USER_ID, }, ].map((row) => ({ ...row, ...(overrides[row.employeeNo] || {}) })); } describe("send-wecom-ops-alert", () => { it("parses recipients and enforces double confirmation", () => { const options = parseArgs([ "--summary-file=/tmp/summary.json", "--recipient-employee-no=wangshunli", "--recipient-employee-no", "HYD000006", "--send", "--format=json", ]); expect(options.summaryFile).toBe("/tmp/summary.json"); expect(options.recipientEmployeeNos).toEqual(["wangshunli", "HYD000006"]); expect(options.send).toBe(true); expect(() => assertAlertSendAllowed(options, {})).toThrow(/WARCH3_OPS_ALERT_SEND=1/); expect(() => assertAlertSendAllowed(options, { WARCH3_OPS_ALERT_SEND: "1" })).not.toThrow(); }); it("builds a dehydrated message from summary only", () => { const message = buildAlertMessage(baseSummary({ reportDir: `/app/${SENSITIVE_TOKEN}/summary`, })); expect(message.title).toContain("PASS_WITH_FINDINGS"); expect(message.body).toContain("阶段:WARCH3"); expect(message.body).toContain("finding:1"); expect(message.body).not.toContain("手机号"); }); it("keeps PASS without findings as no-send unless forced", () => { const summary = baseSummary({ status: "PASS_ARCHIVE_OPS_READY", findingCount: 0 }); expect(shouldSendForSummary(summary, baseOptions())).toBe(false); expect(shouldSendForSummary(summary, baseOptions({ force: true }))).toBe(true); }); it("dry-runs alert without sending and redacts recipients", async () => { const sendMessageImpl = vi.fn(); const report = await executeOpsAlert(baseOptions(), { summary: baseSummary(), recipientRows: recipientRows(), sendMessageImpl, env: {}, config: { wecomRequestTimeoutMs: 1000 }, generatedAt: "2026-05-20T04:00:00.000Z", }); expect(report.status).toBe("PASS_ALERT_DRY_RUN_READY"); expect(report.delivery.status).toBe("DRY_RUN_READY"); expect(sendMessageImpl).not.toHaveBeenCalled(); const text = JSON.stringify(report); expect(text).not.toContain(SENSITIVE_USER_ID); expect(text).toContain("ops_***"); }); it("sends only after confirmation and does not leak full userid", async () => { const sendMessageImpl = vi.fn(async ({ recipientExternalUserIds, message }) => { expect(recipientExternalUserIds).toEqual([SENSITIVE_USER_ID]); expect(message.body).toContain("报告:/app/artifacts/qa-flow/warch3-ops-test"); return { ok: true, code: "wecom_app_sent", message: null, response: { errcode: 0, recipientCount: 1 }, }; }); const report = await executeOpsAlert(baseOptions({ send: true }), { summary: baseSummary(), recipientRows: recipientRows(), sendMessageImpl, env: { WARCH3_OPS_ALERT_SEND: "1" }, config: { wecomRequestTimeoutMs: 1000 }, }); expect(report.status).toBe("PASS_ALERT_SENT"); expect(report.delivery.status).toBe("OK"); expect(sendMessageImpl).toHaveBeenCalledTimes(1); expect(JSON.stringify(report)).not.toContain(SENSITIVE_USER_ID); }); it("sends to the valid admin recipient while recording alias gaps", async () => { const sendMessageImpl = vi.fn(async ({ recipientExternalUserIds }) => { expect(recipientExternalUserIds).toEqual([SENSITIVE_USER_ID]); return { ok: true, code: "wecom_app_sent", message: null, response: { errcode: 0, recipientCount: 1 }, }; }); const report = await executeOpsAlert(baseOptions({ send: true }), { summary: baseSummary(), recipientRows: recipientRows({ wangshunli: { employeeNo: "missing", wecomExternalUserId: "" }, }).filter((row) => row.employeeNo !== "missing"), sendMessageImpl, env: { WARCH3_OPS_ALERT_SEND: "1" }, config: { wecomRequestTimeoutMs: 1000 }, }); expect(report.status).toBe("PASS_ALERT_SENT_WITH_CONFIG_GAPS"); expect(report.recipientGaps.map((item) => item.id)).toContain("recipient_missing"); expect(report.delivery.status).toBe("OK"); expect(sendMessageImpl).toHaveBeenCalledTimes(1); }); it("classifies missing binding as config gap", async () => { const report = await executeOpsAlert(baseOptions({ send: true }), { summary: baseSummary(), recipientRows: recipientRows({ wangshunli: { wecomExternalUserId: "" }, HYD000006: { wecomExternalUserId: "" }, }), sendMessageImpl: vi.fn(), env: { WARCH3_OPS_ALERT_SEND: "1" }, config: {}, }); expect(report.status).toBe("PARTIAL_ALERT_NEEDS_CONFIG"); expect(report.recipientGaps.map((item) => item.id)).toContain("recipient_missing_wecom_binding"); }); it("classifies WeCom API error as external config blocker", async () => { const report = await executeOpsAlert(baseOptions({ send: true }), { summary: baseSummary(), recipientRows: recipientRows(), sendMessageImpl: vi.fn(async () => ({ ok: false, code: "wecom_app_api_error", message: "WeCom app message errcode 48002", response: { errcode: 48002, invaliduser: SENSITIVE_USER_ID }, })), env: { WARCH3_OPS_ALERT_SEND: "1" }, config: {}, }); expect(report.status).toBe("SKIP"); expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG"); expect(JSON.stringify(report)).not.toContain(SENSITIVE_USER_ID); expect(JSON.stringify(report.delivery.response)).toContain("ops_***"); }); });