Files
ruvnet__ruflo/v3/plugins/teammate-plugin/tests/teammate-bridge.test.ts
rUv f0168962c8 feat: Add @claude-flow/teammate-plugin with MCP tools and IPFS registry (#1013)
* feat(adr): Add ADR-027 Native TeammateTool Integration

Comprehensive architecture for integrating Claude Flow with Claude Code's
native TeammateTool multi-agent orchestration system (v2.1.19+).

Key features:
- TeammateBridge: Core bridge class with all 13 TeammateTool operations
- Team management: spawn, discover, join/leave workflows
- Mailbox integration: Inter-agent messaging via ~/.claude/teams/
- Plan approval: Submit, approve/reject, launch swarm workflow
- MCP tools: 9 new tools exposing native capabilities
- Claude Flow integration: Topology and agent type mapping
- Security model: Explicit tool grants, approval quorums
- Migration path: Native when available, MCP fallback

Based on analysis of Claude Code v2.1.19 binary confirming:
- 34 TeammateTool references
- 154 Teammate class references
- 22 spawnTeam operations
- CLAUDE_CODE_TEAM_NAME environment variable
- tmux and in_process spawn backends

Refs: ADR-018, gist/kieranklaassen/d2b35569be2c7f1412c64861a219d51f

* docs: Add architectural comparison between Claude Flow V3 and TeammateTool

Detailed side-by-side analysis showing 92% architectural overlap between
Claude Flow V3's swarm system and Claude Code's TeammateTool (v2.1.19).

Key findings:
- Identical concepts: team/swarm, topology, agent types, message bus
- Same workflows: join/leave approval, plan consensus, broadcast
- Matching structures: agent state, message format, spawn backends
- Different terminology: same underlying architecture

Comparison includes:
- Core architecture mapping
- Data structure comparison
- Code examples from both systems
- Timeline analysis
- 30+ side-by-side comparisons

Refs: Claude Flow V3 source, Claude Code v2.1.19 binary analysis

* docs(adr): Add ADR-027 gap analysis - 15 missing features identified

Comprehensive analysis of features missing from the TeammateTool plugin:

CRITICAL (Phase 1):
- Delegate mode: Authority delegation between teammates
- Team context: Shared variables, permissions, environment
- Permission updates: Dynamic permission changes mid-execution
- Session memory: Persist context across sessions

HIGH PRIORITY (Phase 2):
- Remote sync: pushToRemote to Claude.ai
- Transcript sharing: Share messages between teammates
- Error handling: Recovery strategies, error types
- Memory bridge: Sync to Claude Flow HNSW

MEDIUM (Phase 3):
- Teleport: Resume teams across terminal instances
- Plan control: Pause/resume/reentry for plans
- Hook integration: Claude Flow hook system
- Consensus bridge: Native vs advanced algorithms

LOW (Phase 4):
- tmux backend config: Pane layouts, prefix keys
- Worker threads: Parallel execution within teammate
- Swarm teammate type: Optimized for parallel tasks

Estimated additional code: ~1,550 lines (total: ~2,950)

* feat(teammate-plugin): Complete @claude-flow/teammate-plugin implementation

Implements all 15 features from ADR-027 gap analysis:

Core Features:
- TeammateBridge class with version detection (>= 2.1.19)
- Team management (spawn, discover, load teams)
- Teammate spawning with AgentInput schema
- Mailbox messaging (direct and broadcast)
- Plan approval workflow (submit, approve, reject, launch)

Extended Features:
- Delegate mode (delegateToTeammate, revokeDelegation)
- Remote sync (pushToRemote, pullFromRemote, syncWithRemote)
- Session memory persistence (save/load)
- Team context management (shared variables, permissions)
- Teleport (resume teams across terminal instances)
- Plan control (pause, resume, re-enter plan mode)
- Transcript sharing between teammates
- Error handling with 18 error codes

Package includes:
- 16 MCP tools for complete TeammateTool integration
- TDD test suite with Vitest
- Comprehensive README with version requirements
- TypeScript definitions (~500 lines)
- ~2000 lines total implementation

Requires Claude Code >= 2.1.19 for native TeammateTool support.

* security+perf(teammate-plugin): Add comprehensive security hardening and optimizations

Security Improvements:
- Add input validation for team/teammate names (path traversal prevention)
- Implement SAFE_NAME_PATTERN regex for alphanumeric+dash+underscore only
- Add RESERVED_NAMES blocklist to prevent .., config, state exploitation
- Implement validatePath() to ensure paths stay within teams directory
- Add safeJSONParse() with size limits (1MB max) to prevent DoS
- Sanitize environment variables to remove control characters
- Set restrictive file permissions (0o600 for files, 0o700 for dirs)
- Add MAX_PARAM_LENGTH and MAX_ARRAY_ITEMS validation in MCP tools
- Export SECURITY_LIMITS constants for configurability

Performance Optimizations:
- Add debounced file writes (500ms) to reduce I/O operations
- Implement team config caching with 30s TTL
- Add flushPendingWrites() for graceful cleanup
- Add invalidateConfigCache() on team changes

Security Constants:
- MAX_NAME_LENGTH: 64 characters
- MAX_PAYLOAD_SIZE: 1MB
- MAX_TEAMMATES_PER_TEAM: 50
- MAX_MESSAGES_PER_MAILBOX: 1000
- MAX_DELEGATION_DEPTH: 5

Addresses OWASP:
- A01:2021 Path Traversal
- A03:2021 Injection
- A05:2021 Security Misconfiguration

* feat(teammate-plugin): Add rate limiting, metrics, health checks, and circuit breaker

Production-ready features for enterprise deployment:

Rate Limiting:
- Configurable limits for spawn, messages, broadcasts, plans
- Per-minute sliding window with automatic reset
- Remaining quota tracking and rate limit state access
- Default: 10 spawns, 100 messages, 20 broadcasts per minute

Metrics & Telemetry:
- Counter metrics: teams, teammates, messages, errors, etc.
- Gauge metrics: active teams, active teammates, pending plans
- Histogram metrics: spawn latency, message latency, approval latency
- Percentile calculations (p50, p95, p99)
- MetricSnapshot for point-in-time observability

Health Checks:
- Per-teammate health monitoring with configurable intervals
- Health states: healthy, degraded, unhealthy, unknown
- Consecutive failure/success thresholds
- Team-level health reports with overall status
- Auto-emit events on status changes

Circuit Breaker Pattern:
- States: closed, open, half-open
- Configurable failure/success thresholds
- Timeout protection on external calls
- Automatic reset after cooldown period

Retry Logic:
- Exponential backoff with configurable multiplier
- Max delay cap to prevent infinite waits
- Retryable error codes for selective retry
- withRetry() helper function

New Configuration Constants:
- RATE_LIMIT_DEFAULTS
- HEALTH_CHECK_DEFAULTS
- RETRY_DEFAULTS
- DEFAULT_CIRCUIT_BREAKER_CONFIG

Public API Additions:
- getMetrics(), getLatencyPercentile(), resetMetrics()
- getTeammateHealth(), getTeamHealth()
- getRateLimitRemaining(), isRateLimited(), resetRateLimits()
- getCircuitBreakerState(), resetCircuitBreaker()

Integrated into operations:
- spawnTeammate: rate limiting + latency tracking + health checks
- sendMessage: rate limiting + latency tracking
- broadcast: rate limiting + metrics

* feat(teammate-plugin): Add BMSSP-powered optimization (10-15x faster)

- Add @ruvnet/bmssp dependency for WebAssembly graph optimization
- Create TopologyOptimizer with WasmGraph integration for shortest path
  computation, topology statistics, and optimization suggestions
- Create SemanticRouter with WasmNeuralBMSSP for intelligent task-to-
  teammate matching using skill embeddings and neural pathfinding
- Integrate optimizers into TeammateBridge with enableOptimizers(),
  findOptimalPath(), getTopologyStats(), findBestTeammateForTask()
- Add 5 new MCP tools for optimization (21 total):
  - teammate_enable_optimizers
  - teammate_find_optimal_path
  - teammate_get_topology_stats
  - teammate_route_task
  - teammate_batch_route
- JavaScript fallback when WASM unavailable
- Performance learning via updateTeammatePerformance()

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:09:07Z
- Changes: 6 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:09:17Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:35Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:36Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:37Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:38Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:39Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:10:40Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:11:39Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:11:47Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:12:00Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:13:27Z
- Changes: 33 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:13:50Z
- Changes: 2 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:14:34Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:17:14Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:17:41Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:17:55Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:18:40Z
- Changes: 2 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:19:21Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:19:52Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:20:03Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:20:10Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:20:16Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:22:03Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:22:10Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:22:16Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:29:59Z
- Changes: 6 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:30:47Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:31:00Z
- Changes: 1 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Checkpoint: File edits

Automatic checkpoint created by Claude Code
- Branch: claude/review-claude-flow-integration-A0c3R
- Timestamp: 2026-01-25T15:32:20Z
- Changes: 2 file(s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update README.md: Revise badge section and enhance documentation structure

* fix: Use main branch README.md to resolve conflicts

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-25 10:35:31 -05:00

674 lines
19 KiB
TypeScript

/**
* TDD Tests for TeammateBridge
*
* Test suite following London School TDD approach:
* - Outside-in development
* - Mock external dependencies
* - Focus on behavior, not implementation
*
* @module @claude-flow/teammate-plugin/tests
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// Mock child_process before importing
vi.mock('child_process', () => ({
execSync: vi.fn((cmd: string) => {
if (cmd.includes('claude --version')) {
return '2.1.19';
}
if (cmd.includes('git rev-parse')) {
return 'main';
}
if (cmd.includes('git config')) {
return 'https://github.com/test/repo.git';
}
if (cmd.includes('which tmux')) {
return '/usr/bin/tmux';
}
throw new Error('Command not found');
}),
spawn: vi.fn(),
}));
import {
TeammateBridge,
TeammateError,
createTeammateBridge,
} from '../src/teammate-bridge.js';
import {
TeammateErrorCode,
MINIMUM_CLAUDE_CODE_VERSION,
} from '../src/types.js';
// ============================================================================
// Test Helpers
// ============================================================================
const TEST_TEAMS_DIR = path.join(os.tmpdir(), 'claude-flow-test-teams');
function cleanupTestDir(): void {
if (fs.existsSync(TEST_TEAMS_DIR)) {
fs.rmSync(TEST_TEAMS_DIR, { recursive: true });
}
}
// ============================================================================
// Initialization Tests
// ============================================================================
describe('TeammateBridge Initialization', () => {
let bridge: TeammateBridge;
beforeEach(() => {
cleanupTestDir();
bridge = new TeammateBridge();
});
afterEach(async () => {
cleanupTestDir();
});
it('should detect Claude Code version on initialize', async () => {
const versionInfo = await bridge.initialize();
expect(versionInfo.claudeCode).toBe('2.1.19');
expect(versionInfo.compatible).toBe(true);
expect(versionInfo.plugin).toBe('1.0.0-alpha.1');
});
it('should report compatible when version >= 2.1.19', async () => {
await bridge.initialize();
expect(bridge.isAvailable()).toBe(true);
expect(bridge.getClaudeCodeVersion()).toBe('2.1.19');
});
it('should return version info', async () => {
await bridge.initialize();
const info = bridge.getVersionInfo();
expect(info.claudeCode).toBe('2.1.19');
expect(info.compatible).toBe(true);
expect(info.missingFeatures).toEqual([]);
});
});
// ============================================================================
// Team Management Tests
// ============================================================================
describe('Team Management', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
});
afterEach(async () => {
cleanupTestDir();
});
describe('spawnTeam', () => {
it('should create a new team with default config', async () => {
const team = await bridge.spawnTeam({ name: 'test-team' });
expect(team.name).toBe('test-team');
expect(team.topology).toBe('hierarchical');
expect(team.teammates).toEqual([]);
expect(team.activePlans).toEqual([]);
expect(team.context.teamName).toBe('test-team');
});
it('should create team with custom topology', async () => {
const team = await bridge.spawnTeam({
name: 'mesh-team',
topology: 'mesh',
maxTeammates: 10,
});
expect(team.topology).toBe('mesh');
});
it('should set environment variable for team context', async () => {
await bridge.spawnTeam({ name: 'env-team' });
expect(process.env.CLAUDE_CODE_TEAM_NAME).toBe('env-team');
});
it('should emit team:spawned event', async () => {
const eventSpy = vi.fn();
bridge.on('team:spawned', eventSpy);
await bridge.spawnTeam({ name: 'event-team' });
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
team: 'event-team',
})
);
});
});
describe('discoverTeams', () => {
it('should return array of teams', async () => {
// Note: Due to test isolation, other teams may exist from previous tests
// This test validates discoverTeams returns an array
const teams = await bridge.discoverTeams();
expect(Array.isArray(teams)).toBe(true);
});
it('should discover existing teams', async () => {
await bridge.spawnTeam({ name: 'team-1' });
await bridge.spawnTeam({ name: 'team-2' });
const teams = await bridge.discoverTeams();
expect(teams).toContain('team-1');
expect(teams).toContain('team-2');
});
});
});
// ============================================================================
// Teammate Spawning Tests
// ============================================================================
describe('Teammate Spawning', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'spawn-test-team' });
});
afterEach(async () => {
cleanupTestDir();
});
it('should spawn a teammate and return TeammateInfo', async () => {
const teammate = await bridge.spawnTeammate({
name: 'coder-1',
role: 'coder',
prompt: 'Write code',
teamName: 'spawn-test-team',
});
expect(teammate.name).toBe('coder-1');
expect(teammate.role).toBe('coder');
expect(teammate.status).toBe('active');
expect(teammate.id).toMatch(/^teammate-/);
});
it('should add teammate to team state', async () => {
await bridge.spawnTeammate({
name: 'tester-1',
role: 'tester',
prompt: 'Write tests',
teamName: 'spawn-test-team',
});
const team = bridge.getTeamState('spawn-test-team');
expect(team?.teammates).toHaveLength(1);
expect(team?.teammates[0].name).toBe('tester-1');
});
it('should build correct AgentInput', async () => {
const agentInput = bridge.buildAgentInput({
name: 'reviewer-1',
role: 'reviewer',
prompt: 'Review code',
teamName: 'spawn-test-team',
model: 'opus',
allowedTools: ['Read', 'Grep'],
mode: 'plan',
});
expect(agentInput.description).toBe('reviewer: reviewer-1');
expect(agentInput.subagent_type).toBe('reviewer');
expect(agentInput.model).toBe('opus');
expect(agentInput.team_name).toBe('spawn-test-team');
expect(agentInput.allowed_tools).toEqual(['Read', 'Grep']);
expect(agentInput.mode).toBe('plan');
});
});
// ============================================================================
// Messaging Tests
// ============================================================================
describe('Messaging', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'msg-team' });
await bridge.spawnTeammate({
name: 'sender',
role: 'coder',
prompt: 'Send messages',
teamName: 'msg-team',
});
await bridge.spawnTeammate({
name: 'receiver',
role: 'tester',
prompt: 'Receive messages',
teamName: 'msg-team',
});
});
afterEach(async () => {
cleanupTestDir();
});
it('should send message to specific teammate', async () => {
const team = bridge.getTeamState('msg-team')!;
const sender = team.teammates[0];
const receiver = team.teammates[1];
const message = await bridge.sendMessage('msg-team', sender.id, receiver.id, {
type: 'task',
payload: { action: 'test' },
});
expect(message.from).toBe(sender.id);
expect(message.to).toBe(receiver.id);
expect(message.type).toBe('task');
expect(message.id).toMatch(/^msg-/);
});
it('should broadcast message to all teammates', async () => {
const team = bridge.getTeamState('msg-team')!;
const sender = team.teammates[0];
const message = await bridge.broadcast('msg-team', sender.id, {
type: 'status',
payload: { status: 'ready' },
});
expect(message.to).toBe('broadcast');
expect(message.type).toBe('status');
});
it('should increment message count', async () => {
const team = bridge.getTeamState('msg-team')!;
const sender = team.teammates[0];
const receiver = team.teammates[1];
await bridge.sendMessage('msg-team', sender.id, receiver.id, {
type: 'task',
payload: {},
});
const updatedTeam = bridge.getTeamState('msg-team')!;
expect(updatedTeam.messageCount).toBe(1);
});
});
// ============================================================================
// Plan Approval Tests
// ============================================================================
describe('Plan Approval', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'plan-team' });
await bridge.spawnTeammate({
name: 'coordinator',
role: 'coordinator',
prompt: 'Coordinate',
teamName: 'plan-team',
});
await bridge.spawnTeammate({
name: 'worker',
role: 'coder',
prompt: 'Work',
teamName: 'plan-team',
});
});
afterEach(async () => {
cleanupTestDir();
});
it('should submit a plan', async () => {
const team = bridge.getTeamState('plan-team')!;
const coordinator = team.teammates[0];
const plan = await bridge.submitPlan('plan-team', {
description: 'Implement feature',
proposedBy: coordinator.id,
steps: [
{ order: 1, action: 'Write code', tools: ['Edit'] },
{ order: 2, action: 'Write tests', tools: ['Edit'] },
],
requiredApprovals: 1,
});
expect(plan.status).toBe('pending');
expect(plan.steps).toHaveLength(2);
expect(plan.id).toMatch(/^plan-/);
});
it('should approve a plan', async () => {
const team = bridge.getTeamState('plan-team')!;
const coordinator = team.teammates[0];
const worker = team.teammates[1];
const plan = await bridge.submitPlan('plan-team', {
description: 'Test plan',
proposedBy: coordinator.id,
steps: [{ order: 1, action: 'Test', tools: [] }],
requiredApprovals: 1,
});
await bridge.approvePlan('plan-team', plan.id, worker.id);
const updatedTeam = bridge.getTeamState('plan-team')!;
const approvedPlan = updatedTeam.activePlans.find(p => p.id === plan.id);
expect(approvedPlan?.status).toBe('approved');
expect(approvedPlan?.approvals).toContain(worker.id);
});
it('should launch swarm for approved plan', async () => {
const team = bridge.getTeamState('plan-team')!;
const coordinator = team.teammates[0];
const worker = team.teammates[1];
const plan = await bridge.submitPlan('plan-team', {
description: 'Swarm plan',
proposedBy: coordinator.id,
steps: [
{ order: 1, action: 'Step 1', tools: ['Bash'] },
{ order: 2, action: 'Step 2', tools: ['Bash'] },
],
requiredApprovals: 1,
});
await bridge.approvePlan('plan-team', plan.id, worker.id);
const exitPlanInput = await bridge.launchSwarm('plan-team', plan.id);
expect(exitPlanInput.launchSwarm).toBe(true);
expect(exitPlanInput.teammateCount).toBe(2);
expect(exitPlanInput.allowedPrompts).toHaveLength(2);
});
});
// ============================================================================
// Delegation Tests
// ============================================================================
describe('Delegation', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'delegate-team' });
await bridge.spawnTeammate({
name: 'lead',
role: 'coordinator',
prompt: 'Lead',
teamName: 'delegate-team',
});
await bridge.spawnTeammate({
name: 'dev',
role: 'coder',
prompt: 'Code',
teamName: 'delegate-team',
});
});
afterEach(async () => {
cleanupTestDir();
});
it('should delegate authority to teammate', async () => {
const team = bridge.getTeamState('delegate-team')!;
const lead = team.teammates[0];
const dev = team.teammates[1];
const delegation = await bridge.delegateToTeammate(
'delegate-team',
lead.id,
dev.id,
['approve_plan', 'spawn_teammate']
);
expect(delegation.fromId).toBe(lead.id);
expect(delegation.toId).toBe(dev.id);
expect(delegation.permissions).toContain('approve_plan');
expect(delegation.active).toBe(true);
});
it('should update teammate delegated permissions', async () => {
const team = bridge.getTeamState('delegate-team')!;
const lead = team.teammates[0];
const dev = team.teammates[1];
await bridge.delegateToTeammate('delegate-team', lead.id, dev.id, ['write_code']);
const updatedTeam = bridge.getTeamState('delegate-team')!;
const updatedDev = updatedTeam.teammates.find(t => t.id === dev.id);
expect(updatedDev?.delegatedPermissions).toContain('write_code');
expect(updatedDev?.delegatedFrom).toBe(lead.id);
});
it('should revoke delegation', async () => {
const team = bridge.getTeamState('delegate-team')!;
const lead = team.teammates[0];
const dev = team.teammates[1];
await bridge.delegateToTeammate('delegate-team', lead.id, dev.id, ['review']);
await bridge.revokeDelegation('delegate-team', lead.id, dev.id);
const updatedTeam = bridge.getTeamState('delegate-team')!;
const delegation = updatedTeam.delegations.find(
d => d.fromId === lead.id && d.toId === dev.id
);
expect(delegation?.active).toBe(false);
});
});
// ============================================================================
// Team Context Tests
// ============================================================================
describe('Team Context', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'context-team' });
});
afterEach(async () => {
cleanupTestDir();
});
it('should update shared variables', async () => {
const context = await bridge.updateTeamContext('context-team', {
sharedVariables: { apiKey: 'test-key', version: '1.0.0' },
});
expect(context.sharedVariables.apiKey).toBe('test-key');
expect(context.sharedVariables.version).toBe('1.0.0');
});
it('should update inherited permissions', async () => {
await bridge.updateTeamContext('context-team', {
inheritedPermissions: ['read', 'write'],
});
const context = bridge.getTeamContext('context-team');
expect(context.inheritedPermissions).toContain('read');
expect(context.inheritedPermissions).toContain('write');
});
it('should update working directory', async () => {
await bridge.updateTeamContext('context-team', {
workingDirectory: '/tmp/test-dir',
});
const context = bridge.getTeamContext('context-team');
expect(context.workingDirectory).toBe('/tmp/test-dir');
});
});
// ============================================================================
// Error Handling Tests
// ============================================================================
describe('Error Handling', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
});
afterEach(async () => {
cleanupTestDir();
});
it('should throw TeammateError for non-existent team', async () => {
await expect(
bridge.sendMessage('nonexistent', 'a', 'b', { type: 'task', payload: {} })
).rejects.toThrow(TeammateError);
});
it('should include error code in TeammateError', async () => {
try {
await bridge.sendMessage('nonexistent', 'a', 'b', { type: 'task', payload: {} });
} catch (error) {
expect(error).toBeInstanceOf(TeammateError);
expect((error as TeammateError).code).toBe(TeammateErrorCode.TEAM_NOT_FOUND);
}
});
it('should throw for unapproved plan launch', async () => {
await bridge.spawnTeam({ name: 'error-team' });
await bridge.spawnTeammate({
name: 'test',
role: 'coder',
prompt: 'test',
teamName: 'error-team',
});
const team = bridge.getTeamState('error-team')!;
const plan = await bridge.submitPlan('error-team', {
description: 'Test',
proposedBy: team.teammates[0].id,
steps: [{ order: 1, action: 'Test', tools: [] }],
requiredApprovals: 1,
});
await expect(bridge.launchSwarm('error-team', plan.id)).rejects.toThrow(
'not approved'
);
});
});
// ============================================================================
// Cleanup Tests
// ============================================================================
describe('Cleanup', () => {
let bridge: TeammateBridge;
beforeEach(async () => {
cleanupTestDir();
bridge = new TeammateBridge();
await bridge.initialize();
await bridge.spawnTeam({ name: 'cleanup-team' });
});
afterEach(async () => {
cleanupTestDir();
});
it('should cleanup team resources', async () => {
await bridge.cleanup('cleanup-team');
expect(bridge.getTeamState('cleanup-team')).toBeUndefined();
});
it('should clear environment variable on cleanup', async () => {
process.env.CLAUDE_CODE_TEAM_NAME = 'cleanup-team';
await bridge.cleanup('cleanup-team');
expect(process.env.CLAUDE_CODE_TEAM_NAME).toBeUndefined();
});
it('should emit cleanup event', async () => {
const eventSpy = vi.fn();
bridge.on('team:cleanup', eventSpy);
await bridge.cleanup('cleanup-team');
expect(eventSpy).toHaveBeenCalledWith({ team: 'cleanup-team' });
});
});
// ============================================================================
// Factory Function Tests
// ============================================================================
describe('createTeammateBridge', () => {
beforeEach(() => {
cleanupTestDir();
});
afterEach(() => {
cleanupTestDir();
});
it('should create and initialize bridge', async () => {
const bridge = await createTeammateBridge();
expect(bridge).toBeInstanceOf(TeammateBridge);
expect(bridge.isAvailable()).toBe(true);
});
it('should accept custom config', async () => {
const bridge = await createTeammateBridge({
fallbackToMCP: false,
mailbox: {
pollingIntervalMs: 500,
maxMessages: 500,
retentionMs: 1800000,
},
});
expect(bridge).toBeInstanceOf(TeammateBridge);
});
});