## 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>
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
/**
|
|
* Team Store Tests
|
|
*
|
|
* Tests for multi-agent team collaboration state management.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { useTeamStore } from '../../src/store/teamStore';
|
|
import type { Team, TeamMember, TeamTask, CreateTeamRequest, AddTeamTaskRequest, TeamMemberRole } from '../../src/types/team';
|
|
import { localStorageMock } from '../../tests/setup';
|
|
|
|
// Mock fetch globally
|
|
const mockFetch = vi.fn();
|
|
const originalFetch = global.fetch;
|
|
|
|
describe('teamStore', () => {
|
|
beforeEach(() => {
|
|
global.fetch = mockFetch;
|
|
mockFetch.mockClear();
|
|
localStorageMock.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
mockFetch.mockReset();
|
|
});
|
|
|
|
describe('Initial State', () => {
|
|
it('should have correct initial state', () => {
|
|
const store = useTeamStore.getState();
|
|
expect(store.teams).toEqual([]);
|
|
expect(store.activeTeam).toBeNull();
|
|
expect(store.metrics).toBeNull();
|
|
expect(store.isLoading).toBe(false);
|
|
expect(store.error).toBeNull();
|
|
expect(store.selectedTaskId).toBeNull();
|
|
expect(store.selectedMemberId).toBeNull();
|
|
expect(store.recentEvents).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('loadTeams', () => {
|
|
it('should load teams from localStorage', 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',
|
|
},
|
|
];
|
|
localStorageMock.setItem('zclaw-teams', JSON.stringify(mockTeams));
|
|
await useTeamStore.getState().loadTeams();
|
|
const store = useTeamStore.getState();
|
|
expect(store.teams).toEqual(mockTeams);
|
|
expect(store.isLoading).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('createTeam', () => {
|
|
it('should create a new team with members', async () => {
|
|
const request: CreateTeamRequest = {
|
|
name: 'Dev Team',
|
|
description: 'Development team',
|
|
memberAgents: [
|
|
{ agentId: 'agent-1', role: 'developer' },
|
|
{ agentId: 'agent-2', role: 'reviewer' },
|
|
],
|
|
pattern: 'review_loop',
|
|
};
|
|
const team = await useTeamStore.getState().createTeam(request);
|
|
expect(team).not.toBeNull();
|
|
expect(team.name).toBe('Dev Team');
|
|
expect(team.description).toBe('Development team');
|
|
expect(team.pattern).toBe('review_loop');
|
|
expect(team.members).toHaveLength(2);
|
|
expect(team.status).toBe('active');
|
|
const store = useTeamStore.getState();
|
|
expect(store.teams).toHaveLength(1);
|
|
expect(store.activeTeam?.id).toBe(team.id);
|
|
// Check localStorage was updated
|
|
const stored = localStorageMock.getItem('zclaw-teams');
|
|
expect(stored).toBeDefined();
|
|
const parsed = JSON.parse(stored!);
|
|
expect(parsed).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('deleteTeam', () => {
|
|
it('should delete a team', async () => {
|
|
// First create a team
|
|
const request: CreateTeamRequest = {
|
|
name: 'Team to Delete',
|
|
memberAgents: [],
|
|
pattern: 'sequential',
|
|
};
|
|
await useTeamStore.getState().createTeam(request);
|
|
// Then delete it
|
|
const result = await useTeamStore.getState().deleteTeam('team-to-delete-id');
|
|
expect(result).toBe(true);
|
|
const store = useTeamStore.getState();
|
|
expect(store.teams.find(t => t.id === 'team-to-delete-id')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('setActiveTeam', () => {
|
|
it('should set active team and () => {
|
|
const team: Team = {
|
|
id: 'team-1',
|
|
name: 'Test Team',
|
|
members: [],
|
|
tasks: [],
|
|
pattern: 'sequential',
|
|
activeLoops: [],
|
|
status: 'active',
|
|
createdAt: '2024-01-01T00:00:00Z',
|
|
};
|
|
useTeamStore.getState().setActiveTeam(team);
|
|
const store = useTeamStore.getState();
|
|
expect(store.activeTeam).toEqual(team);
|
|
expect(store.metrics).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('addMember', () => {
|
|
let team: Team;
|
|
beforeEach(async () => {
|
|
const request: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [],
|
|
pattern: 'sequential',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(request))!;
|
|
});
|
|
it('should add a member to team', async () => {
|
|
const member = await useTeamStore.getState().addMember(team.id, 'agent-1', 'developer');
|
|
expect(member).not.toBeNull();
|
|
expect(member.agentId).toBe('agent-1');
|
|
expect(member.role).toBe('developer');
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
expect(updatedTeam?.members).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('removeMember', () => {
|
|
let team: Team;
|
|
let memberId: string;
|
|
beforeEach(async () => {
|
|
const request: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [{ agentId: 'agent-1', role: 'developer' }],
|
|
pattern: 'sequential',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(request))!;
|
|
memberId = team.members[0].id;
|
|
});
|
|
it('should remove a member from team', async () => {
|
|
const result = await useTeamStore.getState().removeMember(team.id, memberId);
|
|
expect(result).toBe(true);
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
expect(updatedTeam?.members).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('addTask', () => {
|
|
let team: Team;
|
|
beforeEach(async () => {
|
|
const request: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [],
|
|
pattern: 'sequential',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(request))!;
|
|
});
|
|
it('should add a task to team', async () => {
|
|
const taskRequest: AddTeamTaskRequest = {
|
|
teamId: team.id,
|
|
title: 'Test Task',
|
|
description: 'Test task description',
|
|
priority: 'high',
|
|
type: 'implementation',
|
|
};
|
|
const task = await useTeamStore.getState().addTask(taskRequest);
|
|
expect(task).not.toBeNull();
|
|
expect(task.title).toBe('Test Task');
|
|
expect(task.status).toBe('pending');
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
expect(updatedTeam?.tasks).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('updateTaskStatus', () => {
|
|
let team: Team;
|
|
let taskId: string;
|
|
beforeEach(async () => {
|
|
const teamRequest: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [],
|
|
pattern: 'sequential',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(teamRequest))!;
|
|
const taskRequest: AddTeamTaskRequest = {
|
|
teamId: team.id,
|
|
title: 'Test Task',
|
|
priority: 'medium',
|
|
type: 'implementation',
|
|
};
|
|
const task = await useTeamStore.getState().addTask(taskRequest);
|
|
taskId = task!.id;
|
|
});
|
|
it('should update task status to in_progress', async () => {
|
|
const result = await useTeamStore.getState().updateTaskStatus(team.id, taskId, 'in_progress');
|
|
expect(result).toBe(true);
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
const updatedTask = updatedTeam?.tasks.find(t => t.id === taskId);
|
|
expect(updatedTask?.status).toBe('in_progress');
|
|
expect(updatedTask?.startedAt).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('startDevQALoop', () => {
|
|
let team: Team;
|
|
let taskId: string;
|
|
let memberId: string;
|
|
beforeEach(async () => {
|
|
const teamRequest: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [
|
|
{ agentId: 'dev-agent', role: 'developer' },
|
|
{ agentId: 'qa-agent', role: 'reviewer' },
|
|
],
|
|
pattern: 'review_loop',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(teamRequest))!;
|
|
const taskRequest: AddTeamTaskRequest = {
|
|
teamId: team.id,
|
|
title: 'Test Task',
|
|
priority: 'high',
|
|
type: 'implementation',
|
|
assigneeId: team.members[0].id,
|
|
};
|
|
const task = await useTeamStore.getState().addTask(taskRequest);
|
|
taskId = task!.id;
|
|
memberId = team.members[0].id;
|
|
});
|
|
it('should start a Dev-QA loop', async () => {
|
|
const loop = await useTeamStore.getState().startDevQALoop(
|
|
team.id,
|
|
taskId,
|
|
team.members[0].id,
|
|
team.members[1].id
|
|
);
|
|
expect(loop).not.toBeNull();
|
|
expect(loop.state).toBe('developing');
|
|
expect(loop.developerId).toBe(team.members[0].id);
|
|
expect(loop.reviewerId).toBe(team.members[1].id);
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
expect(updatedTeam?.activeLoops).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('submitReview', () => {
|
|
let team: Team;
|
|
let loop: any;
|
|
beforeEach(async () => {
|
|
const teamRequest: CreateTeamRequest = {
|
|
name: 'Test Team',
|
|
memberAgents: [
|
|
{ agentId: 'dev-agent', role: 'developer' },
|
|
{ agentId: 'qa-agent', role: 'reviewer' },
|
|
],
|
|
pattern: 'review_loop',
|
|
};
|
|
team = (await useTeamStore.getState().createTeam(teamRequest))!;
|
|
const taskRequest: AddTeamTaskRequest = {
|
|
teamId: team.id,
|
|
title: 'Test Task',
|
|
priority: 'high',
|
|
type: 'implementation',
|
|
assigneeId: team.members[0].id,
|
|
};
|
|
const task = await useTeamStore.getState().addTask(taskRequest);
|
|
loop = await useTeamStore.getState().startDevQALoop(
|
|
team.id,
|
|
task!.id,
|
|
team.members[0].id,
|
|
team.members[1].id
|
|
);
|
|
});
|
|
it('should submit review and async () => {
|
|
const feedback = {
|
|
verdict: 'approved',
|
|
comments: ['Good work!'],
|
|
issues: [],
|
|
};
|
|
const result = await useTeamStore.getState().submitReview(team.id, loop.id, feedback);
|
|
expect(result).toBe(true);
|
|
const store = useTeamStore.getState();
|
|
const updatedTeam = store.teams.find(t => t.id === team.id);
|
|
const updatedLoop = updatedTeam?.activeLoops.find(l => l.id === loop.id);
|
|
expect(updatedLoop?.state).toBe('approved');
|
|
});
|
|
});
|
|
|
|
describe('addEvent', () => {
|
|
it('should add event to recent events', () => {
|
|
const event = {
|
|
type: 'task_completed',
|
|
teamId: 'team-1',
|
|
sourceAgentId: 'agent-1',
|
|
payload: { taskId: 'task-1' },
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
useTeamStore.getState().addEvent(event);
|
|
const store = useTeamStore.getState();
|
|
expect(store.recentEvents).toHaveLength(1);
|
|
expect(store.recentEvents[0]).toEqual(event);
|
|
});
|
|
});
|
|
|
|
describe('clearEvents', () => {
|
|
it('should clear all events', () => {
|
|
const event = {
|
|
type: 'task_completed',
|
|
teamId: 'team-1',
|
|
sourceAgentId: 'agent-1',
|
|
payload: {},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
useTeamStore.getState().addEvent(event);
|
|
useTeamStore.getState().clearEvents();
|
|
const store = useTeamStore.getState();
|
|
expect(store.recentEvents).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('UI Actions', () => {
|
|
it('should set selected task', () => {
|
|
useTeamStore.getState().setSelectedTask('task-1');
|
|
expect(useTeamStore.getState().selectedTaskId).toBe('task-1');
|
|
});
|
|
it('should set selected member', () => {
|
|
useTeamStore.getState().setSelectedMember('member-1');
|
|
expect(useTeamStore.getState().selectedMemberId).toBe('member-1');
|
|
});
|
|
it('should clear error', () => {
|
|
useTeamStore.setState({ error: 'Test error' });
|
|
useTeamStore.getState().clearError();
|
|
expect(useTeamStore.getState().error).toBeNull();
|
|
});
|
|
});
|
|
});
|