diff --git a/lib/__tests__/ding-workflow-sync.test.ts b/lib/__tests__/ding-workflow-sync.test.ts index 9979658..c7e0f55 100644 --- a/lib/__tests__/ding-workflow-sync.test.ts +++ b/lib/__tests__/ding-workflow-sync.test.ts @@ -316,8 +316,8 @@ describe('ding workflow snapshot sync semantics', () => { ])).toEqual({ assignmentNodeLabel: '内容主管分配任务', assignmentStartedAt: '2026-08-03T01:00:00.000Z', - reviewNodeLabel: '编导审核', - reviewFinishedAt: '2026-08-05T02:00:00.000Z', + reviewNodeLabel: '内容主管审核', + reviewFinishedAt: null, }) expect(resolveDingWorkflowDetailMilestones(DING_WORKFLOW_ALLOWED_PROCESS_CODES[1], [ @@ -332,6 +332,45 @@ describe('ding workflow snapshot sync semantics', () => { }) }) + 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('matches initiators by DingTalk roster profile and formats the HR organization path', () => { expect(resolveDingWorkflowInitiator({ displayName: '荀瑶', diff --git a/lib/ding-workflow-sync.ts b/lib/ding-workflow-sync.ts index 1b5775c..a708aa0 100644 --- a/lib/ding-workflow-sync.ts +++ b/lib/ding-workflow-sync.ts @@ -661,16 +661,12 @@ const DETAIL_MILESTONE_NODES: Readonly task.createdAt) .sort(compareTaskTimestamps)[0] || null const completedReviews = config.reviewNodes.flatMap((reviewNode) => tasks - .filter((task) => reviewNode.names.includes(task.nodeName) && task.finishedAt) + // A source task may carry a finish timestamp while the approval node is + // still active (for example, during an upstream mirror update). An active + // review node is not an end milestone, even when finishTime is present. + .filter((task) => reviewNode.names.includes(task.nodeName) && task.finishedAt && !task.isActive) .map((task) => ({ label: reviewNode.label, finishedAt: task.finishedAt as string,