- Replaced legacy references with new runtimeHelpers in context, processStore, tasks, and review modules for improved maintainability. - Introduced leadSessionId handling in review requests and approvals, ensuring consistent tracking of session identifiers across operations. - Added new utility functions in runtimeHelpers for path management and process checks, streamlining task-related functionalities. - Updated tests to validate the inclusion of leadSessionId in various workflows, enhancing the robustness of the messaging system.
460 lines
12 KiB
TypeScript
460 lines
12 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
|
|
|
|
import type { TeamTask } from '../../../../src/shared/types/team';
|
|
|
|
describe('TeamDataService', () => {
|
|
it('keeps getTeamData read-only and skips kanban garbage-collect', async () => {
|
|
const order: string[] = [];
|
|
const tasks: TeamTask[] = [
|
|
{
|
|
id: '12',
|
|
subject: 'Task',
|
|
status: 'pending',
|
|
},
|
|
];
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [] })),
|
|
} as never,
|
|
{
|
|
getTasks: vi.fn(async () => {
|
|
order.push('tasks');
|
|
return tasks;
|
|
}),
|
|
} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => []),
|
|
getMessages: vi.fn(async () => []),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => {
|
|
order.push('gc');
|
|
}),
|
|
} as never
|
|
);
|
|
|
|
await service.getTeamData('my-team');
|
|
expect(order).toEqual(['tasks']);
|
|
});
|
|
|
|
it('delegates explicit reconcile to controller maintenance API', async () => {
|
|
const reconcileArtifacts = vi.fn();
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [{ name: 'team-lead', role: 'Lead' }] })),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{
|
|
readMembers: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
readMessages: vi.fn(async () => []),
|
|
} as never,
|
|
() =>
|
|
({
|
|
maintenance: {
|
|
reconcileArtifacts,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
await service.reconcileTeamArtifacts('my-team');
|
|
expect(reconcileArtifacts).toHaveBeenCalledWith({ reason: 'file-watch' });
|
|
});
|
|
|
|
it('surfaces controller reconcile failures', async () => {
|
|
const reconcileArtifacts = vi.fn(() => {
|
|
throw new Error('reconcile failed');
|
|
});
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [] })),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
() =>
|
|
({
|
|
maintenance: {
|
|
reconcileArtifacts,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
await expect(service.reconcileTeamArtifacts('my-team')).rejects.toThrow('reconcile failed');
|
|
});
|
|
|
|
it('writes UI task comments with author user', async () => {
|
|
const addTaskComment = vi.fn(() => ({
|
|
comment: {
|
|
id: 'comment-1',
|
|
author: 'user',
|
|
text: 'Need clarification',
|
|
createdAt: '2026-03-07T20:00:00.000Z',
|
|
type: 'regular',
|
|
},
|
|
task: {
|
|
id: 'task-1',
|
|
subject: 'Investigate',
|
|
status: 'pending',
|
|
owner: 'team-lead',
|
|
},
|
|
}));
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [{ name: 'team-lead', role: 'Lead' }] })),
|
|
} as never,
|
|
{
|
|
getTasks: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => []),
|
|
getMessages: vi.fn(async () => []),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
() =>
|
|
({
|
|
tasks: {
|
|
addTaskComment,
|
|
setNeedsClarification: vi.fn(),
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
await service.addTaskComment('my-team', 'task-1', 'Need clarification');
|
|
|
|
expect(addTaskComment).toHaveBeenCalledWith('task-1', {
|
|
from: 'user',
|
|
text: 'Need clarification',
|
|
attachments: undefined,
|
|
});
|
|
});
|
|
|
|
it('includes projectPath from config when creating a task', async () => {
|
|
const createTaskMock = vi.fn((task) => task);
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({
|
|
name: 'My team',
|
|
members: [],
|
|
projectPath: '/Users/dev/my-project',
|
|
})),
|
|
} as never,
|
|
{
|
|
getNextTaskId: vi.fn(async () => '1'),
|
|
getTasks: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => []),
|
|
getMessages: vi.fn(async () => []),
|
|
} as never,
|
|
{} as never,
|
|
{
|
|
createTask: createTaskMock,
|
|
addBlocksEntry: vi.fn(async () => undefined),
|
|
} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
(teamName: string) =>
|
|
({
|
|
tasks: {
|
|
createTask: createTaskMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
const result = await service.createTask('my-team', { subject: 'Test' });
|
|
|
|
expect(result.projectPath).toBe('/Users/dev/my-project');
|
|
expect(createTaskMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ projectPath: '/Users/dev/my-project' })
|
|
);
|
|
});
|
|
|
|
it('creates task with status pending when startImmediately is false', async () => {
|
|
const createTaskMock = vi.fn((task) => task);
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [] })),
|
|
} as never,
|
|
{
|
|
getNextTaskId: vi.fn(async () => '2'),
|
|
getTasks: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => []),
|
|
getMessages: vi.fn(async () => []),
|
|
} as never,
|
|
{} as never,
|
|
{
|
|
createTask: createTaskMock,
|
|
addBlocksEntry: vi.fn(async () => undefined),
|
|
} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
(teamName: string) =>
|
|
({
|
|
tasks: {
|
|
createTask: createTaskMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
const result = await service.createTask('my-team', {
|
|
subject: 'Review main file',
|
|
owner: 'alice',
|
|
startImmediately: false,
|
|
});
|
|
|
|
expect(result.status).toBe('pending');
|
|
expect(createTaskMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ status: 'pending', owner: 'alice', createdBy: 'user' })
|
|
);
|
|
});
|
|
|
|
it('persists explicit related task links when creating a task', async () => {
|
|
const createTaskMock = vi.fn((task) => task);
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [] })),
|
|
} as never,
|
|
{
|
|
getNextTaskId: vi.fn(async () => '3'),
|
|
getTasks: vi.fn(async () => []),
|
|
} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => []),
|
|
getMessages: vi.fn(async () => []),
|
|
} as never,
|
|
{} as never,
|
|
{
|
|
createTask: createTaskMock,
|
|
addBlocksEntry: vi.fn(async () => undefined),
|
|
} as never,
|
|
{
|
|
resolveMembers: vi.fn(() => []),
|
|
} as never,
|
|
{
|
|
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
|
|
garbageCollect: vi.fn(async () => undefined),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
(teamName: string) =>
|
|
({
|
|
tasks: {
|
|
createTask: createTaskMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
const result = await service.createTask('my-team', {
|
|
subject: 'Review work task',
|
|
related: ['1', '2'],
|
|
});
|
|
|
|
expect(result.related).toEqual(['1', '2']);
|
|
expect(createTaskMock).toHaveBeenCalledWith(expect.objectContaining({ related: ['1', '2'] }));
|
|
});
|
|
|
|
it('routes durable inbox writes through controller message API', async () => {
|
|
const sendMessageMock = vi.fn(() => ({ deliveredToInbox: true, messageId: 'm-1' }));
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({ name: 'My team', members: [], leadSessionId: 'lead-1' })),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
() =>
|
|
({
|
|
messages: {
|
|
sendMessage: sendMessageMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
const result = await service.sendMessage('my-team', {
|
|
member: 'alice',
|
|
text: 'hello',
|
|
summary: 'ping',
|
|
});
|
|
|
|
expect(result).toEqual({ deliveredToInbox: true, messageId: 'm-1' });
|
|
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
member: 'alice',
|
|
text: 'hello',
|
|
summary: 'ping',
|
|
leadSessionId: 'lead-1',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('delegates review entry to controller review API', async () => {
|
|
const requestReviewMock = vi.fn();
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({
|
|
name: 'My team',
|
|
members: [{ name: 'lead', role: 'team lead' }],
|
|
leadSessionId: 'lead-1',
|
|
})),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
() =>
|
|
({
|
|
review: {
|
|
requestReview: requestReviewMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
await service.requestReview('my-team', 'task-1');
|
|
|
|
expect(requestReviewMock).toHaveBeenCalledWith('task-1', {
|
|
from: 'lead',
|
|
leadSessionId: 'lead-1',
|
|
});
|
|
});
|
|
|
|
it('propagates leadSessionId for kanban-driven review transitions', async () => {
|
|
const requestReviewMock = vi.fn();
|
|
const approveReviewMock = vi.fn();
|
|
const requestChangesMock = vi.fn();
|
|
|
|
const service = new TeamDataService(
|
|
{
|
|
listTeams: vi.fn(),
|
|
getConfig: vi.fn(async () => ({
|
|
name: 'My team',
|
|
members: [{ name: 'lead', role: 'team lead' }],
|
|
leadSessionId: 'lead-2',
|
|
})),
|
|
} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
() =>
|
|
({
|
|
review: {
|
|
requestReview: requestReviewMock,
|
|
approveReview: approveReviewMock,
|
|
requestChanges: requestChangesMock,
|
|
},
|
|
}) as never
|
|
);
|
|
|
|
await service.updateKanban('my-team', 'task-1', { op: 'set_column', column: 'review' });
|
|
await service.updateKanban('my-team', 'task-1', { op: 'set_column', column: 'approved' });
|
|
await service.updateKanban('my-team', 'task-1', { op: 'request_changes', comment: 'Needs fixes' });
|
|
|
|
expect(requestReviewMock).toHaveBeenCalledWith('task-1', {
|
|
from: 'lead',
|
|
leadSessionId: 'lead-2',
|
|
});
|
|
expect(approveReviewMock).toHaveBeenCalledWith('task-1', {
|
|
from: 'lead',
|
|
note: 'Approved from kanban',
|
|
'notify-owner': true,
|
|
leadSessionId: 'lead-2',
|
|
});
|
|
expect(requestChangesMock).toHaveBeenCalledWith('task-1', {
|
|
from: 'lead',
|
|
comment: 'Needs fixes',
|
|
leadSessionId: 'lead-2',
|
|
});
|
|
});
|
|
});
|