Files
zclaw_openfang/tests/desktop/teamStore.test.ts
iven bf79c06d4a 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>
2026-03-15 09:26:08 +08:00

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();
});
});
});