- Add 16 test cases covering team CRUD operations - Add tests for member and task management - Add tests for Dev↔QA loop workflow - Add tests for event management with max 100 limit - Update SYSTEM_ANALYSIS.md with TypeScript fix completion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
518 lines
15 KiB
TypeScript
518 lines
15 KiB
TypeScript
/**
|
|
* Team Store Tests
|
|
*
|
|
* Unit tests for multi-agent team collaboration state management.
|
|
*
|
|
* @module tests/desktop/teamStore.test
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
// Mock localStorage
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {};
|
|
return {
|
|
getItem: vi.fn((key: string) => store[key] || null),
|
|
setItem: vi.fn((key: string, value: string) => {
|
|
store[key] = value;
|
|
}),
|
|
removeItem: vi.fn((key: string) => {
|
|
delete store[key];
|
|
}),
|
|
clear: vi.fn(() => {
|
|
store = {};
|
|
}),
|
|
};
|
|
})();
|
|
|
|
Object.defineProperty(global, 'localStorage', {
|
|
value: localStorageMock,
|
|
});
|
|
|
|
// Import store after mocking
|
|
import { useTeamStore } from '../../desktop/src/store/teamStore';
|
|
import type { Team, TeamMember, TeamTask, DevQALoop } from '../../desktop/src/types/team';
|
|
|
|
describe('TeamStore', () => {
|
|
beforeEach(() => {
|
|
// Reset store state and localStorage
|
|
localStorageMock.clear();
|
|
useTeamStore.setState({
|
|
teams: [],
|
|
activeTeam: null,
|
|
metrics: null,
|
|
isLoading: false,
|
|
error: null,
|
|
selectedTaskId: null,
|
|
selectedMemberId: null,
|
|
recentEvents: [],
|
|
});
|
|
});
|
|
|
|
describe('loadTeams', () => {
|
|
it('should load teams from localStorage', async () => {
|
|
const mockTeams: Team[] = [
|
|
{
|
|
id: 'team-1',
|
|
name: 'Test Team',
|
|
description: 'A test team',
|
|
status: 'active',
|
|
pattern: 'sequential',
|
|
members: [],
|
|
tasks: [],
|
|
loops: [],
|
|
createdAt: '2026-03-15T00:00:00Z',
|
|
},
|
|
];
|
|
|
|
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(mockTeams));
|
|
|
|
const { loadTeams } = useTeamStore.getState();
|
|
await loadTeams();
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams).toHaveLength(1);
|
|
expect(state.teams[0].name).toBe('Test Team');
|
|
});
|
|
|
|
it('should handle empty localStorage', async () => {
|
|
localStorageMock.getItem.mockReturnValueOnce(null);
|
|
|
|
const { loadTeams } = useTeamStore.getState();
|
|
await loadTeams();
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('createTeam', () => {
|
|
it('should create a new team with members', async () => {
|
|
const request = {
|
|
name: 'New Team',
|
|
description: 'A new team for testing',
|
|
pattern: 'parallel' as const,
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'orchestrator' as const },
|
|
{ agentId: 'agent-2', role: 'developer' as const },
|
|
],
|
|
};
|
|
|
|
const { createTeam } = useTeamStore.getState();
|
|
const team = await createTeam(request);
|
|
|
|
expect(team).not.toBeNull();
|
|
expect(team?.name).toBe('New Team');
|
|
expect(team?.pattern).toBe('parallel');
|
|
expect(team?.members).toHaveLength(2);
|
|
expect(team?.members[0].role).toBe('orchestrator');
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('setActiveTeam', () => {
|
|
it('should set active team and calculate metrics', () => {
|
|
const mockTeam: Team = {
|
|
id: 'team-1',
|
|
name: 'Test Team',
|
|
description: 'A test team',
|
|
status: 'active',
|
|
pattern: 'sequential',
|
|
members: [
|
|
{
|
|
id: 'member-1',
|
|
agentId: 'agent-1',
|
|
name: 'Developer 1',
|
|
role: 'developer',
|
|
status: 'idle',
|
|
skills: [],
|
|
workload: 0,
|
|
currentTasks: [],
|
|
maxConcurrentTasks: 2,
|
|
},
|
|
],
|
|
tasks: [
|
|
{
|
|
id: 'task-1',
|
|
title: 'Completed Task',
|
|
description: 'A completed task',
|
|
status: 'completed',
|
|
priority: 'medium',
|
|
type: 'feature',
|
|
assigneeId: 'member-1',
|
|
createdAt: '2026-03-15T00:00:00Z',
|
|
updatedAt: '2026-03-15T01:00:00Z',
|
|
},
|
|
],
|
|
activeLoops: [],
|
|
createdAt: '2026-03-15T00:00:00Z',
|
|
};
|
|
|
|
const { setActiveTeam } = useTeamStore.getState();
|
|
setActiveTeam(mockTeam);
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.activeTeam).toEqual(mockTeam);
|
|
expect(state.metrics).not.toBeNull();
|
|
expect(state.metrics?.tasksCompleted).toBe(1);
|
|
});
|
|
|
|
it('should clear active team when passed null', () => {
|
|
const { setActiveTeam } = useTeamStore.getState();
|
|
|
|
// First set a team
|
|
setActiveTeam({
|
|
id: 'team-1',
|
|
name: 'Test',
|
|
description: '',
|
|
status: 'active',
|
|
pattern: 'sequential',
|
|
members: [],
|
|
tasks: [],
|
|
activeLoops: [],
|
|
createdAt: '2026-03-15T00:00:00Z',
|
|
});
|
|
|
|
// Then clear it
|
|
setActiveTeam(null);
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.activeTeam).toBeNull();
|
|
expect(state.metrics).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('addMember', () => {
|
|
it('should add a member to a team', async () => {
|
|
// Setup: create a team first
|
|
const { createTeam, addMember } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'orchestrator' }],
|
|
});
|
|
|
|
const teamId = useTeamStore.getState().teams[0].id;
|
|
const member = await addMember(teamId, 'agent-2', 'developer');
|
|
|
|
expect(member).not.toBeNull();
|
|
expect(member?.role).toBe('developer');
|
|
expect(member?.agentId).toBe('agent-2');
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams[0].members).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('addTask', () => {
|
|
it('should add a task to a team', async () => {
|
|
const { createTeam, addTask } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'orchestrator' }],
|
|
});
|
|
|
|
const teamId = useTeamStore.getState().teams[0].id;
|
|
|
|
const task = await addTask({
|
|
teamId,
|
|
title: 'New Task',
|
|
description: 'A new task for testing',
|
|
priority: 'high',
|
|
type: 'feature',
|
|
});
|
|
|
|
expect(task).not.toBeNull();
|
|
expect(task?.title).toBe('New Task');
|
|
expect(task?.status).toBe('pending');
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams[0].tasks).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('updateTaskStatus', () => {
|
|
it('should update task status', async () => {
|
|
const { createTeam, addTask, updateTaskStatus } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'orchestrator' }],
|
|
});
|
|
|
|
const teamId = useTeamStore.getState().teams[0].id;
|
|
|
|
await addTask({
|
|
teamId,
|
|
title: 'Test Task',
|
|
description: '',
|
|
priority: 'medium',
|
|
type: 'feature',
|
|
});
|
|
|
|
const taskId = useTeamStore.getState().teams[0].tasks[0].id;
|
|
|
|
await updateTaskStatus(teamId, taskId, 'in_progress');
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams[0].tasks[0].status).toBe('in_progress');
|
|
});
|
|
});
|
|
|
|
describe('assignTask', () => {
|
|
it('should assign task to a member', async () => {
|
|
const { createTeam, addTask, assignTask } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'orchestrator' },
|
|
{ agentId: 'agent-2', role: 'developer' },
|
|
],
|
|
});
|
|
|
|
const state1 = useTeamStore.getState();
|
|
const teamId = state1.teams[0].id;
|
|
const memberId = state1.teams[0].members[1].id;
|
|
|
|
await addTask({
|
|
teamId,
|
|
title: 'Test Task',
|
|
description: '',
|
|
priority: 'medium',
|
|
type: 'feature',
|
|
});
|
|
|
|
const taskId = useTeamStore.getState().teams[0].tasks[0].id;
|
|
|
|
await assignTask(teamId, taskId, memberId);
|
|
|
|
const state2 = useTeamStore.getState();
|
|
expect(state2.teams[0].tasks[0].assigneeId).toBe(memberId);
|
|
expect(state2.teams[0].tasks[0].status).toBe('assigned');
|
|
});
|
|
});
|
|
|
|
describe('startDevQALoop', () => {
|
|
it('should start a Dev↔QA loop', async () => {
|
|
const { createTeam, addTask, startDevQALoop } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'review_loop',
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'orchestrator' },
|
|
{ agentId: 'agent-2', role: 'developer' },
|
|
{ agentId: 'agent-3', role: 'reviewer' },
|
|
],
|
|
});
|
|
|
|
const state1 = useTeamStore.getState();
|
|
const teamId = state1.teams[0].id;
|
|
const developerId = state1.teams[0].members[1].id;
|
|
const reviewerId = state1.teams[0].members[2].id;
|
|
|
|
await addTask({
|
|
teamId,
|
|
title: 'Code Task',
|
|
description: '',
|
|
priority: 'high',
|
|
type: 'feature',
|
|
});
|
|
|
|
const taskId = useTeamStore.getState().teams[0].tasks[0].id;
|
|
|
|
const loop = await startDevQALoop(teamId, taskId, developerId, reviewerId);
|
|
|
|
expect(loop).not.toBeNull();
|
|
expect(loop?.state).toBe('developing');
|
|
expect(loop?.developerId).toBe(developerId);
|
|
expect(loop?.reviewerId).toBe(reviewerId);
|
|
|
|
const state2 = useTeamStore.getState();
|
|
expect(state2.teams[0].activeLoops).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('submitReview', () => {
|
|
it('should submit review feedback', async () => {
|
|
const { createTeam, addTask, startDevQALoop, submitReview } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'review_loop',
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'orchestrator' },
|
|
{ agentId: 'agent-2', role: 'developer' },
|
|
{ agentId: 'agent-3', role: 'reviewer' },
|
|
],
|
|
});
|
|
|
|
const state1 = useTeamStore.getState();
|
|
const teamId = state1.teams[0].id;
|
|
const developerId = state1.teams[0].members[1].id;
|
|
const reviewerId = state1.teams[0].members[2].id;
|
|
|
|
await addTask({
|
|
teamId,
|
|
title: 'Code Task',
|
|
description: '',
|
|
priority: 'high',
|
|
type: 'feature',
|
|
});
|
|
|
|
const taskId = useTeamStore.getState().teams[0].tasks[0].id;
|
|
await startDevQALoop(teamId, taskId, developerId, reviewerId);
|
|
|
|
const loopId = useTeamStore.getState().teams[0].activeLoops[0].id;
|
|
|
|
await submitReview(teamId, loopId, {
|
|
verdict: 'needs_work',
|
|
comments: ['Please fix the bug'],
|
|
issues: [
|
|
{ severity: 'major', description: 'Null pointer exception', file: 'main.ts', line: 42 },
|
|
],
|
|
});
|
|
|
|
const state2 = useTeamStore.getState();
|
|
expect(state2.teams[0].activeLoops[0].state).toBe('revising');
|
|
expect(state2.teams[0].activeLoops[0].feedbackHistory).toHaveLength(1);
|
|
});
|
|
|
|
it('should approve task when verdict is approved', async () => {
|
|
const { createTeam, addTask, startDevQALoop, submitReview } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Test Team',
|
|
description: '',
|
|
pattern: 'review_loop',
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'orchestrator' },
|
|
{ agentId: 'agent-2', role: 'developer' },
|
|
{ agentId: 'agent-3', role: 'reviewer' },
|
|
],
|
|
});
|
|
|
|
const state1 = useTeamStore.getState();
|
|
const teamId = state1.teams[0].id;
|
|
const developerId = state1.teams[0].members[1].id;
|
|
const reviewerId = state1.teams[0].members[2].id;
|
|
|
|
await addTask({
|
|
teamId,
|
|
title: 'Code Task',
|
|
description: '',
|
|
priority: 'high',
|
|
type: 'feature',
|
|
});
|
|
|
|
const taskId = useTeamStore.getState().teams[0].tasks[0].id;
|
|
await startDevQALoop(teamId, taskId, developerId, reviewerId);
|
|
|
|
const loopId = useTeamStore.getState().teams[0].activeLoops[0].id;
|
|
|
|
await submitReview(teamId, loopId, {
|
|
verdict: 'approved',
|
|
comments: ['Great work!'],
|
|
issues: [],
|
|
});
|
|
|
|
const state2 = useTeamStore.getState();
|
|
expect(state2.teams[0].activeLoops[0].state).toBe('approved');
|
|
});
|
|
});
|
|
|
|
describe('addEvent', () => {
|
|
it('should add collaboration event', () => {
|
|
const { addEvent } = useTeamStore.getState();
|
|
|
|
addEvent({
|
|
type: 'task_assigned',
|
|
teamId: 'team-1',
|
|
sourceAgentId: 'agent-1',
|
|
payload: { taskId: 'task-1' },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.recentEvents).toHaveLength(1);
|
|
expect(state.recentEvents[0].type).toBe('task_assigned');
|
|
});
|
|
|
|
it('should limit events to max 100', () => {
|
|
const { addEvent } = useTeamStore.getState();
|
|
|
|
// Add 105 events
|
|
for (let i = 0; i < 105; i++) {
|
|
addEvent({
|
|
type: 'task_assigned',
|
|
teamId: 'team-1',
|
|
sourceAgentId: 'agent-1',
|
|
payload: { index: i },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.recentEvents).toHaveLength(100);
|
|
// Most recent event should be at index 0
|
|
expect(state.recentEvents[0].payload.index).toBe(104);
|
|
// Oldest kept event should be at index 99 (events 0-4 are discarded)
|
|
expect(state.recentEvents[99].payload.index).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('deleteTeam', () => {
|
|
it('should delete a team', async () => {
|
|
const { createTeam, deleteTeam } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Team to Delete',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'orchestrator' }],
|
|
});
|
|
|
|
const teamId = useTeamStore.getState().teams[0].id;
|
|
|
|
await deleteTeam(teamId);
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.teams).toHaveLength(0);
|
|
});
|
|
|
|
it('should clear active team if deleted', async () => {
|
|
const { createTeam, setActiveTeam, deleteTeam } = useTeamStore.getState();
|
|
|
|
await createTeam({
|
|
name: 'Team to Delete',
|
|
description: '',
|
|
pattern: 'sequential',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'orchestrator' }],
|
|
});
|
|
|
|
const teamId = useTeamStore.getState().teams[0].id;
|
|
const team = useTeamStore.getState().teams[0];
|
|
|
|
setActiveTeam(team);
|
|
await deleteTeam(teamId);
|
|
|
|
const state = useTeamStore.getState();
|
|
expect(state.activeTeam).toBeNull();
|
|
});
|
|
});
|
|
});
|