gongxiangp2/lib/__tests__/ding-workflow-team-workload.test.ts

157 lines
5.6 KiB
TypeScript

import { buildTeamWorkloadInsights } from '@/lib/ding-workflow-sync'
jest.mock('mssql', () => ({
__esModule: true,
default: {},
}))
jest.mock('@/lib/prisma', () => ({ prisma: {} }))
jest.mock('@/lib/brand-center-access', () => ({ resolveBrandCenterAccessForUser: jest.fn() }))
function rosterRow(overrides: Partial<{
id: string
dingtalkUserId: string | null
displayName: string
flowerName: string | null
realName: string | null
departmentName: string | null
departmentPath: string | null
employmentState: string | null
departmentMemberships: Array<{ departmentName: string | null; departmentPath: string | null; isPrimary: boolean }>
}> = {}) {
return {
id: 'person-1',
dingtalkUserId: 'ding-1',
displayName: '一号员工',
flowerName: null,
realName: null,
departmentName: '设计部',
departmentPath: '品牌中心/设计部',
employmentState: 'ACTIVE',
departmentMemberships: [],
...overrides,
}
}
function snapshotRow(id: string, assignees: string[], title = id) {
return {
processInstanceId: id,
businessId: `business-${id}`,
title,
processCode: 'PROC-ART',
projectType: '美术设计流程',
activeTasks: assignees.map((assigneeUserId, index) => ({
taskId: `${id}-task-${index}`,
activityId: `activity-${index}`,
nodeName: `节点${index + 1}`,
assigneeUserId,
assigneeName: assigneeUserId,
assigneeDepartment: '品牌中心/设计部',
isBrandCenter: true,
isLeadership: false,
isActive: true,
reviewPassed: false,
result: null,
createdAt: null,
finishedAt: null,
})),
}
}
describe('buildTeamWorkloadInsights', () => {
it('排除指定人员、把空闲人员计入平均值,并按流程去重', () => {
const insights = buildTeamWorkloadInsights(
[
rosterRow({ id: 'busy', dingtalkUserId: 'busy', displayName: '忙碌员工' }),
rosterRow({ id: 'idle', dingtalkUserId: 'idle', displayName: '空闲员工', departmentName: '内容部', departmentPath: '品牌中心/内容部' }),
rosterRow({ id: 'excluded', dingtalkUserId: 'excluded', displayName: '元泽' }),
rosterRow({ id: 'resigned', dingtalkUserId: 'resigned', displayName: '离职员工', employmentState: 'RESIGNED' }),
],
[
snapshotRow('project-1', ['busy', 'busy']),
snapshotRow('project-2', ['busy']),
snapshotRow('project-3', ['busy']),
snapshotRow('project-excluded', ['excluded']),
],
{ lastSyncedAt: '2026-08-31T02:00:00.000Z', syncStatus: 'ready' },
)
expect(insights.summary).toMatchObject({
averageLoadPercent: 75,
personnelCount: 2,
busyPersonnel: 1,
idlePersonnel: 1,
activeProjectAssignments: 3,
sourceOngoingProjects: 4,
})
expect(insights.departments).toEqual(expect.arrayContaining([
expect.objectContaining({ name: '设计部', personnelCount: 1, averageLoadPercent: 150 }),
expect.objectContaining({ name: '内容部', personnelCount: 1, averageLoadPercent: 0 }),
]))
const busy = insights.departments.find((department) => department.name === '设计部')!.people[0]
expect(busy.activeProjectCount).toBe(3)
expect(busy.loadPercent).toBe(150)
expect(busy.status).toBe('overloaded')
expect(busy.projects).toHaveLength(3)
})
it('只统计当前活跃节点,并以当前花名册组织归属为准', () => {
const insights = buildTeamWorkloadInsights(
[rosterRow({
departmentName: '品牌中心',
departmentPath: '品牌中心',
departmentMemberships: [{ departmentName: '设计部', departmentPath: '品牌中心/设计部', isPrimary: false }],
})],
[{
...snapshotRow('project-1', ['ding-1']),
activeTasks: [
{ ...snapshotRow('project-1', ['ding-1']).activeTasks[0], isActive: false },
{ ...snapshotRow('project-1', ['ding-1']).activeTasks[0], taskId: 'other', isBrandCenter: false, isActive: true },
],
}],
{ lastSyncedAt: null, syncStatus: 'pending' },
)
expect(insights.summary.activeProjectAssignments).toBe(1)
expect(insights.summary.averageLoadPercent).toBe(50)
expect(insights.departments[0]).toMatchObject({ name: '设计部', personnelCount: 1 })
})
it('排除商品企划部及其流程归属,不纳入人员和负荷平均值', () => {
const insights = buildTeamWorkloadInsights(
[
rosterRow({ id: 'design', dingtalkUserId: 'design', displayName: '设计员工' }),
rosterRow({
id: 'planning',
dingtalkUserId: 'planning',
displayName: '企划员工',
departmentName: '商品企划部',
departmentPath: '品牌中心/商品企划部',
}),
],
[
snapshotRow('design-project', ['design']),
snapshotRow('planning-project', ['planning']),
],
{ lastSyncedAt: null, syncStatus: 'ready' },
)
expect(insights.summary).toMatchObject({
personnelCount: 1,
busyPersonnel: 1,
idlePersonnel: 0,
activeProjectAssignments: 1,
averageLoadPercent: 50,
sourceOngoingProjects: 2,
excludedDepartmentNames: ['商品企划部'],
})
expect(insights.departments).toHaveLength(1)
expect(insights.departments[0]).toMatchObject({
name: '设计部',
personnelCount: 1,
activeProjectAssignments: 1,
})
expect(insights.departments.some((department) => department.name === '商品企划部')).toBe(false)
expect(insights.departments.flatMap((department) => department.people).map((person) => person.name)).toEqual(['设计员工'])
})
})