WARCH1 实现企微审批最小历史留存
这个提交包含在:
@@ -0,0 +1,164 @@
|
||||
const {
|
||||
executeArchiveCheck,
|
||||
parseArgs,
|
||||
sanitizeArchiveRow,
|
||||
} = require("../../../src/scripts/check-wecom-approval-archive");
|
||||
const { maskApprovalIdentifier } = require("../../../src/services/integrations/wecomApproval");
|
||||
|
||||
const SENSITIVE_SP_NO = "SPARCHIVEREADFULLIDENTIFIERSHOULDNOTLEAK";
|
||||
const SENSITIVE_TEMPLATE_ID = "TPLARCHIVEREADFULLIDENTIFIERSHOULDNOTLEAK";
|
||||
const SENSITIVE_USER_ID = "archive_read_userid_full_should_not_leak";
|
||||
const SENSITIVE_REASON = "原因全文不得进入离线报告";
|
||||
|
||||
function archiveRow(overrides = {}) {
|
||||
return {
|
||||
id: "archive-1",
|
||||
sp_no: SENSITIVE_SP_NO,
|
||||
template_id: SENSITIVE_TEMPLATE_ID,
|
||||
template_type: "leave",
|
||||
normalized_status: "approved",
|
||||
submitted_at: new Date("2026-05-19T01:00:00.000Z"),
|
||||
completed_at: new Date("2026-05-19T02:00:00.000Z"),
|
||||
status_observed_at: new Date("2026-05-19T02:00:00.000Z"),
|
||||
last_source: "compensation_pull",
|
||||
readability_status: "PASS_READABLE",
|
||||
single_tap_approve_risk: true,
|
||||
template_adjustment_required: false,
|
||||
participants: [{
|
||||
role: "applicant",
|
||||
node_index: 0,
|
||||
external_user_id: SENSITIVE_USER_ID,
|
||||
mapping_status: "mapped_or_external",
|
||||
is_current: false,
|
||||
}],
|
||||
actions: [{
|
||||
action_type: "approve",
|
||||
operator_external_user_id: "approver_full_userid_should_not_leak",
|
||||
action_time: new Date("2026-05-19T02:00:00.000Z"),
|
||||
node_index: 1,
|
||||
source: "compensation_pull",
|
||||
comment_summary: SENSITIVE_REASON,
|
||||
}],
|
||||
field_summaries: [{
|
||||
field_name: "请假原因",
|
||||
field_type: "Textarea",
|
||||
sensitive_level: "restricted",
|
||||
display_allowed: false,
|
||||
normalized_value: SENSITIVE_REASON,
|
||||
}],
|
||||
reconcile_findings: [],
|
||||
_count: {
|
||||
participants: 1,
|
||||
actions: 1,
|
||||
field_summaries: 1,
|
||||
attachment_summaries: 0,
|
||||
callback_events: 1,
|
||||
compensation_items: 1,
|
||||
reconcile_findings: 0,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("check-wecom-approval-archive", () => {
|
||||
it("parses offline archive filters", () => {
|
||||
const options = parseArgs([
|
||||
"--offline",
|
||||
"--format=json",
|
||||
"--template=leave",
|
||||
"--status=approved",
|
||||
"--limit=50",
|
||||
]);
|
||||
|
||||
expect(options).toMatchObject({
|
||||
offline: true,
|
||||
format: "json",
|
||||
templates: ["leave"],
|
||||
status: "approved",
|
||||
limit: 50,
|
||||
});
|
||||
expect(() => parseArgs(["--template=outing"])).toThrow(/Unsupported --template/);
|
||||
expect(() => parseArgs(["--limit=101"])).toThrow(/--limit/);
|
||||
});
|
||||
|
||||
it("sanitizes archive rows without leaking full identifiers or restricted values", () => {
|
||||
const sanitized = sanitizeArchiveRow(archiveRow());
|
||||
const serialized = JSON.stringify(sanitized);
|
||||
|
||||
expect(sanitized.spNoPrefix).toBe(maskApprovalIdentifier(SENSITIVE_SP_NO));
|
||||
expect(sanitized.templateIdPrefix).toBe(maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID));
|
||||
expect(sanitized.participants[0].userIdPrefix).toBe(maskApprovalIdentifier(SENSITIVE_USER_ID));
|
||||
expect(sanitized.fieldSummaries[0]).toMatchObject({
|
||||
fieldName: null,
|
||||
fieldType: "Textarea",
|
||||
sensitiveLevel: "restricted",
|
||||
displayAllowed: false,
|
||||
});
|
||||
expect(serialized).not.toContain(SENSITIVE_SP_NO);
|
||||
expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_USER_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_REASON);
|
||||
});
|
||||
|
||||
it("passes when OA DB archived history can be read offline", async () => {
|
||||
const report = await executeArchiveCheck(parseArgs(["--offline", "--format=json"]), {
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
prisma: {},
|
||||
readArchiveRowsImpl: vi.fn(async () => [archiveRow()]),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_ARCHIVE_HISTORY_READABLE");
|
||||
expect(report.scope.wecomApiCalls).toBe(false);
|
||||
expect(report.checks.archiveRead.status).toBe("OK");
|
||||
expect(report.checks.archiveRead.count).toBe(1);
|
||||
});
|
||||
|
||||
it("returns PASS_WITH_FINDINGS when open reconcile findings exist", async () => {
|
||||
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
prisma: {},
|
||||
readArchiveRowsImpl: vi.fn(async () => [archiveRow({
|
||||
reconcile_findings: [{
|
||||
finding_type: "callback_missing_for_pulled_detail",
|
||||
status: "open",
|
||||
severity: "info",
|
||||
source: "compensation_compare",
|
||||
detected_at: new Date("2026-05-19T03:00:00.000Z"),
|
||||
}],
|
||||
})]),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_WITH_FINDINGS");
|
||||
expect(report.checks.archives[0].openFindingCounts).toMatchObject({
|
||||
callback_missing_for_pulled_detail: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns PARTIAL_ARCHIVE_NEEDS_DATA when no archive rows exist", async () => {
|
||||
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
prisma: {},
|
||||
readArchiveRowsImpl: vi.fn(async () => []),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PARTIAL_ARCHIVE_NEEDS_DATA");
|
||||
expect(report.configGaps.map((item) => item.id)).toContain("archive_history_data");
|
||||
});
|
||||
|
||||
it("classifies DB read errors as external blockers", async () => {
|
||||
const report = await executeArchiveCheck(parseArgs(["--offline"]), {
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
prisma: {},
|
||||
readArchiveRowsImpl: vi.fn(async () => {
|
||||
throw new Error("database down with secret-like-value-should-be-redacted");
|
||||
}),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("SKIP");
|
||||
expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG");
|
||||
expect(report.mainBlockers[0]).toMatchObject({
|
||||
id: "archive_db_read",
|
||||
reason: "oa_archive_db_read_failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户