/** @jest-environment node */ import { prisma } from '@/lib/prisma' import { getTeamFunctions, saveTeamFunction } from '@/lib/team-functions' jest.mock('@/lib/prisma', () => ({ prisma: { organizationUnit: { findMany: jest.fn() }, performanceSurveyEmployeeProfile: { findMany: jest.fn() }, teamFunctionConfig: { findMany: jest.fn(), findUnique: jest.fn(), findUniqueOrThrow: jest.fn(), create: jest.fn(), updateMany: jest.fn() }, teamFunctionConfigAudit: { create: jest.fn() }, $transaction: jest.fn(), } })) const config = { id: 'cfg', organizationUnitId: 'team', leadEmployeeProfileId: 'lead', description: '职责', branches: [{ label: '职责', items: ['设计'] }], revision: 2 } const input = { organizationUnitId: 'team', leadEmployeeProfileId: 'lead', description: '更新职责', branches: config.branches, revision: 2 } const profiles = [{ id: 'lead', displayName: '新花名', realName: '姓名', jobNumber: '001', employmentState: 'ACTIVE', departmentPath: '公司/旧部门', sourcePayload: null, departmentMemberships: [{ sourceDeptId: '2', departmentPath: '公司/旧部门' }] }] beforeEach(() => { jest.clearAllMocks() ;(prisma.organizationUnit.findMany as jest.Mock).mockResolvedValue([ { id: 'brand', name: '品牌中心', path: '公司/品牌中心', sourceDeptId: 1, parentId: null }, { id: 'team', name: '新部门名', path: '公司/品牌中心/新部门名', sourceDeptId: 2, parentId: 'brand' }, { id: 'outside', name: '公司部门', path: '公司/公司部门', sourceDeptId: 3, parentId: null }, ]) ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue(profiles) ;(prisma.teamFunctionConfig.findMany as jest.Mock).mockResolvedValue([config]) ;(prisma.teamFunctionConfig.findUnique as jest.Mock).mockResolvedValue(config) ;(prisma.teamFunctionConfig.updateMany as jest.Mock).mockResolvedValue({ count: 1 }) ;(prisma.teamFunctionConfig.findUniqueOrThrow as jest.Mock).mockResolvedValue({ ...config, ...input, revision: 3 }) ;(prisma.$transaction as jest.Mock).mockImplementation(callback => callback(prisma)) }) test('名称从当前花名册读取,过期部门快照不改变稳定 ID 归属', async () => { const result = await getTeamFunctions(false) expect(result.teams).toHaveLength(1) expect(result.teams[0]).toMatchObject({ name: '新部门名', lead: { employeeProfileId: 'lead', name: '新花名' }, candidates: [] }) expect(prisma.teamFunctionConfig.create).not.toHaveBeenCalled() }) test('新增 HR 部门自动出现,未配置职责保持为空', async () => { ;(prisma.teamFunctionConfig.findMany as jest.Mock).mockResolvedValue([]) expect((await getTeamFunctions(true)).teams[0]).toMatchObject({ revision: 0, description: '', branches: [], leadStatus: 'unset', candidates: [{ employeeProfileId: 'lead' }] }) }) test.each(['RESIGNED', 'moved'])('负责人离职或调出后显示失效,不能继续保存该负责人 %s', async state => { ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([{ ...profiles[0], employmentState: state === 'RESIGNED' ? state : 'ACTIVE', departmentMemberships: [{ sourceDeptId: state === 'moved' ? '3' : '2' }] }]) expect((await getTeamFunctions(true)).teams[0].leadStatus).toBe('invalid') await expect(saveTeamFunction(input, 'actor')).rejects.toMatchObject({ status: 400 }) expect(prisma.teamFunctionConfig.updateMany).not.toHaveBeenCalled() }) test('同名品牌中心节点拒绝猜测范围', async () => { ;(prisma.organizationUnit.findMany as jest.Mock).mockResolvedValue([{ id: 'a', name: '品牌中心' }, { id: 'b', name: '品牌中心' }]) await expect(getTeamFunctions(false)).rejects.toMatchObject({ status: 409 }) }) test('过期修订不覆盖其他人的修改', async () => { await expect(saveTeamFunction({ ...input, revision: 1 }, 'actor')).rejects.toMatchObject({ status: 409 }) expect(prisma.teamFunctionConfig.updateMany).not.toHaveBeenCalled() expect(prisma.teamFunctionConfigAudit.create).not.toHaveBeenCalled() }) test('保存及前后快照同一事务,部门改名不会新建配置', async () => { expect(await saveTeamFunction(input, 'actor')).toEqual({ revision: 3 }) expect(prisma.teamFunctionConfig.updateMany).toHaveBeenCalledWith(expect.objectContaining({ where: { id: 'cfg', revision: 2 } })) expect(prisma.teamFunctionConfigAudit.create).toHaveBeenCalledWith({ data: expect.objectContaining({ actorUserId: 'actor', before: expect.objectContaining({ revision: 2 }), after: expect.objectContaining({ revision: 3 }) }) }) expect(prisma.$transaction).toHaveBeenCalledWith(expect.any(Function), { isolationLevel: 'Serializable' }) }) test('非当前品牌中心部门不得写入', async () => { await expect(saveTeamFunction({ ...input, organizationUnitId: 'outside' }, 'actor')).rejects.toMatchObject({ status: 409 }) expect(prisma.teamFunctionConfig.updateMany).not.toHaveBeenCalled() })