Files
zclaw_openfang/desktop/tests/e2e/specs/team-collaboration.spec.ts
iven c5d91cf9f0 feat: add integration test framework and health check improvements
- Add test helper library with assertion functions (scripts/lib/test-helpers.sh)
- Add gateway integration test script (scripts/tests/gateway-test.sh)
- Add configuration validation tool (scripts/validate-config.ts)
- Add health-check.ts library with Tauri command wrappers
- Add HealthStatusIndicator component to ConnectionStatus.tsx
- Add E2E test specs for memory, settings, and team collaboration
- Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state

Key improvements:
- Store architecture now properly documented as migrated
- Tauri backend shown as 85-90% complete
- Component integration status clarified

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 00:09:47 +08:00

1488 lines
48 KiB
TypeScript

/**
* ZCLAW Team Collaboration E2E Tests
*
* Tests for multi-agent team collaboration features with mocked Gateway responses.
* Covers team management, member assignment, task allocation, and Dev<->QA loops.
*
* Test Categories:
* - Team Management: Create, select, delete teams
* - Member Management: Add, remove, update member roles
* - Task Management: Create, assign, update task status
* - Dev<->QA Loop: Developer/Reviewer workflow, approval cycles
*/
import { test, expect, Page } from '@playwright/test';
import { setupMockGateway, mockResponses } from '../fixtures/mock-gateway';
import { storeInspectors, STORAGE_KEYS } from '../fixtures/store-inspectors';
import { userActions, waitForAppReady, skipOnboarding, navigateToTab } from '../utils/user-actions';
// Test configuration
test.setTimeout(120000);
const BASE_URL = 'http://localhost:1420';
// Helper to generate unique IDs
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// ============================================
// Test Suite 1: Team Management Tests
// ============================================
test.describe('Team Collaboration - Team Management Tests', () => {
test.describe.configure({ mode: 'parallel' });
test.beforeEach(async ({ page }) => {
await setupMockGateway(page);
await skipOnboarding(page);
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('TEAM-MGMT-01: Create new team with valid configuration', async ({ page }) => {
// Clear existing teams first
await page.evaluate(() => {
localStorage.removeItem('zclaw-teams');
});
// Navigate to team tab
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Create team via store
const teamData = {
name: 'E2E Test Team',
description: 'A team for E2E testing',
pattern: 'sequential' as const,
memberAgents: [
{ agentId: 'agent-dev-1', role: 'worker' as const },
{ agentId: 'agent-qa-1', role: 'reviewer' as const },
],
};
const createResult = await page.evaluate(async (data) => {
try {
// Access team store directly
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const team = await stores.team.getState().createTeam(data);
return { success: true, team };
}
return { success: false, error: 'Team store not available' };
} catch (e) {
return { success: false, error: String(e) };
}
}, teamData);
expect(createResult.success).toBe(true);
expect(createResult.team).toBeDefined();
expect(createResult.team?.name).toBe(teamData.name);
expect(createResult.team?.pattern).toBe(teamData.pattern);
expect(createResult.team?.members.length).toBe(2);
});
test('TEAM-MGMT-02: Team list loads and displays correctly', async ({ page }) => {
// Pre-populate teams
await page.evaluate(() => {
const teams = [
{
id: 'team-1',
name: 'Development Team',
description: 'Main dev team',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'team-2',
name: 'QA Team',
description: 'Quality assurance',
members: [],
tasks: [],
pattern: 'parallel',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
});
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Verify teams loaded
const teamsState = await page.evaluate(() => {
const stored = localStorage.getItem('zclaw-teams');
if (!stored) return null;
try {
return JSON.parse(stored);
} catch {
return null;
}
});
expect(teamsState).not.toBeNull();
expect(Array.isArray(teamsState)).toBe(true);
expect(teamsState.length).toBe(2);
});
test('TEAM-MGMT-03: Select team sets active team', async ({ page }) => {
// Pre-populate teams
const teamId = generateId();
await page.evaluate((id) => {
const teams = [
{
id,
name: 'Active Test Team',
description: 'Team to select',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, teamId);
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Select team via store
const selectResult = await page.evaluate(async (id) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const state = stores.team.getState();
const team = state.teams.find((t: any) => t.id === id);
if (team) {
state.setActiveTeam(team);
return { success: true, activeTeam: stores.team.getState().activeTeam };
}
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, teamId);
expect(selectResult.success).toBe(true);
expect(selectResult.activeTeam?.id).toBe(teamId);
});
test('TEAM-MGMT-04: Delete team removes from list', async ({ page }) => {
const teamId1 = generateId();
const teamId2 = generateId();
// Pre-populate teams
await page.evaluate(({ id1, id2 }) => {
const teams = [
{
id: id1,
name: 'Team to Delete',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: id2,
name: 'Team to Keep',
members: [],
tasks: [],
pattern: 'parallel',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { id1: teamId1, id2: teamId2 });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Delete team via store
const deleteResult = await page.evaluate(async (id) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().deleteTeam(id);
return { success, remainingTeams: stores.team.getState().teams.length };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, teamId1);
expect(deleteResult.success).toBe(true);
expect(deleteResult.remainingTeams).toBe(1);
});
test('TEAM-MGMT-05: Team pattern affects workflow execution', async ({ page }) => {
// Create teams with different patterns
await page.evaluate(() => {
const teams = [
{
id: 'team-sequential',
name: 'Sequential Team',
pattern: 'sequential',
members: [],
tasks: [],
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'team-parallel',
name: 'Parallel Team',
pattern: 'parallel',
members: [],
tasks: [],
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'team-pipeline',
name: 'Pipeline Team',
pattern: 'pipeline',
members: [],
tasks: [],
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
});
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Verify patterns
const teamsState = await page.evaluate(() => {
const stored = localStorage.getItem('zclaw-teams');
return stored ? JSON.parse(stored) : [];
});
const patterns = teamsState.map((t: any) => t.pattern);
expect(patterns).toContain('sequential');
expect(patterns).toContain('parallel');
expect(patterns).toContain('pipeline');
});
});
// ============================================
// Test Suite 2: Member Management Tests
// ============================================
test.describe('Team Collaboration - Member Management Tests', () => {
test.describe.configure({ mode: 'parallel' });
test.beforeEach(async ({ page }) => {
await setupMockGateway(page);
await skipOnboarding(page);
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('TEAM-MEMBER-01: Add member to team', async ({ page }) => {
const teamId = generateId();
// Create team
await page.evaluate((id) => {
const teams = [
{
id,
name: 'Member Test Team',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, teamId);
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Add member via store
const addResult = await page.evaluate(async ({ teamId, agentId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const member = await stores.team.getState().addMember(teamId, agentId, 'worker');
return { success: true, member };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, agentId: 'agent-test-1' });
expect(addResult.success).toBe(true);
expect(addResult.member).toBeDefined();
expect(addResult.member?.role).toBe('worker');
expect(addResult.member?.agentId).toBe('agent-test-1');
});
test('TEAM-MEMBER-02: Remove member from team', async ({ page }) => {
const teamId = generateId();
const memberId = generateId();
// Create team with member
await page.evaluate(({ teamId, memberId }) => {
const teams = [
{
id: teamId,
name: 'Remove Member Team',
members: [
{
id: memberId,
agentId: 'agent-to-remove',
name: 'Agent to Remove',
role: 'worker',
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, memberId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Remove member via store
const removeResult = await page.evaluate(async ({ teamId, memberId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().removeMember(teamId, memberId);
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
return { success, memberCount: team?.members.length };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, memberId });
expect(removeResult.success).toBe(true);
expect(removeResult.memberCount).toBe(0);
});
test('TEAM-MEMBER-03: Update member role', async ({ page }) => {
const teamId = generateId();
const memberId = generateId();
// Create team with member
await page.evaluate(({ teamId, memberId }) => {
const teams = [
{
id: teamId,
name: 'Role Update Team',
members: [
{
id: memberId,
agentId: 'agent-role-test',
name: 'Agent Role Test',
role: 'worker',
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, memberId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Update role via store
const updateResult = await page.evaluate(async ({ teamId, memberId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().updateMemberRole(teamId, memberId, 'reviewer');
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const member = team?.members.find((m: any) => m.id === memberId);
return { success, newRole: member?.role };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, memberId });
expect(updateResult.success).toBe(true);
expect(updateResult.newRole).toBe('reviewer');
});
test('TEAM-MEMBER-04: Member workload tracking', async ({ page }) => {
const teamId = generateId();
const memberId = generateId();
const taskId = generateId();
// Create team with member and task
await page.evaluate(({ teamId, memberId, taskId }) => {
const teams = [
{
id: teamId,
name: 'Workload Team',
members: [
{
id: memberId,
agentId: 'agent-workload',
name: 'Workload Agent',
role: 'worker',
skills: [],
workload: 50,
status: 'working',
maxConcurrentTasks: 2,
currentTasks: [taskId],
},
],
tasks: [
{
id: taskId,
title: 'Test Task',
status: 'in_progress',
priority: 'medium',
assigneeId: memberId,
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, memberId, taskId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Verify workload
const workloadState = await page.evaluate(({ teamId, memberId }) => {
const stored = localStorage.getItem('zclaw-teams');
if (!stored) return null;
const teams = JSON.parse(stored);
const team = teams.find((t: any) => t.id === teamId);
const member = team?.members.find((m: any) => m.id === memberId);
return {
workload: member?.workload,
status: member?.status,
currentTasksCount: member?.currentTasks?.length,
};
}, { teamId, memberId });
expect(workloadState?.workload).toBe(50);
expect(workloadState?.status).toBe('working');
expect(workloadState?.currentTasksCount).toBe(1);
});
test('TEAM-MEMBER-05: Multiple members with different roles', async ({ page }) => {
const teamId = generateId();
// Create team with multiple members
await page.evaluate((id) => {
const teams = [
{
id,
name: 'Multi-Member Team',
members: [
{
id: 'member-orchestrator',
agentId: 'agent-orchestrator',
name: 'Orchestrator',
role: 'orchestrator',
skills: ['planning', 'coordination'],
workload: 20,
status: 'idle',
maxConcurrentTasks: 5,
currentTasks: [],
},
{
id: 'member-dev-1',
agentId: 'agent-dev-1',
name: 'Developer 1',
role: 'worker',
skills: ['typescript', 'react'],
workload: 75,
status: 'working',
maxConcurrentTasks: 2,
currentTasks: ['task-1'],
},
{
id: 'member-qa-1',
agentId: 'agent-qa-1',
name: 'QA 1',
role: 'reviewer',
skills: ['testing', 'review'],
workload: 30,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, teamId);
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Verify roles distribution
const rolesState = await page.evaluate((id) => {
const stored = localStorage.getItem('zclaw-teams');
if (!stored) return null;
const teams = JSON.parse(stored);
const team = teams.find((t: any) => t.id === id);
return {
memberCount: team?.members.length,
roles: team?.members.map((m: any) => m.role),
};
}, teamId);
expect(rolesState?.memberCount).toBe(3);
expect(rolesState?.roles).toContain('orchestrator');
expect(rolesState?.roles).toContain('worker');
expect(rolesState?.roles).toContain('reviewer');
});
});
// ============================================
// Test Suite 3: Task Management Tests
// ============================================
test.describe('Team Collaboration - Task Management Tests', () => {
test.describe.configure({ mode: 'parallel' });
test.beforeEach(async ({ page }) => {
await setupMockGateway(page);
await skipOnboarding(page);
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('TEAM-TASK-01: Create task in team', async ({ page }) => {
const teamId = generateId();
// Create team
await page.evaluate((id) => {
const teams = [
{
id,
name: 'Task Test Team',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, teamId);
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Add task via store
const taskResult = await page.evaluate(async (teamId) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const task = await stores.team.getState().addTask({
teamId,
title: 'E2E Test Task',
description: 'A task created during E2E testing',
priority: 'high',
type: 'development',
});
return { success: true, task };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, teamId);
expect(taskResult.success).toBe(true);
expect(taskResult.task).toBeDefined();
expect(taskResult.task?.title).toBe('E2E Test Task');
expect(taskResult.task?.status).toBe('pending');
});
test('TEAM-TASK-02: Assign task to member', async ({ page }) => {
const teamId = generateId();
const memberId = generateId();
const taskId = generateId();
// Create team with member and task
await page.evaluate(({ teamId, memberId, taskId }) => {
const teams = [
{
id: teamId,
name: 'Assign Task Team',
members: [
{
id: memberId,
agentId: 'agent-assign',
name: 'Assign Agent',
role: 'worker',
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
],
tasks: [
{
id: taskId,
title: 'Task to Assign',
status: 'pending',
priority: 'medium',
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, memberId, taskId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Assign task via store
const assignResult = await page.evaluate(async ({ teamId, taskId, memberId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().assignTask(teamId, taskId, memberId);
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const task = team?.tasks.find((t: any) => t.id === taskId);
return { success, assigneeId: task?.assigneeId, taskStatus: task?.status };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, taskId, memberId });
expect(assignResult.success).toBe(true);
expect(assignResult.assigneeId).toBe(memberId);
expect(assignResult.taskStatus).toBe('assigned');
});
test('TEAM-TASK-03: Update task status through workflow', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
// Create team with task
await page.evaluate(({ teamId, taskId }) => {
const teams = [
{
id: teamId,
name: 'Status Update Team',
members: [],
tasks: [
{
id: taskId,
title: 'Status Update Task',
status: 'assigned',
priority: 'medium',
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Update status to in_progress
const progressResult = await page.evaluate(async ({ teamId, taskId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
await stores.team.getState().updateTaskStatus(teamId, taskId, 'in_progress');
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const task = team?.tasks.find((t: any) => t.id === taskId);
return { status: task?.status, startedAt: task?.startedAt };
}
return { status: null };
} catch (e) {
return { error: String(e) };
}
}, { teamId, taskId });
expect(progressResult.status).toBe('in_progress');
expect(progressResult.startedAt).toBeDefined();
// Update status to completed
const completeResult = await page.evaluate(async ({ teamId, taskId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
await stores.team.getState().updateTaskStatus(teamId, taskId, 'completed');
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const task = team?.tasks.find((t: any) => t.id === taskId);
return { status: task?.status, completedAt: task?.completedAt };
}
return { status: null };
} catch (e) {
return { error: String(e) };
}
}, { teamId, taskId });
expect(completeResult.status).toBe('completed');
expect(completeResult.completedAt).toBeDefined();
});
test('TEAM-TASK-04: Submit deliverable for review', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
// Create team with task in progress
await page.evaluate(({ teamId, taskId }) => {
const teams = [
{
id: teamId,
name: 'Deliverable Team',
members: [],
tasks: [
{
id: taskId,
title: 'Deliverable Task',
status: 'in_progress',
priority: 'high',
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Submit deliverable
const deliverableResult = await page.evaluate(async ({ teamId, taskId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const deliverable = {
type: 'code',
content: 'function test() { return true; }',
files: ['src/test.ts'],
summary: 'Completed test function',
};
const success = await stores.team.getState().submitDeliverable(teamId, taskId, deliverable);
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const task = team?.tasks.find((t: any) => t.id === taskId);
return { success, taskStatus: task?.status, hasDeliverable: !!task?.deliverable };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, taskId });
expect(deliverableResult.success).toBe(true);
expect(deliverableResult.taskStatus).toBe('review');
expect(deliverableResult.hasDeliverable).toBe(true);
});
test('TEAM-TASK-05: Task dependencies respected', async ({ page }) => {
const teamId = generateId();
const task1Id = generateId();
const task2Id = generateId();
// Create team with dependent tasks
await page.evaluate(({ teamId, task1Id, task2Id }) => {
const teams = [
{
id: teamId,
name: 'Dependencies Team',
members: [],
tasks: [
{
id: task1Id,
title: 'Base Task',
status: 'pending',
priority: 'high',
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: task2Id,
title: 'Dependent Task',
status: 'pending',
priority: 'medium',
dependencies: [task1Id],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'pipeline',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, task1Id, task2Id });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Verify dependencies
const depState = await page.evaluate(({ teamId, task2Id }) => {
const stored = localStorage.getItem('zclaw-teams');
if (!stored) return null;
const teams = JSON.parse(stored);
const team = teams.find((t: any) => t.id === teamId);
const task = team?.tasks.find((t: any) => t.id === task2Id);
return {
hasDependencies: (task?.dependencies?.length ?? 0) > 0,
dependencyCount: task?.dependencies?.length,
};
}, { teamId, task2Id });
expect(depState?.hasDependencies).toBe(true);
expect(depState?.dependencyCount).toBe(1);
});
});
// ============================================
// Test Suite 4: Dev<->QA Loop Tests
// ============================================
test.describe('Team Collaboration - Dev<->QA Loop Tests', () => {
test.describe.configure({ mode: 'serial' });
test.beforeEach(async ({ page }) => {
await setupMockGateway(page);
await skipOnboarding(page);
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('TEAM-LOOP-01: Start Dev<->QA loop', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
const developerId = generateId();
const reviewerId = generateId();
// Create team with developer and reviewer
await page.evaluate(({ teamId, taskId, developerId, reviewerId }) => {
const teams = [
{
id: teamId,
name: 'Loop Team',
members: [
{
id: developerId,
agentId: 'agent-dev',
name: 'Developer',
role: 'worker',
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
{
id: reviewerId,
agentId: 'agent-qa',
name: 'Reviewer',
role: 'reviewer',
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: 2,
currentTasks: [],
},
],
tasks: [
{
id: taskId,
title: 'Loop Test Task',
status: 'assigned',
priority: 'high',
assigneeId: developerId,
dependencies: [],
type: 'development',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId, developerId, reviewerId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Start loop
const loopResult = await page.evaluate(async ({ teamId, taskId, developerId, reviewerId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const loop = await stores.team.getState().startDevQALoop(teamId, taskId, developerId, reviewerId);
return { success: true, loop };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, taskId, developerId, reviewerId });
expect(loopResult.success).toBe(true);
expect(loopResult.loop).toBeDefined();
expect(loopResult.loop?.state).toBe('developing');
expect(loopResult.loop?.developerId).toBe(developerId);
expect(loopResult.loop?.reviewerId).toBe(reviewerId);
});
test('TEAM-LOOP-02: Submit review with approval', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
const developerId = generateId();
const reviewerId = generateId();
const loopId = generateId();
// Create team with active loop
await page.evaluate(({ teamId, taskId, developerId, reviewerId, loopId }) => {
const teams = [
{
id: teamId,
name: 'Review Team',
members: [
{ id: developerId, agentId: 'agent-dev', name: 'Developer', role: 'worker', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
{ id: reviewerId, agentId: 'agent-qa', name: 'Reviewer', role: 'reviewer', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
],
tasks: [
{ id: taskId, title: 'Review Task', status: 'review', priority: 'high', assigneeId: developerId, dependencies: [], type: 'development', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
],
pattern: 'sequential',
activeLoops: [
{
id: loopId,
developerId,
reviewerId,
taskId,
state: 'reviewing',
iterationCount: 0,
maxIterations: 3,
feedbackHistory: [],
startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
},
],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId, developerId, reviewerId, loopId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Submit approval
const approveResult = await page.evaluate(async ({ teamId, loopId, reviewerId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().submitReview(teamId, loopId, {
verdict: 'approved',
comments: 'Great work!',
});
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const loop = team?.activeLoops.find((l: any) => l.id === loopId);
return { success, loopState: loop?.state };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, loopId, reviewerId });
expect(approveResult.success).toBe(true);
expect(approveResult.loopState).toBe('approved');
});
test('TEAM-LOOP-03: Submit review with revision request', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
const developerId = generateId();
const reviewerId = generateId();
const loopId = generateId();
// Create team with active loop
await page.evaluate(({ teamId, taskId, developerId, reviewerId, loopId }) => {
const teams = [
{
id: teamId,
name: 'Revision Team',
members: [
{ id: developerId, agentId: 'agent-dev', name: 'Developer', role: 'worker', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
{ id: reviewerId, agentId: 'agent-qa', name: 'Reviewer', role: 'reviewer', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
],
tasks: [
{ id: taskId, title: 'Revision Task', status: 'review', priority: 'high', assigneeId: developerId, dependencies: [], type: 'development', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
],
pattern: 'sequential',
activeLoops: [
{
id: loopId,
developerId,
reviewerId,
taskId,
state: 'reviewing',
iterationCount: 0,
maxIterations: 3,
feedbackHistory: [],
startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
},
],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId, developerId, reviewerId, loopId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Request revision
const revisionResult = await page.evaluate(async ({ teamId, loopId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().submitReview(teamId, loopId, {
verdict: 'needs_work',
comments: 'Please add more tests',
});
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const loop = team?.activeLoops.find((l: any) => l.id === loopId);
return { success, loopState: loop?.state, iterationCount: loop?.iterationCount };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, loopId });
expect(revisionResult.success).toBe(true);
expect(revisionResult.loopState).toBe('revising');
expect(revisionResult.iterationCount).toBe(1);
});
test('TEAM-LOOP-04: Max iterations triggers escalation', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
const developerId = generateId();
const reviewerId = generateId();
const loopId = generateId();
// Create team with loop at max iterations
await page.evaluate(({ teamId, taskId, developerId, reviewerId, loopId }) => {
const teams = [
{
id: teamId,
name: 'Escalation Team',
members: [
{ id: developerId, agentId: 'agent-dev', name: 'Developer', role: 'worker', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
{ id: reviewerId, agentId: 'agent-qa', name: 'Reviewer', role: 'reviewer', skills: [], workload: 0, status: 'idle', maxConcurrentTasks: 2, currentTasks: [] },
],
tasks: [
{ id: taskId, title: 'Escalation Task', status: 'review', priority: 'high', assigneeId: developerId, dependencies: [], type: 'development', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
],
pattern: 'sequential',
activeLoops: [
{
id: loopId,
developerId,
reviewerId,
taskId,
state: 'reviewing',
iterationCount: 2, // At max - 1
maxIterations: 3,
feedbackHistory: [
{ verdict: 'needs_work', comments: 'First revision', reviewedAt: new Date().toISOString(), reviewerId },
{ verdict: 'needs_work', comments: 'Second revision', reviewedAt: new Date().toISOString(), reviewerId },
],
startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
},
],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId, developerId, reviewerId, loopId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Submit another revision request - should escalate
const escalateResult = await page.evaluate(async ({ teamId, loopId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().submitReview(teamId, loopId, {
verdict: 'needs_work',
comments: 'Still needs work',
});
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const loop = team?.activeLoops.find((l: any) => l.id === loopId);
return { success, loopState: loop?.state };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, loopId });
expect(escalateResult.success).toBe(true);
expect(escalateResult.loopState).toBe('escalated');
});
test('TEAM-LOOP-05: Update loop state directly', async ({ page }) => {
const teamId = generateId();
const loopId = generateId();
// Create team with developing loop
await page.evaluate(({ teamId, loopId }) => {
const teams = [
{
id: teamId,
name: 'State Update Team',
members: [],
tasks: [],
pattern: 'sequential',
activeLoops: [
{
id: loopId,
developerId: 'dev-1',
reviewerId: 'rev-1',
taskId: 'task-1',
state: 'developing',
iterationCount: 0,
maxIterations: 3,
feedbackHistory: [],
startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(),
},
],
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, loopId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Update state to reviewing
const stateResult = await page.evaluate(async ({ teamId, loopId }) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const success = await stores.team.getState().updateLoopState(teamId, loopId, 'reviewing');
const team = stores.team.getState().teams.find((t: any) => t.id === teamId);
const loop = team?.activeLoops.find((l: any) => l.id === loopId);
return { success, loopState: loop?.state };
}
return { success: false };
} catch (e) {
return { success: false, error: String(e) };
}
}, { teamId, loopId });
expect(stateResult.success).toBe(true);
expect(stateResult.loopState).toBe('reviewing');
});
});
// ============================================
// Test Suite 5: Team Metrics Tests
// ============================================
test.describe('Team Collaboration - Metrics Tests', () => {
test.describe.configure({ mode: 'parallel' });
test.beforeEach(async ({ page }) => {
await setupMockGateway(page);
await skipOnboarding(page);
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('TEAM-METRIC-01: Team metrics calculate correctly', async ({ page }) => {
const teamId = generateId();
// Create team with completed tasks
await page.evaluate((id) => {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 3600000);
const thirtyMinAgo = new Date(now.getTime() - 1800000);
const teams = [
{
id,
name: 'Metrics Team',
members: [],
tasks: [
{
id: 'task-completed-1',
title: 'Completed Task 1',
status: 'completed',
priority: 'high',
dependencies: [],
type: 'development',
startedAt: oneHourAgo.toISOString(),
completedAt: thirtyMinAgo.toISOString(),
reviewFeedback: { verdict: 'approved' },
createdAt: oneHourAgo.toISOString(),
updatedAt: now.toISOString(),
},
{
id: 'task-completed-2',
title: 'Completed Task 2',
status: 'completed',
priority: 'medium',
dependencies: [],
type: 'development',
startedAt: oneHourAgo.toISOString(),
completedAt: now.toISOString(),
reviewFeedback: { verdict: 'approved' },
createdAt: oneHourAgo.toISOString(),
updatedAt: now.toISOString(),
},
{
id: 'task-pending',
title: 'Pending Task',
status: 'pending',
priority: 'low',
dependencies: [],
type: 'development',
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, teamId);
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Get metrics
const metricsResult = await page.evaluate(async (teamId) => {
try {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const state = stores.team.getState();
const team = state.teams.find((t: any) => t.id === teamId);
if (team) {
state.setActiveTeam(team);
return { metrics: stores.team.getState().metrics };
}
}
return { metrics: null };
} catch (e) {
return { metrics: null, error: String(e) };
}
}, teamId);
expect(metricsResult.metrics).not.toBeNull();
expect(metricsResult.metrics?.tasksCompleted).toBe(2);
expect(metricsResult.metrics?.passRate).toBeGreaterThan(0);
expect(metricsResult.metrics?.efficiency).toBeGreaterThan(0);
});
test('TEAM-METRIC-02: Metrics update after task completion', async ({ page }) => {
const teamId = generateId();
const taskId = generateId();
// Create team with one task
await page.evaluate(({ teamId, taskId }) => {
const now = new Date();
const teams = [
{
id: teamId,
name: 'Update Metrics Team',
members: [],
tasks: [
{
id: taskId,
title: 'Task to Complete',
status: 'in_progress',
priority: 'high',
dependencies: [],
type: 'development',
startedAt: now.toISOString(),
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
},
],
pattern: 'sequential',
activeLoops: [],
status: 'active',
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
},
];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
}, { teamId, taskId });
await navigateToTab(page, '团队');
await page.waitForTimeout(500);
// Complete the task
await page.evaluate(async ({ teamId, taskId }) => {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
await stores.team.getState().updateTaskStatus(teamId, taskId, 'completed');
}
}, { teamId, taskId });
// Check metrics
const metricsResult = await page.evaluate(async (teamId) => {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.team) {
const state = stores.team.getState();
const team = state.teams.find((t: any) => t.id === teamId);
if (team) {
state.setActiveTeam(team);
return { metrics: stores.team.getState().metrics };
}
}
return { metrics: null };
}, teamId);
expect(metricsResult.metrics?.tasksCompleted).toBe(1);
});
});
// ============================================
// Test Report
// ============================================
test.afterAll(async ({}, testInfo) => {
console.log('\n========================================');
console.log('ZCLAW Team Collaboration E2E Tests Complete');
console.log('========================================');
console.log(`Test Time: ${new Date().toISOString()}`);
console.log('========================================\n');
});