test(team): add comprehensive unit tests for teamStore
- 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>
This commit is contained in:
517
tests/desktop/teamStore.test.ts
Normal file
517
tests/desktop/teamStore.test.ts
Normal file
@@ -0,0 +1,517 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user