Compare commits

..

2 Commits

Author SHA1 Message Date
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
iven
fc30290b1c fix(team): resolve TypeScript errors in team collaboration module
- Remove unused imports and variables in Team components
- Fix CollaborationEvent type import in useTeamEvents
- Add proper type guards for Hand status in gatewayStore
- Fix Session status type compatibility in gateway-client
- Remove unused getGatewayClient import from teamStore
- Handle unknown payload types in TeamCollaborationView

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 09:20:58 +08:00
11 changed files with 616 additions and 48 deletions

View File

@@ -7,11 +7,11 @@
* @module components/DevQALoop
*/
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { useTeamStore } from '../store/teamStore';
import type { DevQALoop as DevQALoopType, ReviewFeedback, ReviewIssue } from '../types/team';
import {
RefreshCw, CheckCircle, XCircle, AlertTriangle, ArrowRight,
RefreshCw, CheckCircle, XCircle, AlertTriangle,
Clock, MessageSquare, FileCode, Bug, Lightbulb, ChevronDown, ChevronUp,
Send, ThumbsUp, ThumbsDown, AlertOctagon,
} from 'lucide-react';
@@ -126,7 +126,7 @@ interface ReviewFormProps {
onCancel: () => void;
}
function ReviewForm({ loopId, teamId, onSubmit, onCancel }: ReviewFormProps) {
function ReviewForm({ loopId: _loopId, teamId: _teamId, onSubmit, onCancel }: ReviewFormProps) {
const [verdict, setVerdict] = useState<ReviewFeedback['verdict']>('needs_work');
const [comment, setComment] = useState('');
const [issues, setIssues] = useState<ReviewIssue[]>([]);
@@ -316,10 +316,6 @@ export function DevQALoopPanel({ loop, teamId, developerName, reviewerName, task
setShowReviewForm(false);
};
const handleStartRevising = async () => {
await updateLoopState(teamId, loop.id, 'revising');
};
const handleCompleteRevision = async () => {
await updateLoopState(teamId, loop.id, 'reviewing');
};

View File

@@ -11,7 +11,7 @@ import { useState, useEffect, useRef } from 'react';
import { useTeamStore } from '../store/teamStore';
import type { Team, TeamMember, TeamTask, CollaborationEvent } from '../types/team';
import {
Activity, Users, CheckCircle, Clock, AlertTriangle, Play, Pause,
Activity, Users, CheckCircle, AlertTriangle, Play,
ArrowRight, GitBranch, MessageSquare, FileCode, Bot, Zap,
TrendingUp, TrendingDown, Minus, Circle,
} from 'lucide-react';
@@ -56,7 +56,9 @@ function EventFeedItem({ event, team }: EventFeedItemProps) {
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-0.5 line-clamp-2">
{event.payload.description || JSON.stringify(event.payload).slice(0, 100)}
{typeof event.payload.description === 'string'
? event.payload.description
: JSON.stringify(event.payload).slice(0, 100)}
</p>
</div>
<span className="text-xs text-gray-400 whitespace-nowrap">

View File

@@ -7,11 +7,10 @@
* @module components/TeamOrchestrator
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect } from 'react';
import { useTeamStore } from '../store/teamStore';
import { useGatewayStore } from '../store/gatewayStore';
import type {
Team,
TeamMember,
TeamTask,
TeamMemberRole,
@@ -19,9 +18,9 @@ import type {
CollaborationPattern,
} from '../types/team';
import {
Users, Plus, Trash2, Edit2, Check, X, ChevronDown, ChevronUp,
Bot, GitBranch, ArrowRight, Clock, AlertTriangle, CheckCircle,
Play, Pause, Settings, UserPlus, FileText, Activity,
Users, Plus, Trash2, X,
Bot, Clock, AlertTriangle, CheckCircle,
Play, UserPlus, FileText,
} from 'lucide-react';
// === Sub-Components ===
@@ -115,7 +114,7 @@ interface TaskCardProps {
onStatusChange: (status: TeamTask['status']) => void;
}
function TaskCard({ task, members, isSelected, onSelect, onAssign, onStatusChange }: TaskCardProps) {
function TaskCard({ task, members, isSelected, onSelect, onAssign, onStatusChange: _onStatusChange }: TaskCardProps) {
const [showAssignMenu, setShowAssignMenu] = useState(false);
const priorityColors: Record<TaskPriority, string> = {
@@ -216,7 +215,6 @@ export function TeamOrchestrator({ isOpen, onClose }: TeamOrchestratorProps) {
teams,
activeTeam,
metrics,
isLoading,
error,
selectedTaskId,
selectedMemberId,

View File

@@ -967,12 +967,42 @@ export class GatewayClient {
// === OpenFang Hands API ===
/** List available Hands */
async listHands(): Promise<{ hands: { id: string; name: string; description: string; status: string; requirements_met?: boolean; category?: string }[] }> {
async listHands(): Promise<{
hands: {
id?: string;
name: string;
description?: string;
status?: string;
requirements_met?: boolean;
category?: string;
icon?: string;
tool_count?: number;
tools?: string[];
metric_count?: number;
metrics?: string[];
}[]
}> {
return this.restGet('/api/hands');
}
/** Get Hand details */
async getHand(name: string): Promise<{ name: string; description: string; config: Record<string, unknown> }> {
async getHand(name: string): Promise<{
id?: string;
name?: string;
description?: string;
status?: string;
requirements_met?: boolean;
category?: string;
icon?: string;
provider?: string;
model?: string;
requirements?: { description?: string; name?: string; met?: boolean; satisfied?: boolean; details?: string; hint?: string }[];
tools?: string[];
metrics?: string[];
config?: Record<string, unknown>;
tool_count?: number;
metric_count?: number;
}> {
return this.restGet(`/api/hands/${name}`);
}
@@ -1097,7 +1127,7 @@ export class GatewayClient {
created_at: string;
updated_at?: string;
message_count?: number;
status?: string;
status?: 'active' | 'archived' | 'expired';
}>;
}> {
const params = new URLSearchParams();
@@ -1113,7 +1143,7 @@ export class GatewayClient {
created_at: string;
updated_at?: string;
message_count?: number;
status?: string;
status?: 'active' | 'archived' | 'expired';
metadata?: Record<string, unknown>;
}> {
return this.restGet(`/api/sessions/${sessionId}`);

View File

@@ -23,6 +23,9 @@ import type {
TeamMetrics,
} from '../types/team';
// Re-export types for consumers
export type { CollaborationEvent } from '../types/team';
// === Configuration ===
const API_BASE = '/api'; // Uses Vite proxy

View File

@@ -10,7 +10,7 @@
import { useEffect, useRef, useCallback } from 'react';
import { useTeamStore } from '../store/teamStore';
import { useGatewayStore } from '../store/gatewayStore';
import type { TeamEventMessage, TeamEventType } from '../lib/team-client';
import type { TeamEventMessage, TeamEventType, CollaborationEvent } from '../lib/team-client';
interface UseTeamEventsOptions {
/** Subscribe to specific team only, or null for all teams */
@@ -25,12 +25,11 @@ interface UseTeamEventsOptions {
* Hook for subscribing to real-time team collaboration events
*/
export function useTeamEvents(options: UseTeamEventsOptions = {}) {
const { teamId = null, eventTypes, maxEvents = 100 } = options;
const { teamId = null, eventTypes } = options;
const unsubscribeRef = useRef<(() => void) | null>(null);
const {
addEvent,
setActiveTeam,
updateTaskStatus,
updateLoopState,
loadTeams,

View File

@@ -1039,17 +1039,23 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
try {
const result = await get().client.listHands();
// Map API response to Hand interface
const hands: Hand[] = (result?.hands || []).map(h => ({
id: h.id || h.name,
name: h.name,
description: h.description || '',
status: h.status || (h.requirements_met ? 'idle' : 'setup_needed'),
requirements_met: h.requirements_met,
category: h.category,
icon: h.icon,
toolCount: h.tool_count || h.tools?.length,
metricCount: h.metric_count || h.metrics?.length,
}));
const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const;
const hands: Hand[] = (result?.hands || []).map(h => {
const status = validStatuses.includes(h.status as any)
? h.status as Hand['status']
: (h.requirements_met ? 'idle' : 'setup_needed');
return {
id: h.id || h.name,
name: h.name,
description: h.description || '',
status,
requirements_met: h.requirements_met,
category: h.category,
icon: h.icon,
toolCount: h.tool_count || h.tools?.length,
metricCount: h.metric_count || h.metrics?.length,
};
});
set({ hands, isLoading: false });
} catch {
set({ isLoading: false });
@@ -1062,24 +1068,39 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
const result = await get().client.getHand(name);
if (!result) return undefined;
// Helper to extract string from unknown config
const getStringFromConfig = (key: string): string | undefined => {
const val = result.config?.[key];
return typeof val === 'string' ? val : undefined;
};
const getArrayFromConfig = (key: string): string[] | undefined => {
const val = result.config?.[key];
return Array.isArray(val) ? val : undefined;
};
const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const;
const status = validStatuses.includes(result.status as any)
? result.status as Hand['status']
: (result.requirements_met ? 'idle' : 'setup_needed');
// Map API response to extended Hand interface
const hand: Hand = {
id: result.id || result.name || name,
name: result.name || name,
description: result.description || '',
status: result.status || (result.requirements_met ? 'idle' : 'setup_needed'),
status,
requirements_met: result.requirements_met,
category: result.category,
icon: result.icon,
provider: result.provider || result.config?.provider,
model: result.model || result.config?.model,
provider: result.provider || getStringFromConfig('provider'),
model: result.model || getStringFromConfig('model'),
requirements: result.requirements?.map((r: any) => ({
description: r.description || r.name || String(r),
met: r.met ?? r.satisfied ?? true,
details: r.details || r.hint,
})),
tools: result.tools || result.config?.tools,
metrics: result.metrics || result.config?.metrics,
tools: result.tools || getArrayFromConfig('tools'),
metrics: result.metrics || getArrayFromConfig('metrics'),
toolCount: result.tool_count || result.tools?.length || 0,
metricCount: result.metric_count || result.metrics?.length || 0,
};
@@ -1119,7 +1140,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
triggerHand: async (name: string, params?: Record<string, unknown>) => {
try {
const result = await get().client.triggerHand(name, params);
return result ? { runId: result.runId, status: result.status } : undefined;
return result ? { runId: result.runId, status: result.status, startedAt: new Date().toISOString() } : undefined;
} catch (err: any) {
set({ error: err.message });
return undefined;
@@ -1446,7 +1467,7 @@ export const useGatewayStore = create<GatewayStore>((set, get) => {
agentId: result.agent_id,
createdAt: result.created_at,
status: 'active',
metadata: result.metadata,
metadata: metadata,
};
set(state => ({ sessions: [...state.sessions, session] }));
return session;

View File

@@ -16,7 +16,6 @@ import type {
TeamMemberRole,
DevQALoop,
DevQALoopState,
CollaborationPattern,
CreateTeamRequest,
AddTeamTaskRequest,
TeamMetrics,
@@ -24,7 +23,6 @@ import type {
ReviewFeedback,
TaskDeliverable,
} from '../types/team';
import { getGatewayClient } from '../lib/gateway-client';
// === Store State ===
@@ -136,7 +134,6 @@ export const useTeamStore = create<TeamStoreState>((set, get) => ({
loadTeams: async () => {
set({ isLoading: true, error: null });
try {
const client = getGatewayClient();
// For now, load from localStorage until API is available
const stored = localStorage.getItem('zclaw-teams');
const teams: Team[] = stored ? JSON.parse(stored) : [];
@@ -150,7 +147,7 @@ export const useTeamStore = create<TeamStoreState>((set, get) => ({
set({ isLoading: true, error: null });
try {
const now = new Date().toISOString();
const members: TeamMember[] = request.memberAgents.map((m, index) => ({
const members: TeamMember[] = request.memberAgents.map((m) => ({
id: generateId(),
agentId: m.agentId,
name: `Agent-${m.agentId.slice(0, 4)}`,
@@ -208,7 +205,7 @@ export const useTeamStore = create<TeamStoreState>((set, get) => ({
},
setActiveTeam: (team: Team | null) => {
set(state => ({
set(() => ({
activeTeam: team,
metrics: team ? calculateMetrics(team) : null,
}));

View File

@@ -7,7 +7,7 @@
* @module types/team
*/
import type { Agent, AgentStatus } from './agent';
import type { AgentStatus } from './agent';
// === Team Definition ===

View File

@@ -449,6 +449,11 @@ ZCLAW 是基于 **OpenFang** (Rust Agent OS) 的 AI Agent 桌面客户端,核
* API 客户端:
* ✅ Team API 客户端 (`lib/team-client.ts`)
* ✅ WebSocket 事件订阅 (`lib/useTeamEvents.ts`)
* 代码质量:
* ✅ TypeScript 类型检查通过 (2026-03-15)
* ✅ 移除未使用的导入和变量
* ✅ 修复类型不兼容问题
* 待完成:
* 与 OpenFang 后端 API 对接测试
*下一步: 后端 API 对接测试与集成验证*
* 单元测试覆盖
*下一步: 后端 API 对接测试与单元测试*

View File

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