mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
c0d469a3bb
Brutal honesty review of the flatten-to-root migration found 12 categories of issues. This commit fixes all of them: - Fix @agentic-qe/v3 package refs in init installers and quality-criteria-service - Fix 7 broken import() paths in rvf-baseline-benchmark.ts (v3/src → src) - Fix prepare-assets.sh and demo-warmup.sh path references - Delete dead scripts/migrate-v2-to-v3-memory.js - Remove .claude-flow daemon state dirs (284KB) from agents/skills - Add .claude-flow exclusion to .npmignore - Fix JSDoc @agentic-qe/v3/ → agentic-qe/ in 27 source files - Fix @module v3/ tags in 9 dream engine files - Fix stale v3/ path refs in docs and architecture ADRs - Revert memory namespace changes (aqe/v3/domains/* are DB identifiers, not filesystem paths — changing them orphans 150K+ existing records) - Update CLAUDE.md with agent classification rules (QE vs non-QE agents) - Fix domain-team-manager test (scaling is not capped by defaultTeamSize) - Fix adr-040 timing test threshold (20ms → 50ms for CI tolerance) - Rename test:v3 → test:all in package.json - Update README.md project structure tree - Fix infra-healing test and demo script cd v3 references Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
498 lines
17 KiB
TypeScript
498 lines
17 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Domain Team Manager Unit Tests
|
|
* ADR-064 Phase 2A: Tests for domain-scoped agent team management
|
|
*
|
|
* Uses real AgentTeamsAdapter (not mocked) since DomainTeamManager
|
|
* relies on its mailbox functionality for message delivery and
|
|
* agent registration tracking.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
DomainTeamManager,
|
|
createDomainTeamManager,
|
|
type DomainTeamManagerConfig,
|
|
} from '../../src/coordination/agent-teams/domain-team-manager.js';
|
|
import {
|
|
AgentTeamsAdapter,
|
|
createAgentTeamsAdapter,
|
|
} from '../../src/coordination/agent-teams/adapter.js';
|
|
|
|
// ============================================================================
|
|
// Test Helpers
|
|
// ============================================================================
|
|
|
|
function makeAdapter(): AgentTeamsAdapter {
|
|
// Disable cleanup timer to avoid leaking timers in tests
|
|
return createAgentTeamsAdapter({ cleanupIntervalMs: 0 });
|
|
}
|
|
|
|
function makeManager(
|
|
adapter: AgentTeamsAdapter,
|
|
config?: Partial<DomainTeamManagerConfig>
|
|
): DomainTeamManager {
|
|
return createDomainTeamManager(adapter, config);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Domain Team Manager Tests
|
|
// ============================================================================
|
|
|
|
describe('DomainTeamManager', () => {
|
|
let adapter: AgentTeamsAdapter;
|
|
let manager: DomainTeamManager;
|
|
|
|
beforeEach(() => {
|
|
adapter = makeAdapter();
|
|
manager = makeManager(adapter);
|
|
});
|
|
|
|
afterEach(() => {
|
|
manager.dispose();
|
|
adapter.shutdown();
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Team Creation
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('createDomainTeam', () => {
|
|
it('should create a domain team with lead and teammates', () => {
|
|
const team = manager.createDomainTeam('security', 'lead-sec', ['tm-1', 'tm-2']);
|
|
|
|
expect(team.domain).toBe('security');
|
|
expect(team.leadAgentId).toBe('lead-sec');
|
|
expect(team.teammateIds).toEqual(['tm-1', 'tm-2']);
|
|
expect(team.taskCount).toBe(0);
|
|
expect(team.completedCount).toBe(0);
|
|
expect(team.createdAt).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should register all agents in the adapter on creation', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-a', 'tm-b']);
|
|
|
|
expect(adapter.isRegistered('lead-t')).toBe(true);
|
|
expect(adapter.isRegistered('tm-a')).toBe(true);
|
|
expect(adapter.isRegistered('tm-b')).toBe(true);
|
|
});
|
|
|
|
it('should prevent duplicate teams for the same domain', () => {
|
|
manager.createDomainTeam('coverage', 'lead-cov', []);
|
|
|
|
expect(() => {
|
|
manager.createDomainTeam('coverage', 'lead-cov-2', []);
|
|
}).toThrow(/already exists/);
|
|
});
|
|
|
|
it('should create a team with no initial teammates', () => {
|
|
const team = manager.createDomainTeam('perf', 'lead-perf');
|
|
|
|
expect(team.leadAgentId).toBe('lead-perf');
|
|
expect(team.teammateIds).toEqual([]);
|
|
});
|
|
|
|
it('should enforce max active teams limit', () => {
|
|
const mgr = makeManager(adapter, { maxActiveTeams: 2 });
|
|
|
|
mgr.createDomainTeam('d1', 'lead-1', []);
|
|
mgr.createDomainTeam('d2', 'lead-2', []);
|
|
|
|
expect(() => {
|
|
mgr.createDomainTeam('d3', 'lead-3', []);
|
|
}).toThrow(/Maximum active teams/);
|
|
|
|
mgr.dispose();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Team Removal
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('removeDomainTeam', () => {
|
|
it('should remove a domain team and clean up agents', () => {
|
|
manager.createDomainTeam('security', 'lead-sec', ['tm-1']);
|
|
|
|
const removed = manager.removeDomainTeam('security');
|
|
|
|
expect(removed).toBe(true);
|
|
expect(manager.getDomainTeam('security')).toBeUndefined();
|
|
expect(adapter.isRegistered('lead-sec')).toBe(false);
|
|
expect(adapter.isRegistered('tm-1')).toBe(false);
|
|
});
|
|
|
|
it('should return false when removing a nonexistent team', () => {
|
|
expect(manager.removeDomainTeam('ghost')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Team Queries
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('getDomainTeam', () => {
|
|
it('should return undefined for an unknown domain', () => {
|
|
expect(manager.getDomainTeam('nope')).toBeUndefined();
|
|
});
|
|
|
|
it('should return a snapshot of an existing team', () => {
|
|
manager.createDomainTeam('qa', 'lead-qa', ['tm-qa']);
|
|
|
|
const team = manager.getDomainTeam('qa');
|
|
|
|
expect(team).toBeDefined();
|
|
expect(team!.domain).toBe('qa');
|
|
expect(team!.leadAgentId).toBe('lead-qa');
|
|
expect(team!.teammateIds).toEqual(['tm-qa']);
|
|
});
|
|
});
|
|
|
|
describe('listDomainTeams', () => {
|
|
it('should list all active domain teams', () => {
|
|
manager.createDomainTeam('d-alpha', 'lead-a', []);
|
|
manager.createDomainTeam('d-beta', 'lead-b', ['tm-b1']);
|
|
|
|
const teams = manager.listDomainTeams();
|
|
|
|
expect(teams).toHaveLength(2);
|
|
const domains = teams.map(t => t.domain).sort();
|
|
expect(domains).toEqual(['d-alpha', 'd-beta']);
|
|
});
|
|
|
|
it('should return an empty list when no teams exist', () => {
|
|
expect(manager.listDomainTeams()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Task Assignment
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('assignTaskToTeam', () => {
|
|
it('should send a task-assignment message to the team lead', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-t']);
|
|
|
|
const result = manager.assignTaskToTeam('testing', 'task-001', { file: 'foo.ts' });
|
|
|
|
expect(result).toBe(true);
|
|
|
|
// The lead should have a pending message
|
|
const unread = adapter.getUnreadCount('lead-t');
|
|
expect(unread).toBe(1);
|
|
});
|
|
|
|
it('should increment the task count on the team', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
manager.assignTaskToTeam('testing', 'task-2', {});
|
|
|
|
const team = manager.getDomainTeam('testing');
|
|
expect(team!.taskCount).toBe(2);
|
|
});
|
|
|
|
it('should return false for a nonexistent team', () => {
|
|
expect(manager.assignTaskToTeam('ghost', 'task-x', {})).toBe(false);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Broadcasting
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('broadcastToDomain', () => {
|
|
it('should broadcast a message to all team members', () => {
|
|
manager.createDomainTeam('security', 'lead-s', ['tm-s1', 'tm-s2']);
|
|
|
|
manager.broadcastToDomain('security', 'alert', { severity: 'high' });
|
|
|
|
// All agents in the domain should get the broadcast
|
|
// (the system sender '__system__' is not in the domain, so everyone gets it)
|
|
expect(adapter.getUnreadCount('lead-s')).toBeGreaterThanOrEqual(1);
|
|
expect(adapter.getUnreadCount('tm-s1')).toBeGreaterThanOrEqual(1);
|
|
expect(adapter.getUnreadCount('tm-s2')).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('should throw when broadcasting to a nonexistent domain team', () => {
|
|
expect(() => {
|
|
manager.broadcastToDomain('nope', 'alert', {});
|
|
}).toThrow(/No domain team found/);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Team Health
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('getTeamHealth', () => {
|
|
it('should return health report with active, idle, and pending counts', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-t1']);
|
|
|
|
// All agents are idle initially (no pending messages)
|
|
const health = manager.getTeamHealth('testing');
|
|
|
|
expect(health).toBeDefined();
|
|
expect(health!.domain).toBe('testing');
|
|
expect(health!.teamSize).toBe(2); // lead + 1 teammate
|
|
expect(health!.activeAgents).toBe(2);
|
|
expect(health!.idleAgents).toBe(2);
|
|
expect(health!.pendingMessages).toBe(0);
|
|
expect(health!.tasksPending).toBe(0);
|
|
expect(health!.tasksCompleted).toBe(0);
|
|
expect(health!.healthy).toBe(true);
|
|
});
|
|
|
|
it('should reflect pending messages in the health report', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
|
|
// Assign a task so the lead has a pending message
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
|
|
const health = manager.getTeamHealth('testing');
|
|
|
|
expect(health!.pendingMessages).toBe(1);
|
|
expect(health!.idleAgents).toBe(0);
|
|
expect(health!.tasksPending).toBe(1);
|
|
});
|
|
|
|
it('should return undefined for an unknown domain', () => {
|
|
expect(manager.getTeamHealth('unknown')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Scaling
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('scaleTeam', () => {
|
|
it('should scale up by adding new teammates', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
|
|
// Default team size is 3, so scale to 3 (lead + 2 teammates)
|
|
const result = manager.scaleTeam('testing', 3);
|
|
|
|
expect(result.previousSize).toBe(1);
|
|
expect(result.newSize).toBe(3);
|
|
expect(result.addedAgents).toHaveLength(2);
|
|
expect(result.removedAgents).toHaveLength(0);
|
|
|
|
// New agents should be registered
|
|
for (const agentId of result.addedAgents) {
|
|
expect(adapter.isRegistered(agentId)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should scale down by removing teammates (not the lead)', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-1', 'tm-2']);
|
|
|
|
const result = manager.scaleTeam('testing', 2);
|
|
|
|
expect(result.previousSize).toBe(3);
|
|
expect(result.newSize).toBe(2);
|
|
expect(result.removedAgents).toHaveLength(1);
|
|
// The lead should still be there
|
|
const team = manager.getDomainTeam('testing');
|
|
expect(team!.leadAgentId).toBe('lead-t');
|
|
});
|
|
|
|
it('should allow explicit scaling beyond defaultTeamSize', () => {
|
|
const mgr = makeManager(adapter, { defaultTeamSize: 3 });
|
|
mgr.createDomainTeam('testing', 'lead-t', []);
|
|
|
|
// defaultTeamSize is for initial creation only, not a hard cap on explicit scaling
|
|
const result = mgr.scaleTeam('testing', 10);
|
|
|
|
expect(result.newSize).toBe(10);
|
|
mgr.dispose();
|
|
});
|
|
|
|
it('should never scale below 1 (the lead)', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-1']);
|
|
|
|
const result = manager.scaleTeam('testing', 0);
|
|
|
|
// Clamped to 1 (lead only)
|
|
expect(result.newSize).toBe(1);
|
|
const team = manager.getDomainTeam('testing');
|
|
expect(team!.leadAgentId).toBe('lead-t');
|
|
expect(team!.teammateIds).toHaveLength(0);
|
|
});
|
|
|
|
it('should throw when scaling a nonexistent team', () => {
|
|
expect(() => {
|
|
manager.scaleTeam('ghost', 5);
|
|
}).toThrow(/No domain team found/);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Idle Teammates
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('getIdleTeammates', () => {
|
|
it('should return idle teammates across all teams', () => {
|
|
manager.createDomainTeam('d1', 'lead-1', ['tm-1a']);
|
|
manager.createDomainTeam('d2', 'lead-2', ['tm-2a']);
|
|
|
|
const idle = manager.getIdleTeammates();
|
|
|
|
// All 4 agents should be idle (no messages pending)
|
|
expect(idle).toHaveLength(4);
|
|
});
|
|
|
|
it('should return idle teammates for a specific domain', () => {
|
|
manager.createDomainTeam('d1', 'lead-1', ['tm-1a']);
|
|
manager.createDomainTeam('d2', 'lead-2', ['tm-2a']);
|
|
|
|
const idle = manager.getIdleTeammates('d1');
|
|
|
|
expect(idle).toHaveLength(2);
|
|
expect(idle).toContain('lead-1');
|
|
expect(idle).toContain('tm-1a');
|
|
});
|
|
|
|
it('should exclude agents with pending messages', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', ['tm-t1']);
|
|
|
|
// Give the lead a pending message
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
|
|
const idle = manager.getIdleTeammates('testing');
|
|
|
|
// Only tm-t1 should be idle; lead-t has a pending message
|
|
expect(idle).toContain('tm-t1');
|
|
expect(idle).not.toContain('lead-t');
|
|
});
|
|
|
|
it('should return empty array for an unknown domain', () => {
|
|
expect(manager.getIdleTeammates('nope')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Rebalancing
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('rebalance', () => {
|
|
it('should produce no moves when teams are at their target size', () => {
|
|
// createDomainTeam caps teammates at maxSize, and target === max,
|
|
// so all teams created through the API start at or below target.
|
|
// Verify rebalance correctly returns zero moves when balanced.
|
|
const freshAdapter = makeAdapter();
|
|
const mgr = makeManager(freshAdapter, { defaultTeamSize: 3 });
|
|
|
|
mgr.createDomainTeam('d1', 'lead-1', ['tm-1a', 'tm-1b']);
|
|
mgr.createDomainTeam('d2', 'lead-2', ['tm-2a', 'tm-2b']);
|
|
|
|
const result = mgr.rebalance();
|
|
|
|
expect(result.moves).toEqual([]);
|
|
expect(result.teamsAffected).toBe(0);
|
|
|
|
mgr.dispose();
|
|
freshAdapter.shutdown();
|
|
});
|
|
|
|
it('should not move agents when all teams are at target', () => {
|
|
const freshAdapter = makeAdapter();
|
|
const mgr = makeManager(freshAdapter, { defaultTeamSize: 2 });
|
|
|
|
mgr.createDomainTeam('d1', 'lead-1', ['tm-1']);
|
|
mgr.createDomainTeam('d2', 'lead-2', ['tm-2']);
|
|
|
|
const result = mgr.rebalance();
|
|
|
|
expect(result.moves).toHaveLength(0);
|
|
expect(result.teamsAffected).toBe(0);
|
|
|
|
mgr.dispose();
|
|
freshAdapter.shutdown();
|
|
});
|
|
|
|
it('should return correct structure with moves and teamsAffected fields', () => {
|
|
const freshAdapter = makeAdapter();
|
|
const mgr = makeManager(freshAdapter, { defaultTeamSize: 3 });
|
|
|
|
mgr.createDomainTeam('alpha', 'lead-a', ['tm-a']);
|
|
mgr.createDomainTeam('beta', 'lead-b', ['tm-b']);
|
|
|
|
const result = mgr.rebalance();
|
|
|
|
expect(result).toHaveProperty('moves');
|
|
expect(result).toHaveProperty('teamsAffected');
|
|
expect(Array.isArray(result.moves)).toBe(true);
|
|
expect(typeof result.teamsAffected).toBe('number');
|
|
|
|
mgr.dispose();
|
|
freshAdapter.shutdown();
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Task Completion
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('markTaskCompleted', () => {
|
|
it('should increment the completed counter', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
manager.assignTaskToTeam('testing', 'task-2', {});
|
|
|
|
manager.markTaskCompleted('testing');
|
|
|
|
const team = manager.getDomainTeam('testing');
|
|
expect(team!.completedCount).toBe(1);
|
|
expect(team!.taskCount).toBe(2);
|
|
});
|
|
|
|
it('should not exceed the task count', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
|
|
manager.markTaskCompleted('testing');
|
|
manager.markTaskCompleted('testing'); // Should be capped
|
|
|
|
const team = manager.getDomainTeam('testing');
|
|
expect(team!.completedCount).toBe(1);
|
|
});
|
|
|
|
it('should return false for unknown domain', () => {
|
|
expect(manager.markTaskCompleted('ghost')).toBe(false);
|
|
});
|
|
|
|
it('should return true for valid domain', () => {
|
|
manager.createDomainTeam('testing', 'lead-t', []);
|
|
manager.assignTaskToTeam('testing', 'task-1', {});
|
|
|
|
expect(manager.markTaskCompleted('testing')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Dispose
|
|
// --------------------------------------------------------------------------
|
|
|
|
describe('dispose', () => {
|
|
it('should clean up all teams and unregister all agents', () => {
|
|
manager.createDomainTeam('d1', 'lead-1', ['tm-1']);
|
|
manager.createDomainTeam('d2', 'lead-2', ['tm-2']);
|
|
|
|
manager.dispose();
|
|
|
|
expect(manager.listDomainTeams()).toEqual([]);
|
|
expect(adapter.isRegistered('lead-1')).toBe(false);
|
|
expect(adapter.isRegistered('tm-1')).toBe(false);
|
|
expect(adapter.isRegistered('lead-2')).toBe(false);
|
|
expect(adapter.isRegistered('tm-2')).toBe(false);
|
|
});
|
|
|
|
it('should be safe to call dispose multiple times', () => {
|
|
manager.createDomainTeam('d1', 'lead-1', []);
|
|
|
|
manager.dispose();
|
|
manager.dispose(); // Should not throw
|
|
|
|
expect(manager.listDomainTeams()).toEqual([]);
|
|
});
|
|
});
|
|
});
|