524 lines
23 KiB
TypeScript
524 lines
23 KiB
TypeScript
import {
|
|
DING_WORKFLOW_ALLOWED_PROCESS_CODES,
|
|
DING_WORKFLOW_BRANDS,
|
|
DING_WORKFLOW_NEW_PRODUCT_DEMAND_TYPE,
|
|
DING_WORKFLOW_STARTED_AT_CUTOFF_ISO,
|
|
buildBrandCenterOngoingArtWorkflowWhere,
|
|
buildBrandCenterOngoingWorkflowWhere,
|
|
buildMonthlyLaunchOngoingWorkflowWhere,
|
|
buildMonthlyLaunchWorkflowWhere,
|
|
getShanghaiMonthRange,
|
|
getShanghaiMonthRangeForValue,
|
|
getMonthlyLaunchMonthOptions,
|
|
resolveMonthlyLaunchDateRange,
|
|
getDingWorkflowActivityNodeName,
|
|
getDingWorkflowProjectType,
|
|
isDingWorkflowTaskOngoing,
|
|
isAllowedDingWorkflowProcessCode,
|
|
isDingWorkflowStartedOnOrAfterCutoff,
|
|
isDingWorkflowOngoing,
|
|
isDingWorkflowTerminal,
|
|
isDingWorkflowCompletedInRange,
|
|
isDingWorkflowDeliveryAssessmentInRange,
|
|
isDingWorkflowApprovedCompletion,
|
|
normalizeDingWorkflowBrand,
|
|
normalizeDingWorkflowDepartment,
|
|
normalizeDingWorkflowDetailValue,
|
|
normalizeDingWorkflowProcessCode,
|
|
parseDingWorkflowSourceCursor,
|
|
resolveDingWorkflowInitiator,
|
|
resolveDingWorkflowDetailMilestones,
|
|
selectLatestDingWorkflowSchemeValue,
|
|
serializeDingWorkflowSourceCursor,
|
|
shouldRunDingWorkflowDailyReconciliation,
|
|
sourceWorkflowTimestampToIso,
|
|
storedWorkflowTimestampToIso,
|
|
} 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() }))
|
|
|
|
describe('ding workflow snapshot sync semantics', () => {
|
|
test.each([
|
|
['RUNNING', null, true],
|
|
['NEW', '', true],
|
|
['COMPLETED', null, false],
|
|
['TERMINATED', null, false],
|
|
['CANCELED', null, false],
|
|
['RUNNING', '2026-08-20 10:30:00', false],
|
|
['', null, false],
|
|
])('status %s and finish time %s resolves ongoing=%s', (status, finishTime, expected) => {
|
|
expect(isDingWorkflowOngoing(status, finishTime)).toBe(expected)
|
|
})
|
|
|
|
test('treats any upstream finish time as terminal even when status is stale', () => {
|
|
expect(isDingWorkflowTerminal('RUNNING', '2026-08-19 18:04:00')).toBe(true)
|
|
expect(isDingWorkflowTerminal('COMPLETED', null)).toBe(true)
|
|
expect(isDingWorkflowTerminal('RUNNING', null)).toBe(false)
|
|
})
|
|
|
|
it('interprets upstream report timestamps as Asia/Shanghai wall-clock values', () => {
|
|
expect(sourceWorkflowTimestampToIso('2026-08-20 10:30:00')).toBe('2026-08-20T02:30:00.000Z')
|
|
expect(sourceWorkflowTimestampToIso('2026-08-20T10:30:00Z')).toBe('2026-08-20T02:30:00.000Z')
|
|
expect(sourceWorkflowTimestampToIso('2026-08-20T10:30:00+08:00')).toBe('2026-08-20T02:30:00.000Z')
|
|
expect(sourceWorkflowTimestampToIso('invalid')).toBeNull()
|
|
})
|
|
|
|
it('does not reinterpret canonical snapshot timestamps a second time', () => {
|
|
expect(storedWorkflowTimestampToIso('2026-08-20T02:30:00.000Z')).toBe('2026-08-20T02:30:00.000Z')
|
|
expect(storedWorkflowTimestampToIso('2026-08-20 10:30:00')).toBe('2026-08-20T02:30:00.000Z')
|
|
expect(storedWorkflowTimestampToIso('invalid')).toBeNull()
|
|
})
|
|
|
|
it('only includes workflows started from 2026-04-01 Asia/Shanghai', () => {
|
|
expect(DING_WORKFLOW_STARTED_AT_CUTOFF_ISO).toBe('2026-03-31T16:00:00.000Z')
|
|
expect(isDingWorkflowStartedOnOrAfterCutoff('2026-03-31 23:59:59')).toBe(false)
|
|
expect(isDingWorkflowStartedOnOrAfterCutoff('2026-04-01 00:00:00')).toBe(true)
|
|
expect(isDingWorkflowStartedOnOrAfterCutoff('2026-03-31T23:59:59.999Z')).toBe(false)
|
|
expect(isDingWorkflowStartedOnOrAfterCutoff('2026-04-01T00:00:00.000Z')).toBe(true)
|
|
expect(isDingWorkflowStartedOnOrAfterCutoff('invalid')).toBe(false)
|
|
})
|
|
|
|
it('uses Shanghai calendar boundaries for monthly completion', () => {
|
|
const range = getShanghaiMonthRange(new Date('2026-08-26T03:00:00.000Z'))
|
|
expect(range.start.toISOString()).toBe('2026-07-31T16:00:00.000Z')
|
|
expect(range.end.toISOString()).toBe('2026-08-31T16:00:00.000Z')
|
|
expect(isDingWorkflowCompletedInRange('COMPLETED', '2026-08-01 00:00:00', range)).toBe(true)
|
|
expect(isDingWorkflowCompletedInRange('COMPLETED', '2026-09-01 00:00:00', range)).toBe(false)
|
|
expect(isDingWorkflowCompletedInRange('TERMINATED', '2026-08-12 12:00:00', range)).toBe(false)
|
|
expect(isDingWorkflowCompletedInRange('COMPLETED', null, range)).toBe(false)
|
|
})
|
|
|
|
it('resolves a requested Shanghai month and inclusive custom date range', () => {
|
|
expect(getShanghaiMonthRangeForValue('2026-08')).toEqual({
|
|
start: new Date('2026-07-31T16:00:00.000Z'),
|
|
end: new Date('2026-08-31T16:00:00.000Z'),
|
|
})
|
|
expect(resolveMonthlyLaunchDateRange({ startDate: '2026-08-01', endDate: '2026-08-31' })).toEqual({
|
|
start: new Date('2026-07-31T16:00:00.000Z'),
|
|
end: new Date('2026-08-31T16:00:00.000Z'),
|
|
})
|
|
})
|
|
|
|
it('offers month presets from the April 2026 cutoff through the current month', () => {
|
|
const options = getMonthlyLaunchMonthOptions(new Date('2026-09-15T00:00:00.000Z'))
|
|
expect(options.map((option) => option.value)).toEqual([
|
|
'2026-09',
|
|
'2026-08',
|
|
'2026-07',
|
|
'2026-06',
|
|
'2026-05',
|
|
'2026-04',
|
|
])
|
|
expect(options[0]).toMatchObject({ label: '本月', startDate: '2026-09-01', endDate: '2026-09-30' })
|
|
expect(options[1]).toMatchObject({ label: '上月', startDate: '2026-08-01', endDate: '2026-08-31' })
|
|
})
|
|
|
|
it('rejects invalid or ambiguous monthly launch date filters', () => {
|
|
expect(() => getShanghaiMonthRangeForValue('2026-13')).toThrow('month不是有效月份')
|
|
expect(() => getShanghaiMonthRangeForValue('2026/08')).toThrow('month格式应为YYYY-MM')
|
|
expect(() => resolveMonthlyLaunchDateRange({ startDate: '2026-02-30', endDate: '2026-03-01' })).toThrow('startDate不是有效日期')
|
|
expect(() => resolveMonthlyLaunchDateRange({ month: '2026-08', startDate: '2026-08-01' })).toThrow('month不能与startDate/endDate同时使用')
|
|
expect(() => resolveMonthlyLaunchDateRange({ startDate: '2026-09-01', endDate: '2026-08-31' })).toThrow('endDate不能早于startDate')
|
|
})
|
|
|
|
it('uses the delivery assessment finish node for monthly inclusion and excludes pending', () => {
|
|
const range = getShanghaiMonthRange(new Date('2026-08-26T03:00:00.000Z'))
|
|
expect(isDingWorkflowDeliveryAssessmentInRange({
|
|
status: 'on_time',
|
|
finishedAt: '2026-08-31T15:59:59.000Z',
|
|
}, range)).toBe(true)
|
|
expect(isDingWorkflowDeliveryAssessmentInRange({
|
|
status: 'overtime',
|
|
finishedAt: '2026-07-31T16:00:00.000Z',
|
|
}, range)).toBe(true)
|
|
expect(isDingWorkflowDeliveryAssessmentInRange({
|
|
status: 'on_time',
|
|
finishedAt: '2026-07-31T15:59:59.000Z',
|
|
}, range)).toBe(false)
|
|
expect(isDingWorkflowDeliveryAssessmentInRange({
|
|
status: 'pending',
|
|
finishedAt: '2026-08-10T00:00:00.000Z',
|
|
}, range)).toBe(false)
|
|
})
|
|
|
|
it('only treats completed workflows without a rejected decision as approved completions', () => {
|
|
expect(isDingWorkflowApprovedCompletion('COMPLETED', 'AGREE', null)).toBe(true)
|
|
expect(isDingWorkflowApprovedCompletion('completed', null, 'NONE')).toBe(true)
|
|
expect(isDingWorkflowApprovedCompletion('COMPLETED', 'REFUSE', null)).toBe(false)
|
|
expect(isDingWorkflowApprovedCompletion('COMPLETED', '已拒绝', null)).toBe(false)
|
|
expect(isDingWorkflowApprovedCompletion('COMPLETED', null, 'reject')).toBe(false)
|
|
expect(isDingWorkflowApprovedCompletion('TERMINATED', 'AGREE', null)).toBe(false)
|
|
})
|
|
|
|
it('only accepts the two approved process types', () => {
|
|
expect(DING_WORKFLOW_ALLOWED_PROCESS_CODES).toEqual([
|
|
'PROC-3571C915-0506-4490-A91A-8AA7EA33F838',
|
|
'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673',
|
|
])
|
|
expect(isAllowedDingWorkflowProcessCode(' proc-3571c915-0506-4490-a91a-8aa7ea33f838 ')).toBe(true)
|
|
expect(isAllowedDingWorkflowProcessCode('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673')).toBe(true)
|
|
expect(isAllowedDingWorkflowProcessCode('PROC-OTHER')).toBe(false)
|
|
expect(isAllowedDingWorkflowProcessCode(null)).toBe(false)
|
|
})
|
|
|
|
it('maps all confirmed content workflow milestone activity ids', () => {
|
|
const contentCode = DING_WORKFLOW_ALLOWED_PROCESS_CODES[1]
|
|
expect(getDingWorkflowActivityNodeName(contentCode, 'cc59_3e5a')).toBe('内容负责人分配任务')
|
|
expect(getDingWorkflowActivityNodeName(contentCode, '9fb8_32e7')).toBe('内容主管分配任务')
|
|
expect(getDingWorkflowActivityNodeName(contentCode, 'a021_1e2e')).toBe('编导审核')
|
|
expect(getDingWorkflowActivityNodeName(contentCode, '39ba_efb1')).toBe('编导审核')
|
|
expect(getDingWorkflowActivityNodeName(contentCode, '6c43_575f')).toBe('内容主管审核')
|
|
})
|
|
|
|
it('treats a task with finishTime as history even if its mirrored status is still running', () => {
|
|
expect(isDingWorkflowTaskOngoing('RUNNING', null)).toBe(true)
|
|
expect(isDingWorkflowTaskOngoing('RUNNING', '2026-08-25 11:20:00')).toBe(false)
|
|
expect(isDingWorkflowTaskOngoing('COMPLETED', null)).toBe(false)
|
|
})
|
|
|
|
it('upgrades the legacy workflow-only cursor to a three-table cursor', () => {
|
|
expect(parseDingWorkflowSourceCursor('2026-08-26 19:42:36')).toEqual({
|
|
version: 4,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: null,
|
|
schemeId: null,
|
|
lastFullReconciledAt: null,
|
|
})
|
|
})
|
|
|
|
it('round-trips the Workflow, Task and Scheme cursors with the daily reconciliation watermark', () => {
|
|
const cursor = {
|
|
version: 4 as const,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: 2248339,
|
|
schemeId: 10634556,
|
|
lastFullReconciledAt: '2026-09-16T18:00:00.000Z',
|
|
}
|
|
expect(parseDingWorkflowSourceCursor(serializeDingWorkflowSourceCursor(cursor))).toEqual(cursor)
|
|
})
|
|
|
|
it('preserves empty Task and Scheme cursors so the next run performs alignment', () => {
|
|
const cursor = {
|
|
version: 4 as const,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: null,
|
|
schemeId: null,
|
|
lastFullReconciledAt: null,
|
|
}
|
|
expect(parseDingWorkflowSourceCursor(serializeDingWorkflowSourceCursor(cursor))).toEqual(cursor)
|
|
})
|
|
|
|
it('upgrades version 2 cursors and requests one full Task and Scheme alignment', () => {
|
|
expect(parseDingWorkflowSourceCursor(JSON.stringify({
|
|
version: 2,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: 2248339,
|
|
schemeId: 10634556,
|
|
}))).toEqual({
|
|
version: 4,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: null,
|
|
schemeId: null,
|
|
lastFullReconciledAt: null,
|
|
})
|
|
})
|
|
|
|
it('upgrades version 3 cursors and requests the first daily reconciliation', () => {
|
|
expect(parseDingWorkflowSourceCursor(JSON.stringify({
|
|
version: 3,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: -1,
|
|
schemeId: 'invalid',
|
|
}))).toEqual({
|
|
version: 4,
|
|
workflowUpdatedAt: '2026-08-26 19:42:36',
|
|
taskId: null,
|
|
schemeId: null,
|
|
lastFullReconciledAt: null,
|
|
})
|
|
})
|
|
|
|
it('runs a full reconciliation at least once every 24 hours', () => {
|
|
const now = new Date('2026-09-17T02:00:00.000Z')
|
|
expect(shouldRunDingWorkflowDailyReconciliation(null, now)).toBe(true)
|
|
expect(shouldRunDingWorkflowDailyReconciliation('2026-09-16T02:00:01.000Z', now)).toBe(false)
|
|
expect(shouldRunDingWorkflowDailyReconciliation('2026-09-16T02:00:00.000Z', now)).toBe(true)
|
|
expect(shouldRunDingWorkflowDailyReconciliation('invalid', now)).toBe(true)
|
|
})
|
|
|
|
it('selects the latest non-empty Scheme row across product ID aliases', () => {
|
|
expect(selectLatestDingWorkflowSchemeValue([
|
|
{ sourceRowId: 101, name: '商品ID', value: '100000001' },
|
|
{ sourceRowId: 109, name: '商品Id', value: '100000009' },
|
|
{ sourceRowId: 110, name: '商品id', value: ' ' },
|
|
{ sourceRowId: 108, name: '品牌', value: '亿家达' },
|
|
], ['商品ID', '商品Id', '商品id'])).toBe('100000009')
|
|
})
|
|
|
|
it('only queries ongoing workflows whose current assignee belongs to Brand Center', () => {
|
|
expect(buildBrandCenterOngoingWorkflowWhere()).toEqual({
|
|
isOngoing: true,
|
|
processCode: { in: [...DING_WORKFLOW_ALLOWED_PROCESS_CODES] },
|
|
sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO },
|
|
activeTasks: { array_contains: [{ isBrandCenter: true, isActive: true }] },
|
|
})
|
|
})
|
|
|
|
it('reuses the ongoing-project art-design subset for monthly launches', () => {
|
|
expect(buildBrandCenterOngoingArtWorkflowWhere()).toEqual({
|
|
isOngoing: true,
|
|
processCode: DING_WORKFLOW_ALLOWED_PROCESS_CODES[0],
|
|
sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO },
|
|
activeTasks: { array_contains: [{ isBrandCenter: true, isActive: true }] },
|
|
})
|
|
expect(buildMonthlyLaunchOngoingWorkflowWhere()).toEqual({
|
|
...buildBrandCenterOngoingArtWorkflowWhere(),
|
|
brand: { in: [...DING_WORKFLOW_BRANDS] },
|
|
demandType: DING_WORKFLOW_NEW_PRODUCT_DEMAND_TYPE,
|
|
})
|
|
})
|
|
|
|
it('only queries post-April art-design workflows for the six-brand monthly launch metric', () => {
|
|
expect(buildMonthlyLaunchWorkflowWhere()).toEqual({
|
|
processCode: 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838',
|
|
sourceCreatedAtRaw: { gte: DING_WORKFLOW_STARTED_AT_CUTOFF_ISO },
|
|
brand: { in: [...DING_WORKFLOW_BRANDS] },
|
|
demandType: '新品',
|
|
})
|
|
})
|
|
|
|
it('normalizes process codes before persistence and comparison', () => {
|
|
expect(normalizeDingWorkflowProcessCode(' proc-ae9ff3e1-a442-4e6a-85cf-0f2b86409673 '))
|
|
.toBe('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673')
|
|
expect(normalizeDingWorkflowProcessCode('')).toBeNull()
|
|
})
|
|
|
|
it('maps the two process codes to the dashboard task types', () => {
|
|
expect(getDingWorkflowProjectType('PROC-3571C915-0506-4490-A91A-8AA7EA33F838')).toBe('美术设计流程')
|
|
expect(getDingWorkflowProjectType('PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673')).toBe('内容视频和图片拍摄')
|
|
expect(getDingWorkflowProjectType('PROC-OTHER')).toBe('其他流程')
|
|
})
|
|
|
|
test.each([
|
|
['sid-startevent', '发起人'],
|
|
['1de3_de55', '部门负责人审核'],
|
|
['94c1_1f0d', '部门总监审核'],
|
|
['3cb8_8096', '品牌策划总监审批'],
|
|
['38da_544a', '品牌策划主管分配任务'],
|
|
['31cf_5b23', '出策划案'],
|
|
['a9e1_5be0', '出图'],
|
|
['de46_708d', '平面'],
|
|
['2f9e_36f0', 'AI视频'],
|
|
['b306_c849', '出图'],
|
|
['f073_738b', '平面'],
|
|
['6864_5429', '出图'],
|
|
['162b_23ea', '平面'],
|
|
['c9fe_a6b8', '出图/平面/AI视频'],
|
|
['c255_84b3', '品牌策划主管审核'],
|
|
['ebb9_213a', '运营接收图片并评分'],
|
|
['b426_1125', '运营接收图片并评分'],
|
|
['3e66_c105', '运营接收图片并评分'],
|
|
['64bb_b63a', '运营接收图片并评分'],
|
|
['2748_c5f9', '运营接收图片并评分'],
|
|
['45f4_e095', '运营接收图片并评分'],
|
|
['a8aa_ce41', '部门负责确认'],
|
|
['00d9_b57f', '抄送部门总监'],
|
|
])('maps art-design activity %s to %s from the supplied process definition', (activityId, expected) => {
|
|
expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], activityId)).toBe(expected)
|
|
})
|
|
|
|
it('does not reuse art-design activity names for other processes or unknown nodes', () => {
|
|
expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], '38da_544a')).toBeNull()
|
|
expect(getDingWorkflowActivityNodeName(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], 'unknown-node')).toBeNull()
|
|
expect(getDingWorkflowActivityNodeName(null, '38da_544a')).toBeNull()
|
|
})
|
|
|
|
it('normalizes approved brands and groups unmatched values as other brands', () => {
|
|
expect(normalizeDingWorkflowBrand('自有品牌-亿家达')).toBe('亿家达')
|
|
expect(normalizeDingWorkflowBrand('自有品牌-卓禾')).toBe('卓禾')
|
|
expect(normalizeDingWorkflowBrand('索菲亚')).toBe('索菲亚')
|
|
expect(normalizeDingWorkflowBrand('新品牌')).toBe('其他品牌')
|
|
expect(normalizeDingWorkflowBrand(null)).toBe('其他品牌')
|
|
})
|
|
|
|
it('normalizes DingTalk department fields before grouping requester dimensions', () => {
|
|
expect(normalizeDingWorkflowDepartment('["运营中心E"]')).toBe('运营中心E')
|
|
expect(normalizeDingWorkflowDepartment('[{"name":"品牌中心"}]')).toBe('品牌中心')
|
|
expect(normalizeDingWorkflowDepartment(['运营中心E', '运营中心E'])).toBe('运营中心E')
|
|
expect(normalizeDingWorkflowDepartment('综合支持部')).toBe('综合支持部')
|
|
expect(normalizeDingWorkflowDepartment(null)).toBeNull()
|
|
})
|
|
|
|
it('normalizes scalar, array and contact-shaped detail form values', () => {
|
|
expect(normalizeDingWorkflowDetailValue('天猫')).toBe('天猫')
|
|
expect(normalizeDingWorkflowDetailValue('["书桌","书柜"]')).toBe('书桌 / 书柜')
|
|
expect(normalizeDingWorkflowDetailValue([{ name: '亿家达' }, { name: '亿家达' }])).toBe('亿家达')
|
|
expect(normalizeDingWorkflowDetailValue(null)).toBeNull()
|
|
})
|
|
|
|
it('uses the confirmed art and content-video milestone definitions', () => {
|
|
const tasks = [
|
|
{
|
|
taskId: 'task-1', activityId: 'art-start', nodeName: '品牌策划主管分配任务',
|
|
assigneeUserId: 'user-1', assigneeName: '甲', assigneeDepartment: '品牌中心',
|
|
isBrandCenter: true, isLeadership: false, isActive: false, reviewPassed: true,
|
|
result: 'AGREE', createdAt: '2026-08-01T01:00:00.000Z', finishedAt: '2026-08-01T02:00:00.000Z',
|
|
},
|
|
{
|
|
taskId: 'task-2', activityId: 'c255_84b3', nodeName: '品牌策划主管审核',
|
|
assigneeUserId: 'user-2', assigneeName: '乙', assigneeDepartment: '品牌中心',
|
|
isBrandCenter: true, isLeadership: true, isActive: false, reviewPassed: true,
|
|
result: 'AGREE', createdAt: '2026-08-02T01:00:00.000Z', finishedAt: '2026-08-02T02:00:00.000Z',
|
|
},
|
|
]
|
|
expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[0], tasks)).toEqual({
|
|
assignmentNodeLabel: '品牌策划主管分配任务',
|
|
assignmentStartedAt: '2026-08-01T01:00:00.000Z',
|
|
reviewNodeLabel: '品牌策划主管审核',
|
|
reviewFinishedAt: '2026-08-02T02:00:00.000Z',
|
|
})
|
|
|
|
expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [
|
|
{ ...tasks[0], nodeName: '内容部负责人', createdAt: '2026-08-03T01:00:00.000Z' },
|
|
{ ...tasks[0], taskId: 'task-3', nodeName: '编导', finishedAt: '2026-08-04T02:00:00.000Z' },
|
|
{ ...tasks[0], taskId: 'task-4', nodeName: '编导', finishedAt: '2026-08-05T02:00:00.000Z' },
|
|
])).toEqual({
|
|
assignmentNodeLabel: '内容主管分配任务',
|
|
assignmentStartedAt: null,
|
|
reviewNodeLabel: '内容主管审核',
|
|
reviewFinishedAt: null,
|
|
})
|
|
|
|
expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [
|
|
{ ...tasks[0], nodeName: '内容部负责人', createdAt: '2026-08-06T01:00:00.000Z' },
|
|
{ ...tasks[0], taskId: 'task-5', nodeName: '编导', finishedAt: '2026-08-07T02:00:00.000Z' },
|
|
{ ...tasks[0], taskId: 'task-6', nodeName: '品牌中心负责人审核', finishedAt: '2026-08-08T02:00:00.000Z' },
|
|
])).toEqual({
|
|
assignmentNodeLabel: '内容主管分配任务',
|
|
assignmentStartedAt: null,
|
|
reviewNodeLabel: '内容主管审核',
|
|
reviewFinishedAt: null,
|
|
})
|
|
})
|
|
|
|
it('does not treat unknown leadership tasks or assignment labels as review milestones', () => {
|
|
const contentCode = DING_WORKFLOW_ALLOWED_PROCESS_CODES[1]
|
|
expect(getDingWorkflowActivityNodeName(contentCode, '8814_bf03')).toBeNull()
|
|
expect(resolveDingWorkflowDetailMilestones(contentCode, [{
|
|
taskId: 'unknown-leadership-task',
|
|
activityId: '8814_bf03',
|
|
nodeName: '品牌中心负责人审核',
|
|
assigneeUserId: 'leader-1',
|
|
assigneeName: '领导者',
|
|
assigneeDepartment: '品牌中心',
|
|
isBrandCenter: true,
|
|
isLeadership: true,
|
|
isActive: false,
|
|
reviewPassed: true,
|
|
result: 'AGREE',
|
|
createdAt: '2026-08-26T03:00:00.000Z',
|
|
finishedAt: '2026-08-26T03:19:00.000Z',
|
|
}])).toEqual({
|
|
assignmentNodeLabel: '内容主管分配任务',
|
|
assignmentStartedAt: null,
|
|
reviewNodeLabel: '内容主管审核',
|
|
reviewFinishedAt: null,
|
|
})
|
|
})
|
|
|
|
it('does not expose a content supervisor review time before the review is confirmed', () => {
|
|
const pendingSupervisorReview = {
|
|
taskId: 'pending-supervisor-review',
|
|
activityId: 'content-review',
|
|
nodeName: '内容主管审核',
|
|
assigneeUserId: 'user-2',
|
|
assigneeName: '乙',
|
|
assigneeDepartment: '品牌中心',
|
|
isBrandCenter: true,
|
|
isLeadership: true,
|
|
isActive: true,
|
|
reviewPassed: false,
|
|
result: null,
|
|
createdAt: '2026-08-27T03:00:00.000Z',
|
|
finishedAt: '2026-08-27T03:19:00.000Z',
|
|
}
|
|
|
|
expect(resolveDingWorkflowDetailMilestones(
|
|
DING_WORKFLOW_ALLOWED_PROCESS_CODES[1],
|
|
[
|
|
pendingSupervisorReview,
|
|
{
|
|
...pendingSupervisorReview,
|
|
taskId: 'director-review',
|
|
nodeName: '编导审核',
|
|
isActive: false,
|
|
reviewPassed: true,
|
|
result: 'AGREE',
|
|
finishedAt: '2026-08-27T02:30:00.000Z',
|
|
},
|
|
],
|
|
)).toEqual({
|
|
assignmentNodeLabel: '内容主管分配任务',
|
|
assignmentStartedAt: null,
|
|
reviewNodeLabel: '内容主管审核',
|
|
reviewFinishedAt: null,
|
|
})
|
|
})
|
|
|
|
it('ignores an unconfirmed review that has a mirrored finish timestamp', () => {
|
|
expect(resolveDingWorkflowDetailMilestones(
|
|
DING_WORKFLOW_ALLOWED_PROCESS_CODES[1],
|
|
[{
|
|
taskId: 'running-supervisor-review',
|
|
activityId: '6c43_575f',
|
|
nodeName: '内容主管审核',
|
|
assigneeUserId: 'user-2',
|
|
assigneeName: '乙',
|
|
assigneeDepartment: '品牌中心',
|
|
isBrandCenter: true,
|
|
isLeadership: true,
|
|
isActive: false,
|
|
reviewPassed: false,
|
|
result: null,
|
|
createdAt: '2026-08-27T03:00:00.000Z',
|
|
finishedAt: '2026-08-27T03:19:00.000Z',
|
|
}],
|
|
)).toEqual({
|
|
assignmentNodeLabel: '内容主管分配任务',
|
|
assignmentStartedAt: null,
|
|
reviewNodeLabel: '内容主管审核',
|
|
reviewFinishedAt: null,
|
|
})
|
|
})
|
|
|
|
it('matches initiators by DingTalk roster profile and formats the HR organization path', () => {
|
|
expect(resolveDingWorkflowInitiator({
|
|
displayName: '荀瑶',
|
|
departmentName: '品类九部',
|
|
departmentPath: '江苏简尚品牌管理有限公司/运营中心A/事业四部/品类九部/淘系四组',
|
|
}, {
|
|
name: '旧需求人',
|
|
department: '旧需求部门',
|
|
})).toEqual({
|
|
name: '荀瑶',
|
|
department: '运营中心A-事业四部-品类九部-淘系四组',
|
|
})
|
|
})
|
|
|
|
it('uses the workflow snapshot when the initiator is absent from the HR roster', () => {
|
|
expect(resolveDingWorkflowInitiator(null, {
|
|
name: '林发起',
|
|
department: '[{"name":"品牌中心-品牌创意A部"}]',
|
|
})).toEqual({
|
|
name: '林发起',
|
|
department: '品牌中心-品牌创意A部',
|
|
})
|
|
})
|
|
})
|