/** * Team Client Tests * * Tests for OpenFang Team API client. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { TeamAPIError, listTeams, getTeam, createTeam, updateTeam, deleteTeam, addTeamMember, removeTeamMember, updateMemberRole, addTeamTask, updateTaskStatus, assignTask, submitDeliverable, startDevQALoop, submitReview, updateLoopState, getTeamMetrics, getTeamEvents, subscribeToTeamEvents, teamClient, } from '../../src/lib/team-client'; import type { Team, TeamMember, TeamTask, TeamMemberRole, DevQALoop } from '../../src/types/team'; // Mock fetch globally const mockFetch = vi.fn(); const originalFetch = global.fetch; describe('team-client', () => { beforeEach(() => { global.fetch = mockFetch; mockFetch.mockClear(); }); afterEach(() => { global.fetch = originalFetch; mockFetch.mockReset(); }); describe('TeamAPIError', () => { it('should create error with all properties', () => { const error = new TeamAPIError('Test error', 404, '/teams/test', { detail: 'test detail' }); expect(error.message).toBe('Test error'); expect(error.statusCode).toBe(404); expect(error.endpoint).toBe('/teams/test'); expect(error.details).toEqual({ detail: 'test detail' }); }); }); describe('listTeams', () => { it('should fetch teams list', async () => { const mockTeams: Team[] = [ { id: 'team-1', name: 'Test Team', members: [], tasks: [], pattern: 'sequential', activeLoops: [], status: 'active', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', }, ]; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => ({ teams: mockTeams, total: 1 }), }); const result = await listTeams(); expect(mockFetch.mock.calls[0][0]).toContain('/api/teams'); expect(result).toEqual({ teams: mockTeams, total: 1 }); }); }); describe('getTeam', () => { it('should fetch single team', async () => { const mockTeam = { team: { id: 'team-1', name: 'Test Team', members: [], tasks: [], pattern: 'sequential' as const, activeLoops: [], status: 'active', createdAt: '2024-01-01T00:00:00Z', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockTeam, }); const result = await getTeam('team-1'); expect(mockFetch.mock.calls[0][0]).toContain('/api/teams/team-1'); expect(result).toEqual(mockTeam); }); }); describe('createTeam', () => { it('should create a new team', async () => { const createRequest = { name: 'New Team', description: 'Test description', memberAgents: [], pattern: 'parallel' as const, }; const mockResponse = { team: { id: 'new-team-id', name: 'New Team', description: 'Test description', members: [], tasks: [], pattern: 'parallel', activeLoops: [], status: 'active', createdAt: '2024-01-01T00:00:00Z', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 201, statusText: 'Created', json: async () => mockResponse, }); const result = await createTeam(createRequest); expect(mockFetch).toHaveBeenCalledWith('/api/teams', expect.objectContaining({ method: 'POST', })); expect(result).toEqual(mockResponse); }); }); describe('updateTeam', () => { it('should update a team', async () => { const updateData = { name: 'Updated Team' }; const mockResponse = { team: { id: 'team-1', name: 'Updated Team', description: 'Test', members: [], tasks: [], pattern: 'sequential' as const, activeLoops: [], status: 'active', createdAt: '2024-01-01T00:00:00Z', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await updateTeam('team-1', updateData); expect(mockFetch).toHaveBeenCalledWith('/api/teams/team-1', expect.objectContaining({ method: 'PUT', })); expect(result).toEqual(mockResponse); }); }); describe('deleteTeam', () => { it('should delete a team', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => ({ success: true }), }); const result = await deleteTeam('team-1'); expect(mockFetch).toHaveBeenCalledWith('/api/teams/team-1', expect.objectContaining({ method: 'DELETE', })); expect(result).toEqual({ success: true }); }); }); describe('addTeamMember', () => { it('should add a member to team', async () => { const mockResponse = { member: { id: 'member-1', agentId: 'agent-1', name: 'Agent 1', role: 'developer', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [], }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await addTeamMember('team-1', 'agent-1', 'developer'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/members'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('removeTeamMember', () => { it('should remove a member from team', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => ({ success: true }), }); const result = await removeTeamMember('team-1', 'member-1'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/members/member-1'), expect.objectContaining({ method: 'DELETE' }) ); expect(result).toEqual({ success: true }); }); }); describe('updateMemberRole', () => { it('should update member role', async () => { const mockResponse = { member: { id: 'member-1', agentId: 'agent-1', name: 'Agent 1', role: 'reviewer', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [], }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await updateMemberRole('team-1', 'member-1', 'reviewer'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/members/member-1'), expect.objectContaining({ method: 'PUT' }) ); expect(result).toEqual(mockResponse); }); }); describe('addTeamTask', () => { it('should add a task to team', async () => { const taskRequest = { teamId: 'team-1', title: 'Test Task', description: 'Test task description', priority: 'high', type: 'implementation', }; const mockResponse = { task: { id: 'task-1', title: 'Test Task', description: 'Test task description', status: 'pending', priority: 'high', dependencies: [], type: 'implementation', createdAt: '2024-01-01T00:00:00Z', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 201, statusText: 'Created', json: async () => mockResponse, }); const result = await addTeamTask(taskRequest); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/tasks'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('updateTaskStatus', () => { it('should update task status', async () => { const mockResponse = { task: { id: 'task-1', title: 'Test Task', status: 'in_progress', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await updateTaskStatus('team-1', 'task-1', 'in_progress'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/tasks/task-1'), expect.objectContaining({ method: 'PUT' }) ); expect(result).toEqual(mockResponse); }); }); describe('assignTask', () => { it('should assign task to member', async () => { const mockResponse = { task: { id: 'task-1', title: 'Test Task', assigneeId: 'member-1', status: 'assigned', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await assignTask('team-1', 'task-1', 'member-1'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/tasks/task-1/assign'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('submitDeliverable', () => { it('should submit deliverable for task', async () => { const deliverable = { type: 'code', description: 'Test deliverable', files: ['/test/file.ts'], }; const mockResponse = { task: { id: 'task-1', title: 'Test Task', deliverable, status: 'review', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await submitDeliverable('team-1', 'task-1', deliverable); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/tasks/task-1/deliverable'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('startDevQALoop', () => { it('should start a DevQA loop', async () => { const mockResponse = { loop: { id: 'loop-1', developerId: 'dev-1', reviewerId: 'reviewer-1', taskId: 'task-1', state: 'developing', iterationCount: 0, maxIterations: 3, feedbackHistory: [], startedAt: '2024-01-01T00:00:00Z', lastUpdatedAt: '2024-01-01T00:00:00Z', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 201, statusText: 'Created', json: async () => mockResponse, }); const result = await startDevQALoop('team-1', 'task-1', 'dev-1', 'reviewer-1'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/loops'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('submitReview', () => { it('should submit a review', async () => { const feedback = { verdict: 'approved', comments: ['LGTM!'], issues: [], }; const mockResponse = { loop: { id: 'loop-1', state: 'approved', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await submitReview('team-1', 'loop-1', feedback); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/loops/loop-1/review'), expect.objectContaining({ method: 'POST' }) ); expect(result).toEqual(mockResponse); }); }); describe('updateLoopState', () => { it('should update loop state', async () => { const mockResponse = { loop: { id: 'loop-1', state: 'reviewing', }, success: true, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockResponse, }); const result = await updateLoopState('team-1', 'loop-1', 'reviewing'); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/api/teams/team-1/loops/loop-1'), expect.objectContaining({ method: 'PUT' }) ); expect(result).toEqual(mockResponse); }); }); describe('getTeamMetrics', () => { it('should get team metrics', async () => { const mockMetrics = { tasksCompleted: 10, avgCompletionTime: 1000, passRate: 85, avgIterations: 1.5, escalations: 0, efficiency: 80, }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => mockMetrics, }); const result = await getTeamMetrics('team-1'); expect(mockFetch.mock.calls[0][0]).toContain('/api/teams/team-1/metrics'); expect(result).toEqual(mockMetrics); }); }); describe('getTeamEvents', () => { it('should get team events', async () => { const mockEvents = [ { type: 'task_assigned', teamId: 'team-1', sourceAgentId: 'agent-1', payload: {}, timestamp: '2024-01-01T00:00:00Z', }, ]; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: async () => ({ events: mockEvents, total: 1 }), }); const result = await getTeamEvents('team-1', 10); expect(mockFetch.mock.calls[0][0]).toContain('/api/teams/team-1/events'); expect(result).toEqual({ events: mockEvents, total: 1 }); }); }); describe('subscribeToTeamEvents', () => { it('should subscribe to team events', () => { const mockWs = { readyState: WebSocket.OPEN, send: vi.fn(), addEventListener: vi.fn((event, handler) => { handler('message'); return mockWs; }), removeEventListener: vi.fn(), }; const callback = vi.fn(); const unsubscribe = subscribeToTeamEvents('team-1', callback, mockWs as unknown as WebSocket); expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify({ type: 'subscribe', topic: 'team:team-1', })); unsubscribe(); expect(mockWs.removeEventListener).toHaveBeenCalled(); expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify({ type: 'unsubscribe', topic: 'team:team-1', })); }); }); describe('teamClient', () => { it('should export all API functions', () => { expect(teamClient.listTeams).toBe(listTeams); expect(teamClient.getTeam).toBe(getTeam); expect(teamClient.createTeam).toBe(createTeam); expect(teamClient.updateTeam).toBe(updateTeam); expect(teamClient.deleteTeam).toBe(deleteTeam); expect(teamClient.addTeamMember).toBe(addTeamMember); expect(teamClient.removeTeamMember).toBe(removeTeamMember); expect(teamClient.updateMemberRole).toBe(updateMemberRole); expect(teamClient.addTeamTask).toBe(addTeamTask); expect(teamClient.updateTaskStatus).toBe(updateTaskStatus); expect(teamClient.assignTask).toBe(assignTask); expect(teamClient.submitDeliverable).toBe(submitDeliverable); expect(teamClient.startDevQALoop).toBe(startDevQALoop); expect(teamClient.submitReview).toBe(submitReview); expect(teamClient.updateLoopState).toBe(updateLoopState); expect(teamClient.getTeamMetrics).toBe(getTeamMetrics); expect(teamClient.getTeamEvents).toBe(getTeamEvents); expect(teamClient.subscribeToTeamEvents).toBe(subscribeToTeamEvents); }); }); });