gongxiangp2/lib/__tests__/performance-survey-position-admin.test.ts
2026-07-27 18:00:10 +08:00

348 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @jest-environment node */
import { prisma } from '@/lib/prisma'
import {
assertPerformanceSurveyPositionPlanPreviewMatches,
createPerformanceSurveyManualPositionAssignments,
listPerformanceSurveyPositionGroups,
removePerformanceSurveyTemplate,
savePerformanceSurveyPositionGroupDraft,
} from '@/lib/performance-survey-position-config'
import { DEFAULT_PERFORMANCE_SURVEY_TEMPLATES } from '@/lib/performance-survey-templates'
jest.mock('@/lib/prisma', () => ({
prisma: {
performanceSurveyPositionGroup: {
findUnique: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
performanceSurveyPositionRule: {
deleteMany: jest.fn(),
createMany: jest.fn(),
update: jest.fn(),
},
performanceSurveyTemplateVersion: {
findMany: jest.fn(),
},
performanceSurveyTemplate: {
findUnique: jest.fn(),
},
performanceSurveyPositionPlanItem: {
count: jest.fn(),
},
performanceSurvey: {
count: jest.fn(),
},
performanceSurveyEmployeeProfile: {
findMany: jest.fn(),
},
$transaction: jest.fn(),
},
}))
describe('performance survey position admin', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('保存岗位组草稿只更新方案草稿,不改线上岗位组和匹配规则', async () => {
const liveGroup = {
id: 'group-1',
code: 'brand_designer',
name: '线上岗位名',
sortOrder: 10,
isActive: true,
isSystem: true,
archivedAt: null,
}
const existingDraft = {
id: 'draft-1',
positionGroupId: liveGroup.id,
version: 2,
status: 'draft',
effectiveMonth: '2026-08',
matchRules: [{ positionTitle: '线上规则' }],
groupConfig: {
name: liveGroup.name,
sortOrder: liveGroup.sortOrder,
isActive: liveGroup.isActive,
},
items: [],
}
const updatedDraft = {
...existingDraft,
matchRules: [{ positionTitle: '草稿规则' }],
groupConfig: {
name: '草稿岗位名',
sortOrder: 20,
isActive: false,
},
}
const tx = {
performanceSurveyPositionPlan: {
findFirst: jest.fn().mockResolvedValue(existingDraft),
update: jest.fn().mockResolvedValue(updatedDraft),
findUnique: jest.fn().mockResolvedValue(updatedDraft),
},
performanceSurveyPositionPlanItem: {
deleteMany: jest.fn(),
createMany: jest.fn(),
},
performanceSurveyPositionGroup: {
findUnique: jest.fn().mockResolvedValue(liveGroup),
update: jest.fn(),
},
performanceSurveyPositionRule: {
deleteMany: jest.fn(),
createMany: jest.fn(),
update: jest.fn(),
},
performanceSurveyConfigAuditLog: {
create: jest.fn().mockResolvedValue({ id: 'audit-1' }),
},
$queryRawUnsafe: jest.fn().mockResolvedValue([{ locked: '1' }]),
}
;(prisma.performanceSurveyPositionGroup.findUnique as jest.Mock).mockResolvedValue(liveGroup)
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback) => callback(tx))
await expect(savePerformanceSurveyPositionGroupDraft({
groupId: liveGroup.id,
name: '草稿岗位名',
sortOrder: 20,
isActive: false,
rules: [{ positionTitle: '草稿规则' }],
effectiveMonth: '2026-08',
reason: '配置调整',
actorUserId: 'admin-1',
actorName: '管理员',
})).resolves.toMatchObject({
groupId: liveGroup.id,
draftPlanId: existingDraft.id,
version: existingDraft.version,
})
expect(tx.performanceSurveyPositionPlan.update).toHaveBeenCalledWith(expect.objectContaining({
where: { id: existingDraft.id },
data: expect.objectContaining({
matchRules: [{ positionTitle: '草稿规则', departmentPathPrefix: null }],
groupConfig: {
name: '草稿岗位名',
sortOrder: 20,
isActive: false,
},
}),
}))
expect(tx.performanceSurveyConfigAuditLog.create).toHaveBeenCalledTimes(1)
expect(prisma.performanceSurveyPositionGroup.update).not.toHaveBeenCalled()
expect(prisma.performanceSurveyPositionRule.deleteMany).not.toHaveBeenCalled()
expect(prisma.performanceSurveyPositionRule.createMany).not.toHaveBeenCalled()
expect(tx.performanceSurveyPositionGroup.update).not.toHaveBeenCalled()
expect(tx.performanceSurveyPositionRule.deleteMany).not.toHaveBeenCalled()
expect(tx.performanceSurveyPositionRule.createMany).not.toHaveBeenCalled()
expect(tx.$queryRawUnsafe).toHaveBeenCalledWith(
expect.stringContaining('pg_advisory_xact_lock'),
`performance-survey-position-group:${liveGroup.id}`,
)
})
test('发布只能使用发布前检查确认过的同一份草稿', () => {
const draft = {
id: 'draft-1',
version: 3,
updatedAt: new Date('2026-07-23T10:00:00.000Z'),
}
expect(() => assertPerformanceSurveyPositionPlanPreviewMatches(draft, {
draftPlanId: draft.id,
draftVersion: draft.version,
draftUpdatedAt: '2026-07-23T10:00:00.000Z',
})).not.toThrow()
expect(() => assertPerformanceSurveyPositionPlanPreviewMatches(draft, {
draftPlanId: draft.id,
draftVersion: draft.version,
draftUpdatedAt: '2026-07-23T10:00:01.000Z',
})).toThrow('岗位方案草稿已发生变化,请重新执行发布前检查')
})
test('人员手动分配原因可以留空', async () => {
const assignment = {
id: 'assignment-1',
employeeProfileId: 'employee-1',
assignmentType: 'position',
positionGroupId: 'group-1',
startMonth: '2026-07',
endMonth: null,
reason: '',
status: 'active',
}
const tx = {
$queryRawUnsafe: jest.fn().mockResolvedValue([{ locked: '1' }]),
performanceSurveyEmployeePositionAssignment: {
findFirst: jest.fn().mockResolvedValue(null),
create: jest.fn().mockResolvedValue(assignment),
},
performanceSurveyConfigAuditLog: {
create: jest.fn().mockResolvedValue({ id: 'audit-1' }),
},
}
;(prisma.performanceSurveyPositionGroup.findUnique as jest.Mock).mockResolvedValue({
id: 'group-1',
archivedAt: null,
})
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([{
id: 'employee-1',
displayName: '张三',
}])
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback) => callback(tx))
await expect(createPerformanceSurveyManualPositionAssignments({
employeeIds: ['employee-1'],
positionGroupId: 'group-1',
assignmentType: 'position',
effectiveFromMonth: '2026-07',
effectiveToMonth: null,
reason: '',
actorUserId: 'admin-1',
actorName: '管理员',
})).resolves.toMatchObject({ count: 1 })
expect(tx.performanceSurveyEmployeePositionAssignment.create).toHaveBeenCalledWith({
data: expect.objectContaining({ reason: '' }),
})
})
test('当前或未来岗位方案仍在使用模板时拒绝删除', async () => {
;(prisma.performanceSurveyTemplate.findUnique as jest.Mock).mockResolvedValue({
id: 'template-1',
code: 'visual_review',
versions: [{ id: 'version-1', status: 'published' }],
})
;(prisma.performanceSurveyPositionPlanItem.count as jest.Mock).mockResolvedValue(1)
;(prisma.performanceSurvey.count as jest.Mock).mockResolvedValue(2)
;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock).mockResolvedValue([{
name: '品牌设计师',
plans: [{
id: 'plan-1',
status: 'published',
effectiveMonth: '2026-07',
version: 1,
items: [{ id: 'item-1' }],
}],
}])
await expect(removePerformanceSurveyTemplate({
templateId: 'template-1',
actorUserId: 'admin-1',
actorName: '管理员',
reason: '不再使用',
})).rejects.toThrow('请先从岗位考核项中移除并发布新方案')
expect(prisma.$transaction).not.toHaveBeenCalled()
})
test('历史查询保留已删除岗位和模板,常规查询默认排除已删除岗位', async () => {
const definition = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock).mockResolvedValueOnce([{
id: 'group-archived',
code: 'archived-group',
name: '已删除岗位',
sortOrder: 10,
isActive: true,
isSystem: false,
archivedAt: new Date('2026-07-01T00:00:00.000Z'),
plans: [{
id: 'plan-history',
status: 'published',
effectiveMonth: '2026-06',
version: 1,
groupConfig: {
name: '已删除岗位',
sortOrder: 10,
isActive: true,
},
matchRules: [],
items: [{
id: 'item-history',
isEnabled: true,
sortOrder: 10,
templateVersion: {
id: 'version-history',
version: 1,
status: 'published',
effectiveMonth: '2026-01',
definition,
template: {
archivedAt: new Date('2026-07-01T00:00:00.000Z'),
},
},
}],
}],
}])
await expect(listPerformanceSurveyPositionGroups('2026-06', {
includeArchived: true,
})).resolves.toMatchObject([{
id: 'group-archived',
status: 'ready',
templates: [{
templateVersionId: 'version-history',
templateArchivedAt: new Date('2026-07-01T00:00:00.000Z'),
}],
}])
expect(prisma.performanceSurveyPositionGroup.findMany).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ where: undefined }),
)
;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock).mockResolvedValueOnce([])
await expect(listPerformanceSurveyPositionGroups('2026-07')).resolves.toEqual([])
expect(prisma.performanceSurveyPositionGroup.findMany).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ where: { archivedAt: null } }),
)
})
test('包含历史岗位时,已删除岗位不会在删除月份及之后重新激活', async () => {
const archivedGroup = {
id: 'group-archived',
code: 'archived-group',
name: '旧岗位',
sortOrder: 10,
isActive: true,
isSystem: false,
// 北京时间 7 月 1 日 00:30,UTC 仍是 6 月;业务月份必须按北京时间判断。
archivedAt: new Date('2026-06-30T16:30:00.000Z'),
plans: [],
}
const activeGroup = {
id: 'group-active',
code: 'active-group',
name: '新岗位',
sortOrder: 20,
isActive: true,
isSystem: false,
archivedAt: null,
plans: [],
}
;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock)
.mockResolvedValue([archivedGroup, activeGroup])
await expect(listPerformanceSurveyPositionGroups('2026-07', {
includeArchived: true,
})).resolves.toMatchObject([
{
id: 'group-archived',
isActive: false,
status: 'disabled',
},
{
id: 'group-active',
isActive: true,
},
])
})
})