255 lines
7.7 KiB
TypeScript
255 lines
7.7 KiB
TypeScript
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: [],
|
|
});
|
|
});
|
|
|
|
it('does not target owners of already approved dependent tasks', async () => {
|
|
const tasks: TeamTask[] = [
|
|
{
|
|
id: 'task-a',
|
|
subject: 'Changed dependency',
|
|
status: 'completed',
|
|
owner: 'alice',
|
|
},
|
|
{
|
|
id: 'task-approved-dependent',
|
|
subject: 'Already approved dependent',
|
|
status: 'in_progress',
|
|
owner: 'tom',
|
|
blockedBy: ['task-a'],
|
|
},
|
|
];
|
|
const resolver = new MemberWorkSyncTaskImpactResolver({
|
|
taskReader: { getTasks: vi.fn(async () => tasks) },
|
|
kanbanManager: {
|
|
getState: vi.fn(async () => ({
|
|
tasks: {
|
|
'task-approved-dependent': {
|
|
column: 'approved',
|
|
movedAt: '2026-05-06T19:06:07.257Z',
|
|
},
|
|
},
|
|
})),
|
|
},
|
|
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'],
|
|
fallbackTeamWide: false,
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('does not treat stale review state as reviewer-missing when kanban says approved', async () => {
|
|
const tasks: TeamTask[] = [
|
|
{
|
|
id: 'task-approved',
|
|
subject: 'Approved after review',
|
|
status: 'in_progress',
|
|
owner: 'alice',
|
|
reviewState: 'review',
|
|
historyEvents: [
|
|
{
|
|
id: 'evt-review',
|
|
type: 'review_requested',
|
|
timestamp: '2026-05-06T19:00:00.000Z',
|
|
from: 'none',
|
|
to: 'review',
|
|
reviewer: 'bob',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
const resolver = new MemberWorkSyncTaskImpactResolver({
|
|
taskReader: { getTasks: vi.fn(async () => tasks) },
|
|
kanbanManager: {
|
|
getState: vi.fn(async () => ({
|
|
tasks: {
|
|
'task-approved': {
|
|
column: 'approved',
|
|
movedAt: '2026-05-06T19:06:07.257Z',
|
|
},
|
|
},
|
|
})),
|
|
},
|
|
activeMemberSource: {
|
|
loadActiveMemberNames: vi.fn(async () => ['alice', 'bob', 'team-lead']),
|
|
},
|
|
} as never);
|
|
|
|
await expect(
|
|
resolver.resolve({ teamName: 'team-a', taskId: 'task-approved' })
|
|
).resolves.toEqual({
|
|
memberNames: ['alice'],
|
|
fallbackTeamWide: false,
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('targets lead oversight when the changed task is a self-review', async () => {
|
|
const tasks: TeamTask[] = [
|
|
{
|
|
id: 'task-self-review',
|
|
subject: 'Self review',
|
|
status: 'completed',
|
|
owner: 'alice',
|
|
reviewState: 'review',
|
|
historyEvents: [
|
|
{
|
|
id: 'evt-self-review',
|
|
type: 'review_requested',
|
|
timestamp: '2026-05-06T19:00:00.000Z',
|
|
from: 'none',
|
|
to: 'review',
|
|
reviewer: 'alice',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
const resolver = new MemberWorkSyncTaskImpactResolver({
|
|
taskReader: { getTasks: vi.fn(async () => tasks) },
|
|
kanbanManager: { getState: vi.fn(async () => ({ tasks: {} })) },
|
|
activeMemberSource: {
|
|
loadActiveMemberNames: vi.fn(async () => ['alice', 'team-lead']),
|
|
},
|
|
} as never);
|
|
|
|
await expect(
|
|
resolver.resolve({ teamName: 'team-a', taskId: 'task-self-review' })
|
|
).resolves.toEqual({
|
|
memberNames: ['alice', 'team-lead'],
|
|
fallbackTeamWide: false,
|
|
diagnostics: ['self_review_invalid'],
|
|
});
|
|
});
|
|
});
|