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", "--template=overtime"]).templates).toEqual(["outing", "overtime"]); expect(() => parseArgs(["--template=unknown"])).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", }); }); });