const { applyPlanWithClient, assertApplyAllowed, buildDryRunResult, buildFixPlan, normalizeEmployeeNoList, parseArgs, } = require("../../../src/scripts/resolve-wecom-oa-user-consistency"); function auditReport(overrides = {}) { return { status: "PASS_WITH_FINDINGS", generatedAt: "2026-05-18T14:25:44.000Z", findings: { oaActiveMissingInWecom: [ { employeeNo: "TEST001", status: "active", roles: ["employee"] }, { employeeNo: "TEST002", status: "active", roles: ["employee"] }, ], missingRequiredFields: [ { employee: { employeeNo: "REAL001", status: "active", roles: ["employee"] }, wecom: { externalUserIdPrefix: "wecom_re***" }, fields: ["email", "phone", "department"], }, ], inactiveWithBinding: [ { employeeNo: "HRWDTEST-20260517214548", status: "archived", archived: true, roles: ["employee"] }, ], matchedActive: [], oaOnlySmokeExempted: [], wecomActiveMissingInOa: [], oaActiveMissingBinding: [], statusMismatch: [], matchConflict: [], }, ...overrides, }; } function employeeRow(overrides = {}) { return { id: overrides.id ?? "11111111-1111-4111-8111-111111111111", employee_no: overrides.employeeNo ?? "TEST001", status: overrides.status ?? "active", employment_status: overrides.employmentStatus ?? "active", archived: overrides.archived ?? false, token_version: overrides.tokenVersion ?? 1, roles: overrides.roles ?? ["employee"], has_wecom_binding: overrides.hasWecomBinding ?? false, }; } function buildClient(rows) { const calls = []; return { calls, async query(sql, params) { const text = String(sql); calls.push({ sql: text, params }); if (text.includes("FROM employees e")) { return { rows }; } return { rows: [], rowCount: 1 }; }, }; } function sqlCalls(client, pattern) { return client.calls.filter((call) => call.sql.includes(pattern)); } describe("resolve-wecom-oa-user-consistency", () => { afterEach(() => { delete process.env.WOUA_FIX_APPLY; }); it("builds a default plan that archives missing OA test employees and accepts missing WeCom fields", () => { const plan = buildFixPlan(auditReport()); const dryRun = buildDryRunResult(plan); const serialized = JSON.stringify(dryRun); expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST001", "TEST002"]); expect(plan.acceptedMissingRequiredFields).toEqual([ expect.objectContaining({ employeeNo: "REAL001", action: "accepted_no_write", fields: ["department", "email", "phone"], }), ]); expect(plan.inactiveWithBinding[0]).toEqual(expect.objectContaining({ employeeNo: "HRWDTEST-20260517214548", action: "accepted_no_write", })); expect(dryRun.totals.wecomWrites).toBe(0); expect(serialized).not.toContain("13800000001"); expect(serialized).not.toContain("employee@example.com"); expect(serialized).not.toContain("wecom_user_001_very_long"); }); it("supports allowlist narrowing to avoid touching unlisted employees", () => { const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST002"] }); expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST002"]); expect(plan.excludedByAllowlist.map((item) => item.employeeNo)).toEqual(["TEST001"]); }); it("exempts source-classified OA-only smoke accounts from archive plans", () => { const plan = buildFixPlan(auditReport({ findings: { ...auditReport().findings, oaOnlySmokeExempted: [ { employeeNo: "TEST001", status: "active", roles: ["admin", "employee"] }, ], }, })); expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST002"]); expect(plan.oaOnlySmokeExempted.map((item) => item.employeeNo)).toEqual(["TEST001"]); }); it("supports explicit OA-only smoke employee-no exemption for legacy reports", () => { const plan = buildFixPlan(auditReport(), { oaOnlySmokeEmployeeNos: ["TEST002"] }); expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST001"]); expect(plan.oaOnlySmokeExempted.map((item) => item.employeeNo)).toEqual(["TEST002"]); }); it("requires explicit environment approval for apply mode", () => { expect(() => assertApplyAllowed({ mode: "apply" })).toThrow(/WOUA_FIX_APPLY=1/); process.env.WOUA_FIX_APPLY = "1"; expect(() => assertApplyAllowed({ mode: "apply" })).not.toThrow(); }); it("parses mode and repeated employee allowlist arguments", () => { const options = parseArgs([ "--report=summary.json", "--apply", "--allow-employee-no=TEST001,TEST002", "--allow-employee-no", "TEST001", "--oa-only-smoke-employee-no=TEST003", "--repair-manager-effects", "--manager-fallback-employee-no=HYD000007", "--format=json", ]); expect(options.mode).toBe("apply"); expect(options.reportPath).toBe("summary.json"); expect(options.format).toBe("json"); expect(options.allowEmployeeNos).toEqual(["TEST001", "TEST002"]); expect(options.oaOnlySmokeEmployeeNos).toEqual(["TEST003"]); expect(options.repairManagerEffects).toBe(true); expect(options.managerFallbackEmployeeNo).toBe("HYD000007"); }); it("deduplicates normalized employee numbers", () => { expect(normalizeEmployeeNoList([" TEST001 ", "TEST001", "", "TEST002"])).toEqual(["TEST001", "TEST002"]); }); it("dry-runs inside a rolled-back transaction and does not update employees", async () => { const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST001"] }); const client = buildClient([employeeRow({ employeeNo: "TEST001" })]); const result = await applyPlanWithClient(client, plan, { mode: "dry_run", now: new Date("2026-05-18T00:00:00.000Z"), }); expect(result.status).toBe("DRY_RUN"); expect(result.totals.archivedPlanned).toBe(1); expect(sqlCalls(client, "UPDATE employees")).toHaveLength(0); expect(sqlCalls(client, "ROLLBACK")).toHaveLength(1); expect(sqlCalls(client, "COMMIT")).toHaveLength(0); }); it("archives active test employees in apply mode and increments token_version", async () => { const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST001"] }); const client = buildClient([employeeRow({ employeeNo: "TEST001" })]); const result = await applyPlanWithClient(client, plan, { mode: "apply", now: new Date("2026-05-18T00:00:00.000Z"), }); expect(result.status).toBe("APPLIED"); expect(result.totals.archivedApplied).toBe(1); expect(sqlCalls(client, "UPDATE employees")).toHaveLength(1); expect(sqlCalls(client, "token_version = token_version + 1")).toHaveLength(1); expect(sqlCalls(client, "INSERT INTO employee_lifecycle_events")).toHaveLength(1); expect(sqlCalls(client, "COMMIT")).toHaveLength(1); expect(sqlCalls(client, "ROLLBACK")).toHaveLength(0); }); it("keeps inactive WeCom bindings by default and clears them only with explicit flag", async () => { const plan = buildFixPlan(auditReport(), { clearInactiveBindings: true }); const dryClient = buildClient([ employeeRow({ employeeNo: "TEST001" }), employeeRow({ id: "22222222-2222-4222-8222-222222222222", employeeNo: "HRWDTEST-20260517214548", status: "archived", employmentStatus: "inactive", archived: true, hasWecomBinding: true, }), ]); const defaultPlan = buildFixPlan(auditReport()); const defaultClient = buildClient([ employeeRow({ employeeNo: "TEST001" }), ]); const defaultResult = await applyPlanWithClient(defaultClient, defaultPlan, { mode: "apply", now: new Date("2026-05-18T00:00:00.000Z"), }); const explicitResult = await applyPlanWithClient(dryClient, plan, { mode: "apply", clearInactiveBindings: true, now: new Date("2026-05-18T00:00:00.000Z"), }); expect(defaultResult.totals.inactiveBindingAccepted).toBe(1); expect(sqlCalls(defaultClient, "DELETE FROM wecom_identity_bindings")).toHaveLength(0); expect(explicitResult.totals.inactiveBindingApplied).toBe(1); expect(sqlCalls(dryClient, "DELETE FROM wecom_identity_bindings")).toHaveLength(1); }); });