WARCH3/WCHECK1/WEXT1 完成受控上线后续自动化
Issue: WARCH3, WCHECK1, WEXT1
这个提交包含在:
@@ -0,0 +1,191 @@
|
||||
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_***");
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户