const { assertAlertSendAllowed, buildAlertMessage, buildAlertState, evaluateAlertDecision, 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: ["HYD000006"], send: false, force: false, stateFile: "/tmp/wecom-ops-alert-state-test.json", 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: "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", "HYD000006", "--send", "--state-file=/tmp/warch3-state.json", "--format=json", ]); expect(options.summaryFile).toBe("/tmp/summary.json"); expect(options.recipientEmployeeNos).toEqual(["HYD000006"]); expect(options.stateFile).toBe("/tmp/warch3-state.json"); 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("records the first PASS_WITH_FINDINGS run as baseline without sending", () => { const summary = baseSummary({ findings: [{ source: "approvalArchive", status: "PASS_WITH_FINDINGS", severity: "info", evidenceDigest: "known" }], }); const decision = evaluateAlertDecision(summary, baseOptions(), null); expect(decision.shouldSend).toBe(false); expect(decision.reason).toBe("baseline_recorded"); }); it("sends when a new finding appears after baseline", () => { const previousSummary = baseSummary({ findings: [{ source: "approvalArchive", status: "PASS_WITH_FINDINGS", severity: "info", evidenceDigest: "known" }], }); const currentSummary = baseSummary({ findings: [ { source: "approvalArchive", status: "PASS_WITH_FINDINGS", severity: "info", evidenceDigest: "known" }, { source: "wecomRuntimeReady", status: "PASS_WITH_FINDINGS", severity: "warn", evidenceDigest: "new" }, ], }); const decision = evaluateAlertDecision(currentSummary, baseOptions(), buildAlertState(previousSummary)); expect(decision.shouldSend).toBe(true); expect(decision.reason).toBe("new_findings"); expect(decision.newFindingKeys).toHaveLength(1); }); it("skips repeated known findings after baseline", () => { const summary = baseSummary({ findings: [{ source: "approvalArchive", status: "PASS_WITH_FINDINGS", severity: "info", evidenceDigest: "known" }], }); const decision = evaluateAlertDecision(summary, baseOptions(), buildAlertState(summary)); expect(decision.shouldSend).toBe(false); expect(decision.reason).toBe("no_new_findings_or_status_worsening"); }); it("sends when status worsens or blocks", () => { const previous = buildAlertState(baseSummary({ status: "PASS_ARCHIVE_OPS_READY", findingCount: 0, findings: [] })); const worsened = evaluateAlertDecision(baseSummary({ status: "PASS_WITH_FINDINGS", findingCount: 0, findings: [] }), baseOptions(), previous); const blocked = evaluateAlertDecision(baseSummary({ status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG", findingCount: 0, findings: [] }), baseOptions(), null); expect(worsened.shouldSend).toBe(true); expect(worsened.reason).toBe("status_worsened"); expect(blocked.shouldSend).toBe(true); expect(blocked.reason).toBe("blocked_or_failed"); }); 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_SKIPPED_NO_FINDINGS"); expect(report.delivery.status).toBe("SKIPPED_NO_FINDINGS"); expect(report.alertDecision.reason).toBe("baseline_recorded"); 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 }, priorState: buildAlertState(baseSummary({ status: "PASS_ARCHIVE_OPS_READY", findingCount: 0, findings: [] })), writeAlertState: vi.fn(() => "/tmp/warch3-state.json"), }); 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("classifies missing binding as config gap", async () => { const report = await executeOpsAlert(baseOptions({ send: true }), { summary: baseSummary(), recipientRows: recipientRows({ HYD000006: { wecomExternalUserId: "" }, }), sendMessageImpl: vi.fn(), env: { WARCH3_OPS_ALERT_SEND: "1" }, config: {}, priorState: buildAlertState(baseSummary({ status: "PASS_ARCHIVE_OPS_READY", findingCount: 0, findings: [] })), }); 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: {}, priorState: buildAlertState(baseSummary({ status: "PASS_ARCHIVE_OPS_READY", findingCount: 0, findings: [] })), writeAlertState: vi.fn(() => "/tmp/warch3-state.json"), }); 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_***"); }); });