255 lines
9.6 KiB
TypeScript
255 lines
9.6 KiB
TypeScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
|
||
import {
|
||
buildHrSyncPreviewLine,
|
||
buildHrSyncDingTalkContent,
|
||
formatBorrowedAssetSummary,
|
||
resolveHrSyncNotificationRoleRoute,
|
||
resolveHrSyncPersonDisplayName,
|
||
resolveSourcePersonStatus,
|
||
shouldNotifyHrSync,
|
||
shouldUseSourceDepartmentBinding
|
||
} from '../hrSyncHelpers'
|
||
|
||
test('resolvePersonStatus 默认应将状态值 2 视为 active,仅将状态值 5 视为 inactive', () => {
|
||
const previous = process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
delete process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
|
||
try {
|
||
assert.equal(resolveSourcePersonStatus('2'), 'active')
|
||
assert.equal(resolveSourcePersonStatus('5'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('3'), 'active')
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
} else {
|
||
process.env.HR_SYNC_INACTIVE_STATUS_VALUES = previous
|
||
}
|
||
}
|
||
})
|
||
|
||
test('resolvePersonStatus 在显式配置状态值 2 为停用时仍应尊重配置', () => {
|
||
const previous = process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
process.env.HR_SYNC_INACTIVE_STATUS_VALUES = '2,5'
|
||
|
||
try {
|
||
assert.equal(resolveSourcePersonStatus('2'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('5'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('3'), 'active')
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
} else {
|
||
process.env.HR_SYNC_INACTIVE_STATUS_VALUES = previous
|
||
}
|
||
}
|
||
})
|
||
|
||
test('shouldUseSourceDepartmentBinding 默认应允许状态值 2 和 3 保留源部门', () => {
|
||
const previous = process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
delete process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
|
||
try {
|
||
assert.equal(shouldUseSourceDepartmentBinding('2'), true)
|
||
assert.equal(shouldUseSourceDepartmentBinding('3'), true)
|
||
assert.equal(shouldUseSourceDepartmentBinding('5'), false)
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.HR_SYNC_INACTIVE_STATUS_VALUES
|
||
} else {
|
||
process.env.HR_SYNC_INACTIVE_STATUS_VALUES = previous
|
||
}
|
||
}
|
||
})
|
||
|
||
test('resolvePersonStatus 在状态文案包含离职语义时应返回 inactive', () => {
|
||
const previousKeywords = process.env.HR_SYNC_INACTIVE_STATUS_KEYWORDS
|
||
process.env.HR_SYNC_INACTIVE_STATUS_KEYWORDS = '离职,停用,inactive'
|
||
|
||
try {
|
||
assert.equal(resolveSourcePersonStatus('离职'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('已停用'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('Inactive'), 'inactive')
|
||
assert.equal(resolveSourcePersonStatus('在职'), 'active')
|
||
} finally {
|
||
process.env.HR_SYNC_INACTIVE_STATUS_KEYWORDS = previousKeywords
|
||
}
|
||
})
|
||
|
||
test('formatBorrowedAssetSummary 会按“编码 / 名称”输出并在超出数量时补充总数', () => {
|
||
const summary = formatBorrowedAssetSummary([
|
||
{ equipmentCode: 'JS-PC-1001', equipmentName: '主机' },
|
||
{ equipmentCode: 'JS-MON-2001', equipmentName: '显示器' },
|
||
{ equipmentCode: 'JS-KB-3001', equipmentName: '键盘' },
|
||
{ equipmentCode: 'JS-MS-4001', equipmentName: '鼠标' }
|
||
])
|
||
|
||
assert.equal(summary, 'JS-PC-1001 / 主机、JS-MON-2001 / 显示器、JS-KB-3001 / 键盘 等 4 项')
|
||
})
|
||
|
||
test('resolveHrSyncPersonDisplayName 会优先返回花名', () => {
|
||
assert.equal(
|
||
resolveHrSyncPersonDisplayName({
|
||
nickname: '三藏',
|
||
realName: '张亮'
|
||
}),
|
||
'三藏'
|
||
)
|
||
|
||
assert.equal(
|
||
resolveHrSyncPersonDisplayName({
|
||
nickname: '',
|
||
realName: '张亮'
|
||
}),
|
||
'张亮'
|
||
)
|
||
})
|
||
|
||
test('buildHrSyncPreviewLine 会优先使用结构化资产生成预览行', () => {
|
||
const line = buildHrSyncPreviewLine({
|
||
changeType: 'person_deactivated',
|
||
title: '人员离职/停用:三藏',
|
||
detail: '该人员已不在本次同步名单中,请行政确认是否还存在资产交接或借还处理。',
|
||
afterSnapshot: {
|
||
assets: [
|
||
{ equipmentCode: 'JS-PC-1001', equipmentName: '主机' },
|
||
{ equipmentCode: 'JS-MON-2001', equipmentName: '显示器' }
|
||
]
|
||
}
|
||
})
|
||
|
||
assert.equal(line, '人员离职/停用:三藏(名下资产:JS-PC-1001 / 主机、JS-MON-2001 / 显示器)')
|
||
})
|
||
|
||
test('buildHrSyncPreviewLine 在没有结构化资产时仍兼容旧的详情文本', () => {
|
||
const line = buildHrSyncPreviewLine({
|
||
changeType: 'person_position_changed',
|
||
title: '岗位变动:悟空',
|
||
detail: '岗位已从“摄影师”调整为“导演”。 名下资产:JS-PC-2001 / 主机。'
|
||
})
|
||
|
||
assert.equal(line, '岗位变动:悟空(名下资产:JS-PC-2001 / 主机)')
|
||
})
|
||
|
||
test('花名册同步无变化只保存日志,有变化或失败才发送通知', () => {
|
||
assert.equal(shouldNotifyHrSync('success', 0), false)
|
||
assert.equal(shouldNotifyHrSync('success', 2), true)
|
||
assert.equal(shouldNotifyHrSync('failed', 0), true)
|
||
})
|
||
|
||
test('花名册同步变动通知 IT,无可用 IT 时回退管理员;同步失败直接通知管理员', () => {
|
||
assert.deepEqual(resolveHrSyncNotificationRoleRoute('success'), {
|
||
primaryRoles: ['it'],
|
||
fallbackRoles: ['admin']
|
||
})
|
||
assert.deepEqual(resolveHrSyncNotificationRoleRoute('failed'), {
|
||
primaryRoles: ['admin'],
|
||
fallbackRoles: []
|
||
})
|
||
})
|
||
|
||
test('花名册钉钉汇总按变动类型分组,并让每件资产独占一行', () => {
|
||
const content = buildHrSyncDingTalkContent({
|
||
status: 'success',
|
||
occurredAt: new Date('2026-07-16T02:00:00.000Z'),
|
||
changesCount: 3,
|
||
summary: '新人入职 1 项,岗位变动 1 项,人员离职/停用 1 项。',
|
||
changes: [
|
||
{
|
||
entityName: '小满',
|
||
changeType: 'person_created',
|
||
title: '新人入职:小满',
|
||
detail: '系统归属部门:产品部,岗位:产品经理。',
|
||
afterSnapshot: {
|
||
displayName: '小满',
|
||
mainDepartmentName: '产品部',
|
||
position: '产品经理'
|
||
}
|
||
},
|
||
{
|
||
entityName: '范闲',
|
||
changeType: 'person_position_changed',
|
||
title: '岗位变动:范闲',
|
||
detail: '岗位发生变化。',
|
||
beforeSnapshot: { position: '摄影师' },
|
||
afterSnapshot: {
|
||
displayName: '范闲',
|
||
position: '导演',
|
||
assets: [
|
||
{ equipmentCode: 'JS-PC-1001', equipmentName: '主机' },
|
||
{ equipmentCode: 'JS-MON-2001', equipmentName: '显示器' }
|
||
]
|
||
}
|
||
},
|
||
{
|
||
entityName: '旭尧',
|
||
changeType: 'person_deactivated',
|
||
title: '人员离职/停用:旭尧',
|
||
detail: '人员离职。',
|
||
afterSnapshot: {
|
||
displayName: '旭尧',
|
||
assets: [
|
||
{ equipmentCode: 'JS-NB-3001', equipmentName: '笔记本' }
|
||
]
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
assert.match(content, /\*\*同步时间:\*\* 2026年7月16日 10:00/)
|
||
assert.match(content, /\*\*执行结果:\*\* 本次同步记录 3 项变化/)
|
||
assert.match(content, /\*\*变动概览:\*\* 新入职 1 项|岗位变动 1 项|离职 \/ 停用 1 项/)
|
||
assert.match(content, /\*\*新入职(1)\*\*/)
|
||
assert.match(content, /部门:产品部/)
|
||
assert.match(content, /\*\*岗位变动(1)\*\*/)
|
||
assert.match(content, /岗位:摄影师 → 导演/)
|
||
assert.match(content, /- JS\\-PC\\-1001|主机\n- JS\\-MON\\-2001|显示器/)
|
||
assert.match(content, /\*\*离职 \/ 停用(1)\*\*/)
|
||
assert.match(content, /- JS\\-NB\\-3001|笔记本/)
|
||
const assetLines = content.split('\n').filter((line) => line.startsWith('- JS'))
|
||
assert.deepEqual(assetLines, [
|
||
'- JS\\-PC\\-1001|主机',
|
||
'- JS\\-MON\\-2001|显示器',
|
||
'- JS\\-NB\\-3001|笔记本'
|
||
])
|
||
})
|
||
|
||
test('花名册消息列出实际自动入库、归属更新和待核对编码,普通状态变化不要求扫码', () => {
|
||
const outcomes = [
|
||
{ kind: 'returned', code: 'AUTO-RETURN' },
|
||
{ kind: 'reassigned', code: 'AUTO-TRANSFER' },
|
||
{ kind: 'review', code: 'CHECK-ASSET' }
|
||
] as const
|
||
const content = buildHrSyncDingTalkContent({
|
||
status: 'success', occurredAt: new Date('2026-11-09T00:00:00Z'), changesCount: 3, summary: '',
|
||
changes: outcomes.map(({ kind, code }) => ({
|
||
changeType: `person_assets_${kind}`, entityName: '测试员工', title: '资产同步', detail: '',
|
||
afterSnapshot: { assetOutcome: {
|
||
personId: 1, sourceUserId: 'mock-only', personName: '测试员工', kind, departmentName: '新部门',
|
||
assets: [{ equipmentCode: code, equipmentName: '测试资产' }], reason: '持有人部门不一致'
|
||
} }
|
||
}))
|
||
}).replace(/\\-/g, '-')
|
||
assert.match(content, /已自动归还入库 1 项:AUTO-RETURN。保留原库位,无需扫码。/)
|
||
assert.match(content, /归属已更新至“新部门”,共 1 项:AUTO-TRANSFER。/)
|
||
assert.match(content, /待核对 1 项:CHECK-ASSET。持有人部门不一致。/)
|
||
assert.doesNotMatch(content, /请逐项核对|快捷入库|需要 IT 核对/)
|
||
})
|
||
|
||
test('批量归属通知为省略提示预留钉钉消息长度', () => {
|
||
const changes = Array.from({ length: 105 }, (_, index) => ({
|
||
changeType: 'person_assets_reassigned', entityName: '测试员工', title: '资产归属更新', detail: '',
|
||
afterSnapshot: { assetOutcome: {
|
||
personId: index + 1, sourceUserId: `employee-${index}`, personName: '测试员工',
|
||
kind: 'reassigned', departmentName: '测试部门',
|
||
assets: [{ equipmentCode: `JS-TRANS-${String(index).padStart(5, '0')}`, equipmentName: '主机' }]
|
||
} }
|
||
}))
|
||
const content = buildHrSyncDingTalkContent({
|
||
status: 'success', occurredAt: new Date('2026-11-09T00:00:00Z'), changesCount: changes.length, summary: '', changes
|
||
})
|
||
assert.ok(content.length <= 4300, `通知长度为 ${content.length}`)
|
||
assert.match(content, /另有 \d+ 条变动未在消息中展开/)
|
||
})
|