gongxiangp2/lib/__tests__/performance-survey-position-config.test.ts
2026-07-29 16:59:01 +08:00

289 lines
11 KiB
TypeScript

/** @jest-environment node */
import {
PERFORMANCE_SURVEY_POSITION_GROUP_SEEDS,
createPerformanceSurveyPositionGroupIdentity,
getAvailablePerformanceSurveyPositionTemplates,
matchesPerformanceSurveyPositionRule,
paginatePerformanceSurveyTemplates,
performanceSurveyMonthRangesOverlap,
resolvePerformanceSurveyPositionMembership,
validatePerformanceSurveyPositionPlanTemplateVersions,
validatePerformanceSurveyTemplateDefinition,
type PositionGroupRuntime,
} from '@/lib/performance-survey-position-config'
import { DEFAULT_PERFORMANCE_SURVEY_TEMPLATES } from '@/lib/performance-survey-templates'
function group(input: Partial<PositionGroupRuntime> & Pick<PositionGroupRuntime, 'id' | 'code' | 'name'>): PositionGroupRuntime {
return {
sortOrder: 0,
isActive: true,
isSystem: false,
archivedAt: null,
status: 'ready',
planId: 'plan-1',
planVersion: 1,
effectiveMonth: '2026-07',
rules: [],
templates: [],
...input,
}
}
describe('performance survey position configuration', () => {
test('新岗位内部标识由系统生成,不依赖管理员填写代码', () => {
const identity = createPerformanceSurveyPositionGroupIdentity()
expect(identity.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
expect(identity.code).toBe(`position_${identity.id.replace(/-/g, '')}`)
})
test('预置八个岗位组且代码稳定唯一', () => {
expect(PERFORMANCE_SURVEY_POSITION_GROUP_SEEDS).toHaveLength(8)
expect(new Set(PERFORMANCE_SURVEY_POSITION_GROUP_SEEDS.map((item) => item.code)).size).toBe(8)
})
test('已删除模板保留在历史岗位方案中,但不能用于新发起', () => {
const activeTemplate = {
...structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0]),
templateVersionId: 'version-active',
templateArchivedAt: null,
}
const archivedTemplate = {
...structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[1]),
templateVersionId: 'version-archived',
templateArchivedAt: new Date('2026-07-01T00:00:00.000Z'),
}
const positionGroup = group({
id: 'history-group',
code: 'history-group',
name: '历史岗位',
templates: [activeTemplate, archivedTemplate],
})
expect(positionGroup.templates).toHaveLength(2)
expect(getAvailablePerformanceSurveyPositionTemplates(positionGroup)).toEqual([activeTemplate])
})
test('单条规则内部门和职位按 AND 匹配,部门路径支持子部门', () => {
const rule = { departmentPathPrefix: '集团/品牌中心', positionTitle: 'AI策划' }
expect(matchesPerformanceSurveyPositionRule({
departmentPath: '集团/品牌中心/品牌创意A部',
positionTitle: 'AI策划',
}, rule)).toBe(true)
expect(matchesPerformanceSurveyPositionRule({
departmentPath: '集团/综合中心',
positionTitle: 'AI策划',
}, rule)).toBe(false)
expect(matchesPerformanceSurveyPositionRule({
departmentPath: '集团/品牌中心/品牌创意A部',
positionTitle: '品牌设计师',
}, rule)).toBe(false)
})
test('同组多条规则按 OR 匹配,跨组重复命中明确返回冲突', () => {
const creative = group({
id: 'creative',
code: 'creative',
name: '创意',
rules: [
{ departmentPathPrefix: '集团/创意一部', positionTitle: 'AI策划' },
{ departmentPathPrefix: '集团/创意二部', positionTitle: 'AI策划' },
],
})
const duplicate = group({
id: 'duplicate',
code: 'duplicate',
name: '重复组',
rules: [{ positionTitle: 'AI策划' }],
})
const employee = { departmentPath: '集团/创意二部', positionTitle: 'AI策划' }
expect(resolvePerformanceSurveyPositionMembership(employee, [creative])).toMatchObject({
status: 'matched',
group: { code: 'creative' },
})
expect(resolvePerformanceSurveyPositionMembership(employee, [creative, duplicate])).toMatchObject({
status: 'conflict',
group: null,
})
expect(resolvePerformanceSurveyPositionMembership({
departmentPath: '集团/财务部',
positionTitle: '会计',
}, [creative])).toMatchObject({ status: 'unmatched', group: null })
})
test('兼任部门参与岗位匹配,跨部门命中不同岗位时明确返回冲突', () => {
const primaryGroup = group({
id: 'primary',
code: 'primary',
name: '营销策划',
rules: [{ departmentPathPrefix: '集团/营销策划中心', positionTitle: '营销策划主管' }],
})
const secondaryGroup = group({
id: 'secondary',
code: 'secondary',
name: '品牌创意B部',
rules: [{ departmentPathPrefix: '集团/品牌中心/品牌创意B部', positionTitle: '营销策划主管' }],
})
const employee = {
departmentPath: '集团/营销策划中心',
positionTitle: '营销策划主管',
departmentMemberships: [
{
departmentName: '营销策划中心',
departmentPath: '集团/营销策划中心',
isPrimary: true,
},
{
departmentName: '品牌创意B部',
departmentPath: '集团/品牌中心/品牌创意B部',
isPrimary: false,
},
],
}
expect(matchesPerformanceSurveyPositionRule(employee, secondaryGroup.rules[0])).toBe(true)
expect(resolvePerformanceSurveyPositionMembership(employee, [secondaryGroup])).toMatchObject({
status: 'matched',
group: { code: 'secondary' },
})
expect(resolvePerformanceSurveyPositionMembership(employee, [primaryGroup, secondaryGroup])).toMatchObject({
status: 'conflict',
group: null,
matches: [
{ code: 'primary' },
{ code: 'secondary' },
],
})
})
test('人工岗位或排除配置优先于自动岗位规则', () => {
const automatic = group({
id: 'automatic',
code: 'automatic',
name: '自动岗位',
rules: [{ positionTitle: 'AI策划' }],
})
const manual = group({
id: 'manual',
code: 'manual',
name: '人工岗位',
rules: [],
})
const employee = { positionTitle: 'AI策划' }
expect(resolvePerformanceSurveyPositionMembership(employee, [automatic, manual], {
id: 'assignment-position',
assignmentType: 'position',
positionGroupId: manual.id,
})).toMatchObject({
status: 'matched',
group: { id: manual.id },
source: 'manual',
manualAssignmentId: 'assignment-position',
})
expect(resolvePerformanceSurveyPositionMembership(employee, [automatic, manual], {
id: 'assignment-excluded',
assignmentType: 'excluded',
})).toEqual({
status: 'excluded',
group: null,
matches: [],
source: 'manual',
manualAssignmentId: 'assignment-excluded',
})
})
test('人工岗位月份区间按闭区间判断重叠', () => {
expect(performanceSurveyMonthRangesOverlap(
{ startMonth: '2026-01', endMonth: '2026-03' },
{ startMonth: '2026-03', endMonth: '2026-05' },
)).toBe(true)
expect(performanceSurveyMonthRangesOverlap(
{ startMonth: '2026-01', endMonth: '2026-02' },
{ startMonth: '2026-03', endMonth: null },
)).toBe(false)
expect(performanceSurveyMonthRangesOverlap(
{ startMonth: '2026-03', endMonth: null },
{ startMonth: '2099-01', endMonth: '2099-12' },
)).toBe(true)
})
test('岗位方案拒绝重复和未发布模板,但不限制模板与岗位的生效月份先后', () => {
expect(() => validatePerformanceSurveyPositionPlanTemplateVersions([
{ id: 'version-1', templateId: 'template-1', status: 'published', effectiveMonth: '2026-01' },
{ id: 'version-2', templateId: 'template-1', status: 'published', effectiveMonth: '2026-02' },
], '2026-03')).toThrow('同一模板只能选择一个版本')
expect(() => validatePerformanceSurveyPositionPlanTemplateVersions([
{ id: 'version-1', templateId: 'template-1', status: 'draft', effectiveMonth: '2026-01' },
], '2026-03')).toThrow('只能选择已发布模板版本')
expect(() => validatePerformanceSurveyPositionPlanTemplateVersions([
{ id: 'version-1', templateId: 'template-1', status: 'published', effectiveMonth: '2026-04' },
], '2026-03')).not.toThrow()
})
test('看板考核项固定每页最多四项并钳制越界页码', () => {
const items = Array.from({ length: 9 }, (_, index) => `item-${index + 1}`)
expect(paginatePerformanceSurveyTemplates(items, 1)).toEqual({
items: ['item-1', 'item-2', 'item-3', 'item-4'],
pager: { page: 1, pageSize: 4, totalPages: 3, totalItems: 9, start: 1, end: 4 },
})
expect(paginatePerformanceSurveyTemplates(items, 2)).toMatchObject({
items: ['item-5', 'item-6', 'item-7', 'item-8'],
pager: { page: 2, start: 5, end: 8 },
})
expect(paginatePerformanceSurveyTemplates(items, 99)).toMatchObject({
items: ['item-9'],
pager: { page: 3, start: 9, end: 9 },
})
expect(paginatePerformanceSurveyTemplates([], 3).pager).toEqual({
page: 1,
pageSize: 4,
totalPages: 1,
totalItems: 0,
start: 0,
end: 0,
})
})
test('一个完整考核项也允许发布,不要求凑满四项', () => {
const template = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
template.questions = [template.questions[0]]
template.maxTotal = Number(template.questions[0].maxScore || template.questionMaxScore || 20)
expect(() => validatePerformanceSurveyTemplateDefinition(template)).not.toThrow()
})
test('模板总分必须与评分项满分合计一致', () => {
const template = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
template.maxTotal = Number(template.maxTotal || 0) + 1
expect(() => validatePerformanceSurveyTemplateDefinition(template)).toThrow('与模板总分')
})
test('拒绝空题目、重复阶段顺序和不唯一的终审计分人', () => {
const empty = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
empty.questions = []
expect(() => validatePerformanceSurveyTemplateDefinition(empty)).toThrow('至少需要 1 个评分项')
const duplicateOrder = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0])
duplicateOrder.workflow!.stages.push({
key: 'another',
label: '第二阶段',
order: duplicateOrder.workflow!.stages[0].order,
reviewers: [{ source: 'self', label: '本人' }],
})
expect(() => validatePerformanceSurveyTemplateDefinition(duplicateOrder)).toThrow('评审阶段顺序重复')
const ambiguousFinal = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[3])
ambiguousFinal.workflow!.stages[1].reviewers.push({
source: 'fixed_employee',
label: '另一位终审人',
employeeId: 'employee-2',
})
ambiguousFinal.workflow!.stages[1].reviewers[1].employeeId = 'employee-1'
expect(() => validatePerformanceSurveyTemplateDefinition(ambiguousFinal)).toThrow('终审计分人员必须唯一')
})
})