1908 lines
69 KiB
TypeScript
1908 lines
69 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import type { Session } from 'next-auth'
|
||
import {
|
||
PerformanceSurveyApiError,
|
||
cancelPerformanceSurvey,
|
||
createPerformanceSurvey,
|
||
getPerformanceSurveyDetail,
|
||
getPerformanceSurveyDashboard,
|
||
getPerformanceSurveyDashboardDetail,
|
||
getPerformanceSurveyMe,
|
||
getPerformanceSurveyResults,
|
||
listPerformanceSurveys,
|
||
previewPerformanceSurveyEmployeeProfiles,
|
||
searchPerformanceSurveyEmployees,
|
||
submitPerformanceSurveyResponse,
|
||
updatePerformanceSurvey,
|
||
} from '@/lib/performance-survey-service'
|
||
import { prisma } from '@/lib/prisma'
|
||
import {
|
||
ensurePerformanceSurveyEmployeeProfileForUser,
|
||
getPerformanceSurveyAccountProfileWhere,
|
||
previewPerformanceSurveyEmployeeSync,
|
||
} from '@/lib/performance-survey-report-sync'
|
||
import {
|
||
DEFAULT_PERFORMANCE_SURVEY_TEMPLATES,
|
||
getPerformanceSurveyTemplate,
|
||
listPerformanceSurveyTemplates,
|
||
} from '@/lib/performance-survey-templates'
|
||
import {
|
||
getPerformanceSurveyPositionGroupByCode,
|
||
getPerformanceSurveyPositionGroupForEmployee,
|
||
listPerformanceSurveyPositionGroups,
|
||
} from '@/lib/performance-survey-position-config'
|
||
import { queueSurveyCreatorNotification } from '@/lib/performance-survey-notifications'
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
user: { findUnique: jest.fn(), findFirst: jest.fn(), findMany: jest.fn() },
|
||
performanceSurvey: {
|
||
findUnique: jest.fn(),
|
||
findMany: jest.fn(),
|
||
update: jest.fn(),
|
||
count: jest.fn(),
|
||
},
|
||
performanceSurveyAssignment: { findFirst: jest.fn(), findMany: jest.fn(), count: jest.fn() },
|
||
performanceSurveyResponse: { findMany: jest.fn() },
|
||
performanceSurveyTarget: { findMany: jest.fn() },
|
||
performanceSurveyEmployeeProfile: {
|
||
findFirst: jest.fn(),
|
||
findMany: jest.fn(),
|
||
count: jest.fn(),
|
||
},
|
||
performanceSurveyEmployeeDepartmentMembership: {
|
||
findMany: jest.fn(),
|
||
},
|
||
$transaction: jest.fn(),
|
||
},
|
||
}))
|
||
|
||
jest.mock('@/lib/performance-survey-templates', () => {
|
||
const actual = jest.requireActual('@/lib/performance-survey-templates')
|
||
return {
|
||
...actual,
|
||
getPerformanceSurveyTemplate: jest.fn(),
|
||
listPerformanceSurveyTemplates: jest.fn(),
|
||
}
|
||
})
|
||
|
||
jest.mock('@/lib/performance-survey-position-config', () => {
|
||
const actual = jest.requireActual('@/lib/performance-survey-position-config')
|
||
return {
|
||
...actual,
|
||
getPerformanceSurveyPositionGroupByCode: jest.fn(),
|
||
getPerformanceSurveyPositionGroupForEmployee: jest.fn(),
|
||
listPerformanceSurveyPositionGroups: jest.fn(),
|
||
}
|
||
})
|
||
|
||
jest.mock('@/lib/performance-survey-report-sync', () => ({
|
||
ensurePerformanceSurveyDailyEmployeeSyncStarted: jest.fn(),
|
||
ensurePerformanceSurveyEmployeeProfileForUser: jest.fn(),
|
||
getPerformanceSurveyAccountProfileWhere: jest.fn(async () => ({ id: { not: '__hidden__' } })),
|
||
previewPerformanceSurveyEmployeeSync: jest.fn(),
|
||
syncPerformanceSurveyEmployees: jest.fn(),
|
||
}))
|
||
|
||
jest.mock('@/lib/performance-survey-notifications', () => ({
|
||
ensurePerformanceSurveyNotificationWorkerStarted: jest.fn(),
|
||
queueDuePerformanceSurveyReminders: jest.fn(),
|
||
queueSurveyAssignmentNotifications: jest.fn(),
|
||
queueSurveyCreatorNotification: jest.fn(),
|
||
queueSurveyResultNotifications: jest.fn(),
|
||
queueSurveyTargetConfirmationNotifications: jest.fn(),
|
||
recoverDuePerformanceSurveyNotifications: jest.fn(),
|
||
refreshPerformanceSurveyNotificationReadStatuses: jest.fn(),
|
||
retryPerformanceSurveyNotification: jest.fn(),
|
||
}))
|
||
|
||
jest.mock('@/lib/dingtalk-client', () => ({
|
||
getDingtalkPerformanceSurveyNotificationChannel: jest.fn(() => 'work_notice'),
|
||
getDingtalkRobotOtoConfigStatus: jest.fn(() => ({ configured: true })),
|
||
getDingtalkRobotOtoTestUserIds: jest.fn(() => []),
|
||
getDingtalkWorkNoticeConfigStatus: jest.fn(() => ({ configured: true })),
|
||
}))
|
||
|
||
jest.mock('@/lib/s3', () => ({
|
||
deleteFromS3: jest.fn(),
|
||
uploadToS3: jest.fn(),
|
||
}))
|
||
|
||
const session = { user: { id: 'user-1' } } as Session
|
||
|
||
const baseUser = {
|
||
id: 'user-1',
|
||
username: 'tester',
|
||
email: 'tester@example.com',
|
||
role: 'USER',
|
||
department: '策划部',
|
||
managedDepartment: null,
|
||
isSuperAdmin: false,
|
||
dingtalkUserId: 'ding-user-1',
|
||
realName: '测试人员',
|
||
avatar: null,
|
||
}
|
||
|
||
const baseEmployee = {
|
||
id: 'employee-1',
|
||
displayName: '测试人员',
|
||
departmentName: '策划部',
|
||
departmentPath: '集团A/品牌中心/策划部',
|
||
levelType: 'P',
|
||
positionLevel: 'P5',
|
||
isLeadership: false,
|
||
}
|
||
|
||
function buildConfiguredPositionGroup() {
|
||
const templates = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES).map((template, index) => ({
|
||
...template,
|
||
templateVersionId: `template-version-${index + 1}`,
|
||
templateVersion: 1,
|
||
templateVersionStatus: 'published' as const,
|
||
effectiveMonth: '2026-07',
|
||
}))
|
||
const fixedReviewer = templates.find((template) => template.code === 'visual_brand_tone')
|
||
?.workflow?.stages.flatMap((stage) => stage.reviewers)
|
||
.find((reviewer) => reviewer.source === 'fixed_employee')
|
||
if (fixedReviewer) fixedReviewer.employeeId = 'employee-yuanzhe'
|
||
return {
|
||
id: 'position-group-brand',
|
||
code: 'brand_designer',
|
||
name: '品牌设计师',
|
||
sortOrder: 10,
|
||
isActive: true,
|
||
status: 'ready' as const,
|
||
planId: 'position-plan-1',
|
||
planVersion: 1,
|
||
effectiveMonth: '2026-07',
|
||
rules: [{ departmentPathPrefix: '集团A/品牌中心', positionTitle: null }],
|
||
templates,
|
||
}
|
||
}
|
||
|
||
function buildSurvey(overrides: Record<string, unknown> = {}) {
|
||
return {
|
||
id: 'survey-1',
|
||
title: '2026年6月 · 课程分享',
|
||
description: null,
|
||
surveyKind: 'person_period',
|
||
templateCode: 'course_share',
|
||
templateVersionId: 'template-version-1',
|
||
positionGroupId: 'position-group-brand',
|
||
positionGroupCode: 'brand_designer',
|
||
positionGroupSnapshot: { code: 'brand_designer', name: '品牌设计师' },
|
||
templateSnapshot: {
|
||
code: 'course_share',
|
||
name: '课程分享月度考核',
|
||
shortName: '课程分享',
|
||
questions: [],
|
||
},
|
||
status: 'draft',
|
||
creatorUserId: 'user-1',
|
||
creatorName: '测试人员',
|
||
creatorEmployeeId: 'employee-1',
|
||
departmentName: '策划部',
|
||
assessmentMonth: '2026-06',
|
||
dueAt: null,
|
||
openedAt: null,
|
||
closedAt: null,
|
||
createdAt: new Date('2026-06-01T00:00:00.000Z'),
|
||
targets: [{
|
||
id: 'target-1',
|
||
employeeProfileId: 'employee-1',
|
||
targetType: 'employee',
|
||
targetName: '测试人员',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
assignments: [],
|
||
_count: { assignments: 0, responses: 0, targets: 1 },
|
||
...overrides,
|
||
}
|
||
}
|
||
|
||
const transactionClient = {
|
||
$queryRawUnsafe: jest.fn(),
|
||
performanceSurveyTarget: { findMany: jest.fn(), createMany: jest.fn() },
|
||
performanceSurvey: { update: jest.fn(), create: jest.fn() },
|
||
performanceSurveyAssignment: { createMany: jest.fn(), update: jest.fn() },
|
||
performanceSurveyResponse: { create: jest.fn() },
|
||
performanceSurveyNotification: { updateMany: jest.fn() },
|
||
}
|
||
|
||
describe('performance survey service policies', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(baseUser)
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset()
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(baseEmployee)
|
||
;(previewPerformanceSurveyEmployeeSync as jest.Mock).mockReset()
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey())
|
||
;(prisma.performanceSurvey.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurveyAssignment.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.performanceSurveyAssignment.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurveyAssignment.count as jest.Mock).mockResolvedValue(0)
|
||
;(prisma.performanceSurveyResponse.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurvey.update as jest.Mock).mockResolvedValue({})
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurveyEmployeeProfile.count as jest.Mock).mockResolvedValue(0)
|
||
;(prisma.performanceSurveyEmployeeDepartmentMembership.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeDepartmentMembership.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.user.findMany as jest.Mock).mockReset()
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
|
||
;(prisma.user.findFirst as jest.Mock).mockReset()
|
||
;(prisma.user.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.performanceSurvey.count as jest.Mock).mockResolvedValue(0)
|
||
const configuredGroup = buildConfiguredPositionGroup()
|
||
;(listPerformanceSurveyPositionGroups as jest.Mock).mockResolvedValue([configuredGroup])
|
||
;(getPerformanceSurveyPositionGroupByCode as jest.Mock).mockResolvedValue(configuredGroup)
|
||
;(getPerformanceSurveyPositionGroupForEmployee as jest.Mock).mockResolvedValue({
|
||
status: 'matched',
|
||
group: configuredGroup,
|
||
matches: [configuredGroup],
|
||
})
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockReset()
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'course_share',
|
||
name: '课程分享月度考核',
|
||
shortName: '课程分享',
|
||
description: '',
|
||
surveyType: 'person_period',
|
||
weight: 0.3,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(listPerformanceSurveyTemplates as jest.Mock).mockResolvedValue([
|
||
{
|
||
code: 'course_share',
|
||
name: '课程分享月度考核',
|
||
shortName: '课程分享',
|
||
description: '',
|
||
surveyType: 'person_period',
|
||
weight: 0.3,
|
||
questions: [],
|
||
scoreBands: [],
|
||
},
|
||
])
|
||
transactionClient.$queryRawUnsafe.mockImplementation(async (query: string) => (
|
||
query.includes('FOR UPDATE') ? [{ status: 'open', dueAt: null }] : [{ locked: '' }]
|
||
))
|
||
transactionClient.performanceSurveyTarget.findMany.mockResolvedValue([])
|
||
transactionClient.performanceSurveyTarget.createMany.mockResolvedValue({ count: 1 })
|
||
transactionClient.performanceSurvey.update.mockResolvedValue({})
|
||
transactionClient.performanceSurvey.create.mockResolvedValue({})
|
||
transactionClient.performanceSurveyAssignment.createMany.mockResolvedValue({ count: 1 })
|
||
transactionClient.performanceSurveyAssignment.update.mockResolvedValue({})
|
||
transactionClient.performanceSurveyResponse.create.mockResolvedValue({ id: 'response-1' })
|
||
transactionClient.performanceSurveyNotification.updateMany.mockResolvedValue({ count: 0 })
|
||
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback) => callback(transactionClient))
|
||
})
|
||
|
||
test('普通用户不能执行人员同步预检', async () => {
|
||
await expect(previewPerformanceSurveyEmployeeProfiles(session, { limit: 100 }))
|
||
.rejects.toMatchObject({
|
||
message: '需要系统管理员权限',
|
||
status: 403,
|
||
})
|
||
expect(previewPerformanceSurveyEmployeeSync).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('素材库系统管理员但考核档案不是M级时不能执行人员同步预检', async () => {
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
||
...baseUser,
|
||
role: 'ADMIN',
|
||
isSuperAdmin: true,
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue({
|
||
levelType: 'P',
|
||
positionLevel: 'P5',
|
||
isLeadership: false,
|
||
})
|
||
|
||
await expect(previewPerformanceSurveyEmployeeProfiles(session, { limit: 200 }))
|
||
.rejects.toMatchObject({
|
||
message: '需要系统管理员权限',
|
||
status: 403,
|
||
})
|
||
expect(previewPerformanceSurveyEmployeeSync).not.toHaveBeenCalled()
|
||
expect(ensurePerformanceSurveyEmployeeProfileForUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('M级人员在考核调研内可执行系统管理员的只读预检', async () => {
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue({
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(previewPerformanceSurveyEmployeeSync as jest.Mock).mockResolvedValue({
|
||
mode: 'preview',
|
||
dryRun: true,
|
||
})
|
||
|
||
await expect(previewPerformanceSurveyEmployeeProfiles(session, { limit: 200 }))
|
||
.resolves.toEqual({
|
||
mode: 'preview',
|
||
dryRun: true,
|
||
})
|
||
expect(previewPerformanceSurveyEmployeeSync).toHaveBeenCalledWith({ limit: 200 })
|
||
expect(ensurePerformanceSurveyEmployeeProfileForUser).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('人员同步预检超时会返回中文错误且强调未写入', async () => {
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue({
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(previewPerformanceSurveyEmployeeSync as jest.Mock).mockRejectedValue(
|
||
Object.assign(new Error('request timeout'), { code: 'ETIMEOUT' }),
|
||
)
|
||
|
||
await expect(previewPerformanceSurveyEmployeeProfiles(session, {}))
|
||
.rejects.toMatchObject({
|
||
message: expect.stringContaining('预检未写入任何数据'),
|
||
status: 504,
|
||
})
|
||
})
|
||
|
||
test('视觉模板统计只采用最终评分角色,不混入自评和主管评分', async () => {
|
||
;(listPerformanceSurveyTemplates as jest.Mock).mockResolvedValue([{
|
||
code: 'visual_brand_tone',
|
||
name: '品牌调性视觉评分',
|
||
shortName: '视觉评分',
|
||
description: '',
|
||
surveyType: 'visual_case',
|
||
weight: 1,
|
||
maxTotal: 120,
|
||
questions: [],
|
||
scoreBands: [],
|
||
}])
|
||
;(prisma.performanceSurveyResponse.findMany as jest.Mock).mockResolvedValue([
|
||
{
|
||
id: 'response-self',
|
||
survey: { templateCode: 'visual_brand_tone' },
|
||
assignment: { evaluatorSnapshot: { performanceSurveyWorkflowRole: 'self' } },
|
||
totalScore: 40,
|
||
createdAt: new Date('2026-07-19T00:00:00.000Z'),
|
||
scores: {},
|
||
respondentName: '自评人',
|
||
},
|
||
{
|
||
id: 'response-manager',
|
||
survey: { templateCode: 'visual_brand_tone' },
|
||
assignment: { evaluatorSnapshot: { performanceSurveyWorkflowRole: 'manager' } },
|
||
totalScore: 80,
|
||
createdAt: new Date('2026-07-19T00:01:00.000Z'),
|
||
scores: {},
|
||
respondentName: '主管',
|
||
},
|
||
{
|
||
id: 'response-final',
|
||
survey: { templateCode: 'visual_brand_tone' },
|
||
assignment: { evaluatorSnapshot: { performanceSurveyWorkflowRole: 'visual_final' } },
|
||
totalScore: 105,
|
||
createdAt: new Date('2026-07-19T00:02:00.000Z'),
|
||
scores: {},
|
||
respondentName: '元泽',
|
||
},
|
||
])
|
||
|
||
const result = await listPerformanceSurveys(session)
|
||
expect(result.templateStats[0].responseCount).toBe(1)
|
||
expect(result.templateStats[0].aggregate.achievementScore).toBe(105)
|
||
})
|
||
|
||
test('公共看板从当月调研目标反查人员且不再构造素材账户 JSON 权限条件', async () => {
|
||
const dashboardEmployee = {
|
||
...baseEmployee,
|
||
id: 'employee-public',
|
||
displayName: '公共看板人员',
|
||
}
|
||
;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([{
|
||
id: 'target-public',
|
||
surveyId: 'survey-public',
|
||
employeeProfileId: dashboardEmployee.id,
|
||
employeeProfile: dashboardEmployee,
|
||
createdAt: new Date('2026-07-01T00:00:00.000Z'),
|
||
survey: {
|
||
id: 'survey-public',
|
||
title: '2026年7月 · 课程分享',
|
||
templateCode: 'course_share',
|
||
status: 'closed',
|
||
assessmentMonth: '2026-07',
|
||
dueAt: null,
|
||
openedAt: null,
|
||
closedAt: new Date('2026-07-10T00:00:00.000Z'),
|
||
createdAt: new Date('2026-07-01T00:00:00.000Z'),
|
||
},
|
||
assignments: [],
|
||
responses: [],
|
||
_count: { workItems: 0 },
|
||
}])
|
||
|
||
const result = await getPerformanceSurveyDashboard(session, new URLSearchParams({
|
||
view: 'public',
|
||
month: '2026-07',
|
||
}))
|
||
|
||
expect(getPerformanceSurveyAccountProfileWhere).not.toHaveBeenCalled()
|
||
expect(prisma.performanceSurveyEmployeeProfile.findMany).not.toHaveBeenCalled()
|
||
expect(prisma.performanceSurveyTarget.findMany).toHaveBeenCalledTimes(1)
|
||
const query = (prisma.performanceSurveyTarget.findMany as jest.Mock).mock.calls[0][0]
|
||
expect(query.where).toEqual(expect.objectContaining({
|
||
targetType: 'employee',
|
||
employeeProfileId: { not: null },
|
||
employeeProfile: { is: {} },
|
||
}))
|
||
expect(query.select.employeeProfile).toBe(true)
|
||
expect(result.public.rows).toHaveLength(1)
|
||
expect(result.public.rows[0]).toEqual(expect.objectContaining({
|
||
month: '2026-07',
|
||
hasData: true,
|
||
employee: expect.objectContaining({ id: dashboardEmployee.id }),
|
||
}))
|
||
})
|
||
|
||
test('月度看板使用调研创建时的模板快照,并直接读取历史实际得分而非旧 KPI 分', async () => {
|
||
const dashboardEmployee = {
|
||
...baseEmployee,
|
||
id: 'employee-history',
|
||
displayName: '历史评分人员',
|
||
}
|
||
;(listPerformanceSurveyTemplates as jest.Mock).mockResolvedValue([{
|
||
code: 'course_share',
|
||
name: '当前课程分享模板',
|
||
shortName: '课程分享',
|
||
description: '',
|
||
surveyType: 'person_period',
|
||
weight: 0.3,
|
||
maxTotal: 100,
|
||
questions: [],
|
||
scoreBands: [{ min: 77, max: 100, outMin: 60, outMax: 120 }],
|
||
}])
|
||
;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([{
|
||
id: 'target-history',
|
||
surveyId: 'survey-history',
|
||
employeeProfileId: dashboardEmployee.id,
|
||
employeeProfile: dashboardEmployee,
|
||
createdAt: new Date('2026-07-01T00:00:00.000Z'),
|
||
survey: {
|
||
id: 'survey-history',
|
||
title: '历史课程分享',
|
||
templateCode: 'course_share',
|
||
templateSnapshot: {
|
||
code: 'course_share',
|
||
name: '历史 20 分制课程分享',
|
||
description: '',
|
||
surveyType: 'person_period',
|
||
weight: 0.3,
|
||
maxTotal: 20,
|
||
questions: [],
|
||
scoreBands: [{ min: 0, max: 20, outMin: 0, outMax: 120 }],
|
||
},
|
||
status: 'closed',
|
||
assessmentMonth: '2026-07',
|
||
dueAt: null,
|
||
openedAt: null,
|
||
closedAt: new Date('2026-07-10T00:00:00.000Z'),
|
||
createdAt: new Date('2026-07-01T00:00:00.000Z'),
|
||
},
|
||
assignments: [{ id: 'assignment-history', status: 'submitted', submittedAt: new Date() }],
|
||
responses: [{
|
||
id: 'response-history',
|
||
totalScore: 18,
|
||
kpiScore: 108,
|
||
createdAt: new Date('2026-07-02T00:00:00.000Z'),
|
||
scores: {},
|
||
respondentName: '历史评价人',
|
||
assignment: { evaluatorSnapshot: {} },
|
||
}],
|
||
_count: { workItems: 0 },
|
||
}])
|
||
|
||
const result = await getPerformanceSurveyDashboard(session, new URLSearchParams({
|
||
view: 'public',
|
||
month: '2026-07',
|
||
}))
|
||
const cell = result.public.rows[0].cells.course_share
|
||
|
||
expect(cell.score).toBe(18)
|
||
expect(cell.rawAverage).toBe(18)
|
||
expect(cell.effectiveAverage).toBe(18)
|
||
expect(cell.rawMaxTotal).toBe(20)
|
||
})
|
||
|
||
test('公共看板关系查询继续应用领导组织范围及部门和人员筛选', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
levelType: 'M',
|
||
positionLevel: 'M2',
|
||
isLeadership: true,
|
||
})
|
||
|
||
await getPerformanceSurveyDashboard(session, new URLSearchParams({
|
||
view: 'public',
|
||
month: '2026-07',
|
||
department: 'deptPath:%E9%9B%86%E5%9B%A2A%2F%E5%93%81%E7%89%8C%E4%B8%AD%E5%BF%83',
|
||
employeeId: 'employee-public',
|
||
}))
|
||
|
||
const employeeWhere = (prisma.performanceSurveyTarget.findMany as jest.Mock).mock.calls[0][0]
|
||
.where.employeeProfile.is
|
||
expect(employeeWhere.AND).toEqual(expect.arrayContaining([
|
||
{
|
||
OR: [
|
||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||
{
|
||
departmentMemberships: {
|
||
some: {
|
||
OR: [
|
||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{ id: 'employee-public' },
|
||
]))
|
||
expect(JSON.stringify(employeeWhere)).toContain('集团A/品牌中心')
|
||
expect(getPerformanceSurveyAccountProfileWhere).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('M级作为考核系统管理员可跨组织查看人员考核明细', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'employee-other',
|
||
displayName: '另一组织员工',
|
||
departmentName: '策划部',
|
||
departmentPath: '集团B/品牌中心/策划部',
|
||
})
|
||
|
||
const request = new URLSearchParams({
|
||
month: '2026-07',
|
||
employeeId: 'employee-other',
|
||
templateCode: 'course_share',
|
||
})
|
||
|
||
await expect(getPerformanceSurveyDashboardDetail(session, request)).resolves.toEqual(
|
||
expect.objectContaining({
|
||
employee: expect.objectContaining({ id: 'employee-other' }),
|
||
}),
|
||
)
|
||
expect(prisma.performanceSurveyEmployeeProfile.count).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('M级作为考核系统管理员时不依赖本人组织路径查看他人详情', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentPath: null,
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'employee-other',
|
||
displayName: '其他员工',
|
||
departmentName: '策划部',
|
||
departmentPath: '集团B/品牌中心/策划部',
|
||
})
|
||
|
||
await expect(getPerformanceSurveyDashboardDetail(session, new URLSearchParams({
|
||
month: '2026-07',
|
||
employeeId: 'employee-other',
|
||
templateCode: 'course_share',
|
||
}))).resolves.toEqual(expect.objectContaining({
|
||
employee: expect.objectContaining({ id: 'employee-other' }),
|
||
}))
|
||
|
||
expect(prisma.performanceSurveyEmployeeProfile.count).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('考核明细查询排除已撤销调研', async () => {
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(baseEmployee)
|
||
;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([])
|
||
|
||
await getPerformanceSurveyDashboardDetail(session, new URLSearchParams({
|
||
month: '2026-07',
|
||
employeeId: 'employee-1',
|
||
templateCode: 'course_share',
|
||
}))
|
||
|
||
const surveyWhere = (prisma.performanceSurveyTarget.findMany as jest.Mock).mock.calls[0][0].where.survey
|
||
expect(surveyWhere.AND).toContainEqual({ status: { not: 'cancelled' } })
|
||
})
|
||
|
||
test('普通问卷评分已提交后,完成通知排队失败不会把提交结果反转为失败', async () => {
|
||
const openSurvey = buildSurvey({
|
||
status: 'open',
|
||
templateSnapshot: {
|
||
code: 'course_share',
|
||
name: '课程分享',
|
||
description: '',
|
||
surveyType: 'person_period',
|
||
weight: 0.3,
|
||
questions: [{ id: 'usefulness', title: '内容实用度', maxScore: 20 }],
|
||
scoreBands: [{ min: 0, max: 20, outMin: 0, outMax: 120 }],
|
||
},
|
||
})
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(openSurvey)
|
||
;(prisma.performanceSurveyAssignment.findFirst as jest.Mock).mockResolvedValue({
|
||
id: 'assignment-1',
|
||
targetId: 'target-1',
|
||
status: 'pending',
|
||
evaluatorSnapshot: {},
|
||
response: null,
|
||
target: { id: 'target-1' },
|
||
})
|
||
;(prisma.performanceSurveyAssignment.count as jest.Mock).mockResolvedValue(0)
|
||
;(queueSurveyCreatorNotification as jest.Mock).mockRejectedValueOnce(new Error('notification queue unavailable'))
|
||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||
|
||
await expect(submitPerformanceSurveyResponse(session, 'survey-1', {
|
||
targetId: 'target-1',
|
||
scores: { usefulness: 10 },
|
||
})).resolves.toEqual({ id: 'response-1', message: '提交成功' })
|
||
|
||
expect(transactionClient.performanceSurveyResponse.create).toHaveBeenCalledTimes(1)
|
||
expect(transactionClient.performanceSurveyAssignment.update).toHaveBeenCalledWith(expect.objectContaining({
|
||
data: expect.objectContaining({ status: 'submitted' }),
|
||
}))
|
||
expect(consoleError).toHaveBeenCalledWith(
|
||
expect.stringContaining('评分已保存,但后续通知排队失败'),
|
||
expect.any(Error),
|
||
)
|
||
consoleError.mockRestore()
|
||
})
|
||
|
||
test('M级作为考核系统管理员可管理其他人员发起的调研', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
creatorUserId: 'user-other',
|
||
}))
|
||
|
||
await expect(
|
||
updatePerformanceSurvey(session, 'survey-1', { title: '管理员修改' }),
|
||
).resolves.toEqual(expect.objectContaining({ id: 'survey-1' }))
|
||
|
||
expect(prisma.performanceSurvey.count).not.toHaveBeenCalled()
|
||
expect(transactionClient.performanceSurvey.update).toHaveBeenCalled()
|
||
})
|
||
|
||
test('发起人可为非旧代码的视觉模板草稿继续上传作品', async () => {
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'ai_work_review',
|
||
templateSnapshot: {
|
||
code: 'ai_work_review',
|
||
name: 'AI作品评审',
|
||
shortName: 'AI作品评审',
|
||
surveyType: 'visual_case',
|
||
requiredWorkItemCount: 3,
|
||
questions: [],
|
||
},
|
||
targets: [{
|
||
id: 'target-ai-designer',
|
||
employeeProfileId: 'employee-ai-designer',
|
||
targetType: 'employee',
|
||
targetName: 'AI设计师',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
const detail = await getPerformanceSurveyDetail(session, 'survey-1')
|
||
|
||
expect(detail.targets).toEqual([
|
||
expect.objectContaining({
|
||
id: 'target-ai-designer',
|
||
canUploadWorkItems: false,
|
||
canCreateWorkItem: true,
|
||
requiredWorkItemCount: 3,
|
||
workItemUploadRemaining: 3,
|
||
}),
|
||
])
|
||
})
|
||
|
||
test('素材库管理员身份不会扩大个人调研详情到其他被考核人', async () => {
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
||
...baseUser,
|
||
role: 'ADMIN',
|
||
})
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
creatorUserId: 'user-other',
|
||
targets: [{
|
||
id: 'target-other',
|
||
employeeProfileId: 'employee-other',
|
||
targetType: 'employee',
|
||
targetName: '其他员工',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await expect(getPerformanceSurveyDetail(session, 'survey-1')).rejects.toEqual(
|
||
new PerformanceSurveyApiError('调研不存在或无权限', 404),
|
||
)
|
||
expect(prisma.performanceSurvey.count).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('我发起的历史多目标调研结果只返回当前账户对应目标', async () => {
|
||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
||
...baseUser,
|
||
role: 'ADMIN',
|
||
})
|
||
const selfTarget = {
|
||
id: 'target-self',
|
||
employeeProfileId: 'employee-1',
|
||
targetType: 'employee',
|
||
targetName: '测试人员',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
workItems: [],
|
||
_count: { assignments: 3, responses: 2 },
|
||
}
|
||
const otherTarget = {
|
||
...selfTarget,
|
||
id: 'target-other',
|
||
employeeProfileId: 'employee-other',
|
||
targetName: '其他员工',
|
||
}
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
status: 'closed',
|
||
targets: [selfTarget, otherTarget],
|
||
}))
|
||
|
||
const result = await getPerformanceSurveyResults(session, 'survey-1')
|
||
|
||
expect(result.targets.map((target) => target.id)).toEqual(['target-self'])
|
||
expect(result.survey.counts.targets).toBe(1)
|
||
expect(result.survey.counts.assignments).toBe(3)
|
||
expect(result.survey.counts.responses).toBe(2)
|
||
})
|
||
|
||
test('我发起的历史多目标调研列表只返回当前账户目标计数', async () => {
|
||
;(prisma.performanceSurvey.findMany as jest.Mock).mockResolvedValue([{
|
||
...buildSurvey({ status: 'closed' }),
|
||
targets: [{
|
||
id: 'target-self',
|
||
_count: { assignments: 3, responses: 2 },
|
||
}],
|
||
_count: { targets: 6, assignments: 12, responses: 12 },
|
||
}])
|
||
|
||
const result = await listPerformanceSurveys(session)
|
||
|
||
expect(result.created).toHaveLength(1)
|
||
expect(result.created[0].personalTargetId).toBe('target-self')
|
||
expect(result.created[0].counts).toEqual({
|
||
targets: 1,
|
||
assignments: 3,
|
||
responses: 2,
|
||
})
|
||
})
|
||
|
||
test('M级可撤销权限范围内调研并保留撤销审计信息', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
status: 'open',
|
||
creatorUserId: 'user-other',
|
||
}))
|
||
;(prisma.performanceSurvey.count as jest.Mock).mockResolvedValue(1)
|
||
|
||
await cancelPerformanceSurvey(session, 'survey-1', { reason: '评分录入错误' })
|
||
|
||
expect(transactionClient.performanceSurvey.update).toHaveBeenCalledWith({
|
||
where: { id: 'survey-1' },
|
||
data: expect.objectContaining({
|
||
status: 'cancelled',
|
||
cancelledByUserId: 'user-1',
|
||
cancelledByName: '测试人员',
|
||
cancelReason: '评分录入错误',
|
||
cancelledAt: expect.any(Date),
|
||
}),
|
||
})
|
||
expect(transactionClient.$queryRawUnsafe).toHaveBeenCalledWith(
|
||
expect.stringContaining('FOR UPDATE'),
|
||
'survey-1',
|
||
)
|
||
expect(transactionClient.performanceSurveyNotification.updateMany).toHaveBeenCalledWith({
|
||
where: {
|
||
surveyId: 'survey-1',
|
||
status: { in: ['pending', 'retrying', 'processing'] },
|
||
},
|
||
data: expect.objectContaining({
|
||
status: 'skipped',
|
||
nextRetryAt: null,
|
||
completedAt: expect.any(Date),
|
||
lastError: '调研已撤销:评分录入错误',
|
||
}),
|
||
})
|
||
})
|
||
|
||
test('普通人员即使是发起人也不能撤销调研', async () => {
|
||
await expect(
|
||
cancelPerformanceSurvey(session, 'survey-1', { reason: '误操作' }),
|
||
).rejects.toEqual(new PerformanceSurveyApiError('无权限管理考核调研', 403))
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('固定月度模板拒绝自定义标题覆盖', async () => {
|
||
await updatePerformanceSurvey(session, 'survey-1', { title: '自定义标题' })
|
||
|
||
expect(transactionClient.performanceSurvey.update).toHaveBeenCalledWith({
|
||
where: { id: 'survey-1' },
|
||
data: { title: '2026年6月 · 课程分享' },
|
||
})
|
||
expect(transactionClient.$queryRawUnsafe).toHaveBeenCalledWith(
|
||
expect.stringContaining('FOR UPDATE'),
|
||
'survey-1',
|
||
)
|
||
expect(transactionClient.$queryRawUnsafe.mock.invocationCallOrder[0]).toBeLessThan(
|
||
transactionClient.performanceSurvey.update.mock.invocationCallOrder[0],
|
||
)
|
||
})
|
||
|
||
test('修改月份时先加事务锁、排除当前调研,再同步月份和固定标题', async () => {
|
||
await updatePerformanceSurvey(session, 'survey-1', { assessmentMonth: '2026-07' })
|
||
|
||
expect(transactionClient.$queryRawUnsafe).toHaveBeenCalledTimes(3)
|
||
expect(transactionClient.$queryRawUnsafe.mock.calls.slice(1).map((call) => call[1])).toEqual([
|
||
'performance-survey-monthly:course_share:2026-06:employee-1',
|
||
'performance-survey-monthly:course_share:2026-07:employee-1',
|
||
])
|
||
expect(transactionClient.$queryRawUnsafe.mock.calls[0][0]).toContain('FOR UPDATE')
|
||
expect(transactionClient.$queryRawUnsafe.mock.calls[1][0]).toContain('::text AS locked')
|
||
expect(transactionClient.$queryRawUnsafe.mock.invocationCallOrder[0]).toBeLessThan(
|
||
transactionClient.$queryRawUnsafe.mock.invocationCallOrder[1],
|
||
)
|
||
|
||
const duplicateWhere = transactionClient.performanceSurveyTarget.findMany.mock.calls[0][0].where
|
||
expect(duplicateWhere.survey.AND).toContainEqual({ id: { not: 'survey-1' } })
|
||
expect(transactionClient.performanceSurvey.update).toHaveBeenCalledWith({
|
||
where: { id: 'survey-1' },
|
||
data: {
|
||
assessmentMonth: '2026-07',
|
||
title: '2026年7月 · 课程分享',
|
||
},
|
||
})
|
||
})
|
||
|
||
test('创建月度调研时在同一事务内先加锁和查重,再写入调研', async () => {
|
||
const evaluator = {
|
||
...baseEmployee,
|
||
id: 'employee-2',
|
||
displayName: '评价人员',
|
||
dingtalkUserId: null,
|
||
dingEmail: null,
|
||
}
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([baseEmployee])
|
||
.mockResolvedValueOnce([evaluator])
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-1'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
openNow: false,
|
||
})
|
||
|
||
expect(transactionClient.$queryRawUnsafe).toHaveBeenCalledWith(
|
||
expect.stringContaining('::text AS locked'),
|
||
'performance-survey-monthly:course_share:2026-07:employee-1',
|
||
)
|
||
expect(transactionClient.performanceSurveyTarget.findMany).toHaveBeenCalledTimes(1)
|
||
expect(transactionClient.performanceSurvey.create).toHaveBeenCalledTimes(1)
|
||
expect(transactionClient.$queryRawUnsafe.mock.invocationCallOrder[0]).toBeLessThan(
|
||
transactionClient.performanceSurveyTarget.findMany.mock.invocationCallOrder[0],
|
||
)
|
||
expect(transactionClient.performanceSurveyTarget.findMany.mock.invocationCallOrder[0]).toBeLessThan(
|
||
transactionClient.performanceSurvey.create.mock.invocationCallOrder[0],
|
||
)
|
||
expect(getPerformanceSurveyPositionGroupByCode).not.toHaveBeenCalled()
|
||
expect(transactionClient.performanceSurvey.create).toHaveBeenCalledWith({
|
||
data: expect.objectContaining({
|
||
positionGroupCode: 'brand_designer',
|
||
positionGroupId: 'position-group-brand',
|
||
}),
|
||
})
|
||
})
|
||
|
||
test('客户端不能用旧岗位参数覆盖被考核人的自动岗位', async () => {
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'ai_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-1'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
openNow: false,
|
||
})).rejects.toEqual(
|
||
new PerformanceSurveyApiError('考核岗位已根据被考核人的人员分配自动关联,请刷新页面后重试', 409),
|
||
)
|
||
|
||
expect(getPerformanceSurveyPositionGroupByCode).not.toHaveBeenCalled()
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('发起人不能手动选择自己作为评分人员', async () => {
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-1'],
|
||
evaluatorEmployeeIds: ['employee-1'],
|
||
openNow: false,
|
||
})).rejects.toEqual(
|
||
new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409),
|
||
)
|
||
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('评分人员搜索和部门批量加载都排除当前发起人', async () => {
|
||
await searchPerformanceSurveyEmployees(session, new URLSearchParams({
|
||
purpose: 'evaluator',
|
||
department: '集团A/品牌中心/策划部',
|
||
limit: '1000',
|
||
}))
|
||
|
||
const query = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0]
|
||
expect(query.where.AND).toContainEqual({ id: { not: 'employee-1' } })
|
||
})
|
||
|
||
test('人员搜索会命中兼任部门并返回全部部门及钉钉身份', async () => {
|
||
const employee = {
|
||
...baseEmployee,
|
||
displayName: '清云',
|
||
positionTitle: '营销策划主管',
|
||
positionLevel: 'M1',
|
||
levelType: 'M',
|
||
isLeadership: true,
|
||
isDingtalkAdmin: true,
|
||
isDingtalkBoss: false,
|
||
isDingtalkSenior: false,
|
||
departmentMemberships: [
|
||
{
|
||
id: 'membership-primary',
|
||
sourceDeptId: '994456259',
|
||
departmentName: '营销策划中心',
|
||
departmentPath: '集团/龙晨品牌中心/营销策划中心',
|
||
isPrimary: true,
|
||
syncedAt: new Date('2026-07-27T00:00:00.000Z'),
|
||
},
|
||
{
|
||
id: 'membership-secondary',
|
||
sourceDeptId: '1085358755',
|
||
departmentName: '品牌创意B部',
|
||
departmentPath: '集团/品牌中心/品牌创意B部',
|
||
isPrimary: false,
|
||
syncedAt: new Date('2026-07-27T00:00:00.000Z'),
|
||
},
|
||
],
|
||
}
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([employee])
|
||
|
||
const result = await searchPerformanceSurveyEmployees(session, new URLSearchParams({
|
||
search: '品牌创意B部',
|
||
purpose: 'evaluator',
|
||
}))
|
||
|
||
const query = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0]
|
||
expect(query.where.AND).toContainEqual({
|
||
OR: expect.arrayContaining([
|
||
{
|
||
departmentMemberships: {
|
||
some: {
|
||
OR: [
|
||
{ departmentName: { contains: '品牌创意B部', mode: 'insensitive' } },
|
||
{ departmentPath: { contains: '品牌创意B部', mode: 'insensitive' } },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
]),
|
||
})
|
||
expect(result).toEqual([
|
||
expect.objectContaining({
|
||
displayName: '清云',
|
||
positionLevel: 'M1',
|
||
isDingtalkAdmin: true,
|
||
departmentMemberships: [
|
||
expect.objectContaining({ departmentName: '营销策划中心', isPrimary: true }),
|
||
expect.objectContaining({ departmentName: '品牌创意B部', isPrimary: false }),
|
||
],
|
||
}),
|
||
])
|
||
})
|
||
|
||
test('创建接口拒绝直接传入组织范围外的被考核人 ID', async () => {
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([baseEmployee])
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-outside'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
openNow: false,
|
||
})).rejects.toEqual(
|
||
new PerformanceSurveyApiError('部分被考核人不在当前组织权限范围内', 403),
|
||
)
|
||
|
||
const targetQuery = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0]
|
||
expect(targetQuery.where.AND).toContainEqual({
|
||
OR: [
|
||
{ id: 'employee-1' },
|
||
{
|
||
OR: [
|
||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||
{
|
||
departmentMemberships: {
|
||
some: {
|
||
OR: [
|
||
{ departmentPath: '集团A/品牌中心/策划部' },
|
||
{ departmentPath: { startsWith: '集团A/品牌中心/策划部/' } },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
})
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('缺少完整组织路径时不使用重名部门叶子回退扩大被考核人范围', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentPath: null,
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([{
|
||
...baseEmployee,
|
||
departmentPath: null,
|
||
}])
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-outside'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
openNow: false,
|
||
})).rejects.toThrow('部分被考核人不在当前组织权限范围内')
|
||
|
||
expect(prisma.performanceSurveyEmployeeProfile.findMany).toHaveBeenCalled()
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('创建资格严格按钉钉完整组织路径中的品牌中心或摄影中心判断', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentName: '品牌中心',
|
||
departmentPath: '集团A/其他中心/品牌中心同名小组',
|
||
})
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-1'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
})).rejects.toEqual(expect.objectContaining({ status: 403 }))
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('考核档案M级即使不在指定组织也可发起,canCreate 与创建接口保持一致', async () => {
|
||
const adminEmployee = {
|
||
...baseEmployee,
|
||
departmentName: '视觉一部',
|
||
departmentPath: '集团A/其他中心/视觉一部',
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
}
|
||
const evaluator = {
|
||
...baseEmployee,
|
||
id: 'employee-2',
|
||
displayName: '评价人员',
|
||
dingtalkUserId: null,
|
||
}
|
||
const targetEmployee = {
|
||
...baseEmployee,
|
||
id: 'employee-target',
|
||
displayName: '被考核人员',
|
||
dingtalkUserId: null,
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(adminEmployee)
|
||
|
||
await expect(getPerformanceSurveyMe(session)).resolves.toEqual(expect.objectContaining({
|
||
permissions: expect.objectContaining({ canCreate: true }),
|
||
}))
|
||
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([targetEmployee])
|
||
.mockResolvedValueOnce([evaluator])
|
||
|
||
const creation = createPerformanceSurvey(session, {
|
||
templateCode: 'course_share',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-target'],
|
||
evaluatorEmployeeIds: ['employee-2'],
|
||
})
|
||
await expect(creation).rejects.not.toEqual(expect.objectContaining({ status: 403 }))
|
||
expect(prisma.performanceSurveyEmployeeProfile.findMany).toHaveBeenCalled()
|
||
})
|
||
|
||
test('M级人员在考核调研内按系统管理员授权', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
|
||
await expect(getPerformanceSurveyMe(session)).resolves.toEqual(expect.objectContaining({
|
||
permissions: expect.objectContaining({
|
||
isSystemAdmin: true,
|
||
isDepartmentAdmin: true,
|
||
isLeadership: true,
|
||
canCreate: true,
|
||
}),
|
||
}))
|
||
})
|
||
|
||
test.each(['商品企划部', '空间视觉部'])(
|
||
'%s不再由部门名称硬编码禁止发起,资格交给岗位匹配规则',
|
||
async (departmentName) => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentName,
|
||
departmentPath: `集团A/品牌中心/${departmentName}`,
|
||
})
|
||
|
||
await expect(getPerformanceSurveyMe(session)).resolves.toEqual(expect.objectContaining({
|
||
permissions: expect.objectContaining({ canCreate: true }),
|
||
}))
|
||
},
|
||
)
|
||
|
||
test('考核档案M级属于品牌中心未开放部门时仍保留全局发起能力', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentName: '商品企划部',
|
||
departmentPath: '集团A/品牌中心/商品企划部',
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
isLeadership: true,
|
||
})
|
||
|
||
await expect(getPerformanceSurveyMe(session)).resolves.toEqual(expect.objectContaining({
|
||
permissions: expect.objectContaining({ canCreate: true }),
|
||
}))
|
||
})
|
||
|
||
test('综合支持部普通人员可发起考核调研', async () => {
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({
|
||
...baseEmployee,
|
||
departmentName: '综合支持部',
|
||
departmentPath: null,
|
||
})
|
||
|
||
await expect(getPerformanceSurveyMe(session)).resolves.toEqual(expect.objectContaining({
|
||
permissions: expect.objectContaining({ canCreate: true }),
|
||
}))
|
||
})
|
||
|
||
test('自动匹配的非自评人员如果是发起人则拒绝创建', async () => {
|
||
const creator = {
|
||
...baseEmployee,
|
||
dingtalkUserId: baseUser.dingtalkUserId,
|
||
departmentName: '品牌创意A部',
|
||
departmentPath: '集团A/品牌中心/品牌创意A部',
|
||
managerDingtalkUserId: 'ding-other-manager',
|
||
}
|
||
const target = {
|
||
...creator,
|
||
id: 'employee-target',
|
||
displayName: '其他被考核人',
|
||
dingtalkUserId: 'ding-target',
|
||
managerDingtalkUserId: creator.dingtalkUserId,
|
||
}
|
||
const creatorManager = {
|
||
...creator,
|
||
id: 'employee-creator-manager',
|
||
displayName: '发起人的主管',
|
||
dingtalkUserId: 'ding-other-manager',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset()
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(creator)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([creator, target])
|
||
.mockResolvedValueOnce([creator])
|
||
;(prisma.user.findFirst as jest.Mock).mockResolvedValue({
|
||
id: baseUser.id,
|
||
dingtalkUserId: baseUser.dingtalkUserId,
|
||
})
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [target.id],
|
||
openNow: false,
|
||
})).rejects.toEqual(
|
||
new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409),
|
||
)
|
||
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test.each([
|
||
{
|
||
label: '品牌创意A部P级人员',
|
||
employee: {
|
||
departmentName: '品牌创意A部',
|
||
departmentPath: '集团A/品牌中心/品牌创意A部',
|
||
dingtalkUserId: 'ding-p-employee',
|
||
levelType: 'P',
|
||
positionLevel: 'P6',
|
||
managerDingtalkUserId: 'ding-direct-manager',
|
||
},
|
||
evaluator: {
|
||
id: 'employee-direct-manager',
|
||
displayName: '直属主管',
|
||
dingtalkUserId: 'ding-direct-manager',
|
||
},
|
||
},
|
||
{
|
||
label: '综合支持部M级人员',
|
||
employee: {
|
||
departmentName: '综合支持部',
|
||
departmentPath: '集团A/品牌中心/综合支持部',
|
||
dingtalkUserId: 'ding-m-employee',
|
||
levelType: 'M',
|
||
positionLevel: 'M2',
|
||
managerDingtalkUserId: 'ding-other-manager',
|
||
},
|
||
evaluator: {
|
||
id: 'employee-other-manager',
|
||
displayName: '直属主管',
|
||
dingtalkUserId: 'ding-other-manager',
|
||
},
|
||
},
|
||
{
|
||
label: '观世(档案为P级)',
|
||
employee: {
|
||
departmentName: '摄影中心',
|
||
departmentPath: '集团A/摄影中心',
|
||
dingtalkUserId: '17642061233583361',
|
||
levelType: 'P',
|
||
positionLevel: 'P7',
|
||
managerDingtalkUserId: 'ding-other-manager',
|
||
},
|
||
evaluator: {
|
||
id: 'employee-other-manager',
|
||
displayName: '直属主管',
|
||
dingtalkUserId: 'ding-other-manager',
|
||
},
|
||
},
|
||
])('$label发起工作配合度时按模板读取人员档案中的直属主管', async ({ employee, evaluator }) => {
|
||
const target = {
|
||
...baseEmployee,
|
||
...employee,
|
||
id: 'employee-target',
|
||
displayName: '被考核人',
|
||
}
|
||
const evaluatorProfile = {
|
||
...baseEmployee,
|
||
...evaluator,
|
||
departmentName: employee.departmentName,
|
||
departmentPath: employee.departmentPath,
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([target])
|
||
.mockResolvedValueOnce([evaluatorProfile])
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([{
|
||
id: 'user-evaluator',
|
||
dingtalkUserId: evaluator.dingtalkUserId,
|
||
}])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'work_cooperation',
|
||
creatorEmployeeId: target.id,
|
||
targets: [{
|
||
id: 'target-work-cooperation',
|
||
employeeProfileId: target.id,
|
||
targetType: 'employee',
|
||
targetName: target.displayName,
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [target.id],
|
||
openNow: false,
|
||
})
|
||
|
||
const evaluatorQuery = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[1][0]
|
||
expect(evaluatorQuery.where.AND[1].OR).toContainEqual({ dingtalkUserId: evaluator.dingtalkUserId })
|
||
const assignmentRows = transactionClient.performanceSurveyAssignment.createMany.mock.calls[0][0].data
|
||
expect(assignmentRows).toEqual([
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: evaluator.id,
|
||
evaluatorDingtalkUserId: evaluator.dingtalkUserId,
|
||
evaluatorName: evaluator.displayName,
|
||
workflowStageKey: 'manager_review',
|
||
reviewerSource: 'direct_manager',
|
||
}),
|
||
])
|
||
})
|
||
|
||
test.each([
|
||
['指定人员', { evaluatorEmployeeIds: ['employee-manual-evaluator'] }],
|
||
['指定部门', { evaluatorDepartmentNames: ['集团A/品牌中心/评审组'] }],
|
||
])('模板配置为发起时选择人员后支持%s评分人', async (_label, evaluatorInput) => {
|
||
const target = {
|
||
...baseEmployee,
|
||
id: 'employee-target',
|
||
displayName: '被考核人',
|
||
departmentName: '品牌创意A部',
|
||
departmentPath: '集团A/品牌中心/品牌创意A部',
|
||
dingtalkUserId: 'ding-target',
|
||
levelType: 'P',
|
||
positionLevel: 'P6',
|
||
managerDingtalkUserId: null,
|
||
}
|
||
const manualEvaluator = {
|
||
...baseEmployee,
|
||
id: 'employee-manual-evaluator',
|
||
displayName: '手动评分人',
|
||
dingtalkUserId: 'ding-manual-evaluator',
|
||
}
|
||
const manualGroup = buildConfiguredPositionGroup()
|
||
const workTemplate = manualGroup.templates.find((template) => template.code === 'work_cooperation')!
|
||
workTemplate.workflow = {
|
||
stages: [{
|
||
key: 'manual_review',
|
||
label: '人工评价',
|
||
order: 0,
|
||
reviewers: [{ source: 'manual_selection', label: '发起时选择人员' }],
|
||
}],
|
||
scoring: { mode: 'average_reviewers', stageKeys: ['manual_review'] },
|
||
}
|
||
;(getPerformanceSurveyPositionGroupByCode as jest.Mock).mockResolvedValue(manualGroup)
|
||
;(getPerformanceSurveyPositionGroupForEmployee as jest.Mock).mockResolvedValue({
|
||
status: 'matched',
|
||
group: manualGroup,
|
||
matches: [manualGroup],
|
||
})
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([target])
|
||
.mockResolvedValueOnce([manualEvaluator])
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([{
|
||
id: 'user-manual-evaluator',
|
||
dingtalkUserId: manualEvaluator.dingtalkUserId,
|
||
email: null,
|
||
}])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'work_cooperation',
|
||
creatorEmployeeId: target.id,
|
||
targets: [{
|
||
id: 'target-work-cooperation',
|
||
employeeProfileId: target.id,
|
||
targetType: 'employee',
|
||
targetName: target.displayName,
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [target.id],
|
||
...evaluatorInput,
|
||
})
|
||
|
||
const assignmentRows = transactionClient.performanceSurveyAssignment.createMany.mock.calls[0][0].data
|
||
expect(assignmentRows).toEqual([
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: manualEvaluator.id,
|
||
evaluatorDingtalkUserId: manualEvaluator.dingtalkUserId,
|
||
evaluatorName: manualEvaluator.displayName,
|
||
}),
|
||
])
|
||
})
|
||
|
||
test('直属主管模板遇到人员档案缺少主管唯一值时明确拒绝创建', async () => {
|
||
const target = {
|
||
...baseEmployee,
|
||
id: 'employee-target',
|
||
displayName: '缺主管人员',
|
||
departmentName: '品牌创意B部',
|
||
departmentPath: '集团A/品牌中心/品牌创意B部',
|
||
dingtalkUserId: 'ding-p-no-manager',
|
||
levelType: 'P',
|
||
positionLevel: 'P5',
|
||
managerDingtalkUserId: null,
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([target])
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [target.id],
|
||
})).rejects.toEqual(new PerformanceSurveyApiError(
|
||
'无法确定缺主管人员的直属主管,请先校准人员档案',
|
||
409,
|
||
))
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('直属主管模板不按同部门 M 级人员猜测主管', async () => {
|
||
const target = {
|
||
...baseEmployee,
|
||
id: 'employee-yijiang',
|
||
displayName: '忆江',
|
||
departmentName: '综合支持部',
|
||
departmentPath: '集团A/品牌中心/综合支持部',
|
||
dingtalkUserId: 'ding-yijiang',
|
||
levelType: 'P',
|
||
positionLevel: 'P4',
|
||
managerDingtalkUserId: null,
|
||
}
|
||
const uniqueManager = {
|
||
...baseEmployee,
|
||
id: 'employee-wuxie',
|
||
displayName: '无邪',
|
||
departmentName: target.departmentName,
|
||
departmentPath: target.departmentPath,
|
||
dingtalkUserId: 'ding-wuxie',
|
||
levelType: 'M',
|
||
positionLevel: 'M1',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([target])
|
||
.mockResolvedValueOnce([uniqueManager])
|
||
.mockResolvedValueOnce([uniqueManager])
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([{
|
||
id: 'user-wuxie',
|
||
dingtalkUserId: uniqueManager.dingtalkUserId,
|
||
}])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'work_cooperation',
|
||
creatorEmployeeId: target.id,
|
||
targets: [{
|
||
id: 'target-work-cooperation',
|
||
employeeProfileId: target.id,
|
||
targetType: 'employee',
|
||
targetName: target.displayName,
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [target.id],
|
||
openNow: false,
|
||
})).rejects.toEqual(new PerformanceSurveyApiError(
|
||
'无法确定忆江的直属主管,请先校准人员档案',
|
||
409,
|
||
))
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('人员姓名不再触发固定评分人规则,缺主管仍按档案错误处理', async () => {
|
||
const yuanzhe = {
|
||
...baseEmployee,
|
||
id: 'employee-yuanzhe',
|
||
displayName: '元泽',
|
||
flowerName: '元泽',
|
||
departmentName: '品牌创意A部',
|
||
departmentPath: '集团A/品牌中心/品牌创意A部',
|
||
dingtalkUserId: '27102756692071120919',
|
||
levelType: 'M',
|
||
positionLevel: 'M3',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(yuanzhe)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'work_cooperation',
|
||
name: '工作配合度满意度调研',
|
||
shortName: '工作配合度',
|
||
surveyType: 'person_period',
|
||
weight: 0.2,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([yuanzhe])
|
||
|
||
await expect(createPerformanceSurvey(session, {
|
||
templateCode: 'work_cooperation',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: [yuanzhe.id],
|
||
})).rejects.toEqual(new PerformanceSurveyApiError(
|
||
'无法确定元泽的直属主管,请先校准人员档案',
|
||
409,
|
||
))
|
||
expect(prisma.$transaction).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('同一人员在不同阶段承担不同角色时保留独立任务并按阶段开放', async () => {
|
||
const yuanzhe = {
|
||
...baseEmployee,
|
||
id: 'employee-yuanzhe',
|
||
displayName: '元泽',
|
||
flowerName: '元泽',
|
||
realName: '元泽',
|
||
departmentName: '综合支持部',
|
||
departmentPath: '集团A/品牌中心/综合支持部',
|
||
dingtalkUserId: 'ding-yuanzhe',
|
||
managerDingtalkUserId: 'ding-manager',
|
||
}
|
||
const manager = {
|
||
...baseEmployee,
|
||
id: 'employee-manager',
|
||
displayName: '直属主管',
|
||
dingtalkUserId: 'ding-manager',
|
||
levelType: 'M',
|
||
positionLevel: 'M2',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(yuanzhe)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'visual_brand_tone',
|
||
name: '品牌调性视觉评分',
|
||
shortName: '视觉评分',
|
||
surveyType: 'visual_case',
|
||
weight: 1,
|
||
questionMaxScore: 8,
|
||
maxTotal: 120,
|
||
questions: [
|
||
{ id: 'brandUnderstanding', title: '品牌理解', maxScore: 8 },
|
||
{ id: 'visualStyle', title: '视觉风格', maxScore: 8 },
|
||
{ id: 'copyExpression', title: '文案表达', maxScore: 8 },
|
||
{ id: 'visualCopyFit', title: '图文协同', maxScore: 8 },
|
||
{ id: 'creativeHighlight', title: '创意亮点', maxScore: 8 },
|
||
],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([yuanzhe])
|
||
.mockResolvedValueOnce([manager])
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockReset()
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(yuanzhe)
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([
|
||
{ id: 'user-1', dingtalkUserId: 'ding-yuanzhe' },
|
||
{ id: 'user-manager', dingtalkUserId: 'ding-manager' },
|
||
])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'visual_brand_tone',
|
||
targets: [{
|
||
id: 'target-yuanzhe',
|
||
employeeProfileId: 'employee-yuanzhe',
|
||
targetType: 'employee',
|
||
targetName: '元泽',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'visual_brand_tone',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
targetEmployeeIds: ['employee-yuanzhe'],
|
||
openNow: true,
|
||
})
|
||
|
||
const targetQuery = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0]
|
||
expect(targetQuery.where.AND).toContainEqual({ id: { in: ['employee-yuanzhe'] } })
|
||
const assignmentRows = transactionClient.performanceSurveyAssignment.createMany.mock.calls[0][0].data
|
||
expect(transactionClient.performanceSurvey.create.mock.calls[0][0].data.templateVersionId).toBe('template-version-4')
|
||
expect(assignmentRows).toHaveLength(3)
|
||
expect(assignmentRows).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-yuanzhe',
|
||
workflowStageKey: 'self_review',
|
||
reviewerSource: 'self',
|
||
releasedAt: expect.any(Date),
|
||
}),
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-manager',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'direct_manager',
|
||
releasedAt: null,
|
||
}),
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-yuanzhe',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'fixed_employee',
|
||
releasedAt: null,
|
||
}),
|
||
]))
|
||
})
|
||
|
||
test.each([
|
||
['无邪', '09061817188066'],
|
||
['伯言', '09061617282237'],
|
||
['清云', '09053022115891'],
|
||
])('%s的视觉评分按人员档案取直属主管,并按模板取固定终审人', async (displayName, dingtalkUserId) => {
|
||
const target = {
|
||
...baseEmployee,
|
||
id: `employee-${displayName}`,
|
||
displayName,
|
||
flowerName: displayName,
|
||
dingtalkUserId,
|
||
managerDingtalkUserId: 'ding-declared-manager',
|
||
}
|
||
const manager = {
|
||
...baseEmployee,
|
||
id: 'employee-declared-manager',
|
||
displayName: '档案直属主管',
|
||
dingtalkUserId: 'ding-declared-manager',
|
||
}
|
||
const yuanzhe = {
|
||
...baseEmployee,
|
||
id: 'employee-yuanzhe',
|
||
displayName: '元泽',
|
||
flowerName: '元泽',
|
||
dingtalkUserId: '27102756692071120919',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'visual_brand_tone',
|
||
name: '品牌调性视觉评分',
|
||
shortName: '视觉评分',
|
||
surveyType: 'visual_case',
|
||
weight: 1,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([target])
|
||
.mockResolvedValueOnce([manager])
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(yuanzhe)
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([
|
||
{ id: 'user-target', dingtalkUserId },
|
||
{ id: 'user-yuanzhe', dingtalkUserId: '27102756692071120919' },
|
||
])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'visual_brand_tone',
|
||
targets: [{
|
||
id: 'target-person',
|
||
employeeProfileId: target.id,
|
||
targetType: 'employee',
|
||
targetName: displayName,
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'visual_brand_tone',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
openNow: false,
|
||
})
|
||
|
||
expect(prisma.performanceSurveyEmployeeProfile.findFirst).toHaveBeenCalledWith({
|
||
where: {
|
||
AND: [
|
||
{ id: { not: '__hidden__' } },
|
||
{ id: 'employee-yuanzhe' },
|
||
],
|
||
},
|
||
})
|
||
const assignmentRows = transactionClient.performanceSurveyAssignment.createMany.mock.calls[0][0].data
|
||
expect(assignmentRows).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-yuanzhe',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'fixed_employee',
|
||
}),
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-declared-manager',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'direct_manager',
|
||
}),
|
||
]))
|
||
})
|
||
|
||
test.each([
|
||
'集团A/摄影中心/摄影一部',
|
||
'摄影中心/摄影一部',
|
||
])('部门路径不再覆盖视觉评分的档案直属主管:%s', async (departmentPath) => {
|
||
const target = {
|
||
...baseEmployee,
|
||
id: 'employee-photographer',
|
||
displayName: '摄影师',
|
||
dingtalkUserId: 'ding-photographer',
|
||
departmentName: '摄影一部',
|
||
departmentPath,
|
||
managerDingtalkUserId: '17642061233583361',
|
||
}
|
||
const yuanzhe = {
|
||
...baseEmployee,
|
||
id: 'employee-yuanzhe',
|
||
displayName: '元泽',
|
||
dingtalkUserId: '27102756692071120919',
|
||
}
|
||
const guanshi = {
|
||
...baseEmployee,
|
||
id: 'employee-guanshi',
|
||
displayName: '观世',
|
||
dingtalkUserId: '17642061233583361',
|
||
}
|
||
;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(target)
|
||
;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({
|
||
code: 'visual_brand_tone',
|
||
name: '品牌调性视觉评分',
|
||
shortName: '视觉评分',
|
||
surveyType: 'visual_case',
|
||
weight: 1,
|
||
questions: [],
|
||
scoreBands: [],
|
||
})
|
||
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock)
|
||
.mockResolvedValueOnce([target])
|
||
.mockResolvedValueOnce([guanshi])
|
||
;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(yuanzhe)
|
||
;(prisma.user.findMany as jest.Mock).mockResolvedValue([
|
||
{ id: 'user-target', dingtalkUserId: 'ding-photographer' },
|
||
{ id: 'user-yuanzhe', dingtalkUserId: '27102756692071120919' },
|
||
{ id: 'user-guanshi', dingtalkUserId: '17642061233583361' },
|
||
])
|
||
;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey({
|
||
templateCode: 'visual_brand_tone',
|
||
targets: [{
|
||
id: 'target-photographer',
|
||
employeeProfileId: target.id,
|
||
targetType: 'employee',
|
||
targetName: '摄影师',
|
||
targetSnapshot: {},
|
||
responses: [],
|
||
assignments: [],
|
||
workItems: [],
|
||
}],
|
||
}))
|
||
|
||
await createPerformanceSurvey(session, {
|
||
templateCode: 'visual_brand_tone',
|
||
positionGroupCode: 'brand_designer',
|
||
assessmentMonth: '2026-07',
|
||
openNow: false,
|
||
})
|
||
|
||
const managerQuery = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[1][0]
|
||
expect(managerQuery.where.AND[1].OR).toContainEqual({ dingtalkUserId: '17642061233583361' })
|
||
const assignmentRows = transactionClient.performanceSurveyAssignment.createMany.mock.calls[0][0].data
|
||
expect(assignmentRows).toEqual(expect.arrayContaining([
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-guanshi',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'direct_manager',
|
||
}),
|
||
expect.objectContaining({
|
||
evaluatorEmployeeId: 'employee-yuanzhe',
|
||
workflowStageKey: 'final_review',
|
||
reviewerSource: 'fixed_employee',
|
||
}),
|
||
]))
|
||
})
|
||
|
||
test('修改月份命中另一条同人同月同模板调研时拒绝更新', async () => {
|
||
transactionClient.performanceSurveyTarget.findMany.mockResolvedValue([{
|
||
targetName: '测试人员',
|
||
employeeProfile: null,
|
||
survey: { id: 'survey-2' },
|
||
}])
|
||
|
||
await expect(
|
||
updatePerformanceSurvey(session, 'survey-1', { assessmentMonth: '2026-07' }),
|
||
).rejects.toThrow('2026-07 已存在该模板考核:测试人员')
|
||
|
||
expect(transactionClient.performanceSurvey.update).not.toHaveBeenCalled()
|
||
})
|
||
})
|