WARCH1 实现企微审批最小历史留存

这个提交包含在:
2026-05-20 00:22:27 +08:00
父节点 56a8e2bb0c
当前提交 5cd960cf90
共修改 28 个文件,包含 3081 行新增和 261 行删除
@@ -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",
});
});
});