文件
hyb-oa/server/tests/unit/scripts/send-wecom-ops-alert.test.js

284 行
11 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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_***");
});
});