/** @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 & Pick): 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('终审计分人员必须唯一') }) })