## Sidebar Enhancement - Change tabs to icon + small label layout for better space utilization - Add Teams tab with team collaboration entry point ## Settings Page Improvements - Connect theme toggle to gatewayStore.saveQuickConfig for persistence - Remove OpenFang backend download section, simplify UI - Add time range filter to UsageStats (7d/30d/all) - Add stat cards with icons (sessions, messages, input/output tokens) - Add token usage overview bar chart - Add 8 ZCLAW system skill definitions with categories ## Bug Fixes - Fix ChannelList duplicate content with deduplication logic - Integrate CreateTriggerModal in TriggersPanel - Add independent SecurityStatusPanel with 12 default enabled layers - Change workflow view to use SchedulerPanel as unified entry ## New Components - CreateTriggerModal: Event trigger creation modal - HandApprovalModal: Hand approval workflow dialog - HandParamsForm: Enhanced Hand parameter form - SecurityLayersPanel: 16-layer security status display ## Architecture - Add TOML config parsing support (toml-utils.ts, config-parser.ts) - Add request timeout and retry mechanism (request-helper.ts) - Add secure token storage (secure-storage.ts, secure_storage.rs) ## Tests - Add unit tests for config-parser, toml-utils, request-helper - Add team-client and teamStore tests ## Documentation - Update SYSTEM_ANALYSIS.md with Phase 8 completion - UI completion: 100% (30/30 components) - API coverage: 93% (63/68 endpoints) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
565 lines
16 KiB
TypeScript
565 lines
16 KiB
TypeScript
/**
|
|
* 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).toHaveBeenCalledWith('/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).toHaveBeenCalledWith('/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('/api/teams/team-1/members');
|
|
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('/api/teams/team-1/members/member-1');
|
|
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('/api/teams/team-1/members/member-1');
|
|
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('/api/teams/team-1/tasks');
|
|
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('/api/teams/team-1/tasks/task-1');
|
|
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('/api/teams/team-1/tasks/task-1/assign');
|
|
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('/api/teams/team-1/tasks/task-1/deliverable');
|
|
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('/api/teams/team-1/loops');
|
|
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('/api/teams/team-1/loops/loop-1/review');
|
|
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('/api/teams/team-1/loops/loop-1');
|
|
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).toHaveBeenCalledWith('/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).toHaveBeenCalledWith('/api/teams/team-1/events?limit=10');
|
|
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.removeEventListenerEventListener).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);
|
|
});
|
|
});
|
|
});
|