feat(ui): Phase 8 UI/UX optimization and system documentation update

## 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>
This commit is contained in:
iven
2026-03-15 14:12:11 +08:00
parent bf79c06d4a
commit 3e81bd3e50
30 changed files with 8875 additions and 284 deletions

View File

@@ -0,0 +1,362 @@
/**
* 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();
});
});
});