const { assertAlertSendAllowed, buildAlertMessage, buildAlertState, evaluateAlertDecision, executeOpsAlert, parseArgs, readSummary, 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("reads stdin summary without preserving an injected absolute report path", () => { const summary = readSummary("-", JSON.stringify({ issueId: "AUTO_BACKUP", status: "FAIL_AUTOMATIC_BACKUP", findingCount: 1, reportDir: `/private/${SENSITIVE_TOKEN}/state`, })); expect(summary.issueId).toBe("AUTO_BACKUP"); expect(summary.reportDir).toBe("stdin-summary"); expect(JSON.stringify(summary)).not.toContain(SENSITIVE_TOKEN); expect(JSON.stringify(summary)).not.toContain("/private/"); }); it("keeps AUTO_BACKUP issue id and uses backup-specific failure guidance", async () => { const summary = baseSummary({ issueId: "AUTO_BACKUP", status: "FAIL_AUTOMATIC_BACKUP", reportDir: "host-backup-state", }); const message = buildAlertMessage(summary); const report = await executeOpsAlert(baseOptions(), { summary, recipientRows: recipientRows(), sendMessageImpl: vi.fn(), env: {}, config: {}, }); expect(message.title).toContain("自动备份"); expect(message.body).toContain("异机传输链路"); expect(report.issueId).toBe("AUTO_BACKUP"); expect(report.summary.issueId).toBe("AUTO_BACKUP"); }); it("uses manual ARM shadow-restore guidance for quarterly reminder", () => { const summary = baseSummary({ issueId: "AUTO_BACKUP", findings: [{ detail: "quarterly_shadow_restore_due_2026-Q3" }], reportDir: "host-backup-state", }); const message = buildAlertMessage(summary); const firstDecision = evaluateAlertDecision(summary, baseOptions(), null); const repeatedDecision = evaluateAlertDecision(summary, baseOptions(), buildAlertState(summary)); expect(message.body).toContain("ARM 隔离目标执行人工影子恢复"); expect(firstDecision.shouldSend).toBe(true); expect(firstDecision.reason).toBe("quarterly_restore_reminder"); expect(repeatedDecision.shouldSend).toBe(false); }); it("sends the first automatic-backup failure and suppresses an identical repeat", () => { const summary = baseSummary({ issueId: "AUTO_BACKUP", status: "FAIL_AUTOMATIC_BACKUP", findings: [{ source: "automatic_backup", status: "FAIL_AUTOMATIC_BACKUP", detail: "phase_offhost_transfer_failed" }], }); const firstDecision = evaluateAlertDecision(summary, baseOptions(), null); const repeatedDecision = evaluateAlertDecision(summary, baseOptions(), buildAlertState(summary)); expect(firstDecision.shouldSend).toBe(true); expect(firstDecision.reason).toBe("blocked_or_failed"); expect(repeatedDecision.shouldSend).toBe(false); expect(repeatedDecision.reason).toBe("repeated_automatic_backup_failure"); }); 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_***"); }); });