import { describe, expect, it, vi } from 'vitest'; import { MemberWorkSyncTaskImpactResolver } from '@features/member-work-sync/main/adapters/input/MemberWorkSyncTaskImpactResolver'; import type { TeamTask } from '@shared/types'; describe('MemberWorkSyncTaskImpactResolver', () => { it('targets owner, reviewer, dependent owners and lead oversight without team-wide fan-out', async () => { const tasks: TeamTask[] = [ { id: 'task-a', displayId: '#11111111', subject: 'Changed', status: 'in_progress', owner: 'alice', reviewState: 'review', historyEvents: [ { id: 'evt-review', type: 'review_requested', timestamp: '2026-04-29T00:00:00.000Z', from: 'none', to: 'review', reviewer: 'bob', }, ], }, { id: 'task-b', subject: 'Unblocked by A', status: 'pending', owner: 'tom', blockedBy: ['task-a'], }, ]; const resolver = new MemberWorkSyncTaskImpactResolver({ taskReader: { getTasks: vi.fn(async () => tasks) }, kanbanManager: { getState: vi.fn(async () => ({ tasks: {} })) }, activeMemberSource: { loadActiveMemberNames: vi.fn(async () => ['alice', 'bob', 'team-lead', 'tom']), }, } as never); await expect(resolver.resolve({ teamName: 'team-a', taskId: '#11111111' })).resolves.toEqual({ memberNames: ['alice', 'bob', 'tom'], fallbackTeamWide: false, diagnostics: [], }); }); it('falls back to team-wide routing when a task was removed before impact can be resolved', async () => { const resolver = new MemberWorkSyncTaskImpactResolver({ taskReader: { getTasks: vi.fn(async () => []) }, kanbanManager: { getState: vi.fn(async () => ({ tasks: {} })) }, activeMemberSource: { loadActiveMemberNames: vi.fn(async () => ['alice']) }, } as never); await expect(resolver.resolve({ teamName: 'team-a', taskId: 'deleted-task' })).resolves.toEqual( { memberNames: [], fallbackTeamWide: true, diagnostics: ['task_not_found'], } ); }); it('targets lead when a deleted task breaks active dependent work', async () => { const tasks: TeamTask[] = [ { id: 'task-deleted', subject: 'Deleted dependency', status: 'deleted', owner: 'alice', deletedAt: '2026-04-29T00:00:00.000Z', }, { id: 'task-dependent', subject: 'Depends on deleted task', status: 'pending', owner: 'tom', blockedBy: ['task-deleted'], }, ]; const resolver = new MemberWorkSyncTaskImpactResolver({ taskReader: { getTasks: vi.fn(async () => tasks) }, kanbanManager: { getState: vi.fn(async () => ({ tasks: {} })) }, activeMemberSource: { loadActiveMemberNames: vi.fn(async () => ['alice', 'team-lead', 'tom']), }, } as never); await expect(resolver.resolve({ teamName: 'team-a', taskId: 'task-deleted' })).resolves.toEqual( { memberNames: ['alice', 'team-lead', 'tom'], fallbackTeamWide: false, diagnostics: ['dependent_task_has_deleted_dependency'], } ); }); it('targets dependent owners when dependencies reference the changed task by display id', async () => { const tasks: TeamTask[] = [ { id: 'task-a', displayId: '#11111111', subject: 'Changed dependency', status: 'in_progress', owner: 'alice', }, { id: 'task-b', subject: 'Depends on display id', status: 'pending', owner: 'tom', blockedBy: ['11111111'], }, ]; const resolver = new MemberWorkSyncTaskImpactResolver({ taskReader: { getTasks: vi.fn(async () => tasks) }, kanbanManager: { getState: vi.fn(async () => ({ tasks: {} })) }, activeMemberSource: { loadActiveMemberNames: vi.fn(async () => ['alice', 'team-lead', 'tom']), }, } as never); await expect(resolver.resolve({ teamName: 'team-a', taskId: 'task-a' })).resolves.toEqual({ memberNames: ['alice', 'tom'], fallbackTeamWide: false, diagnostics: [], }); }); });