/** * E2E Test: Concurrent Agents * * Tests multiple agents running concurrently with proper isolation. * Verifies agent-level isolation, queue management, and concurrent operations. * * Covers issue #228 (ORCH-127) */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { AgentSpawnerService } from "../../src/spawner/agent-spawner.service"; import { AgentsController } from "../../src/api/agents/agents.controller"; import { QueueService } from "../../src/queue/queue.service"; import { AgentLifecycleService } from "../../src/spawner/agent-lifecycle.service"; import { KillswitchService } from "../../src/killswitch/killswitch.service"; import { ConfigService } from "@nestjs/config"; describe("E2E: Concurrent Agents", () => { let controller: AgentsController; let spawnerService: AgentSpawnerService; const mockConfigService = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.claude.apiKey": "test-api-key", }; return config[key] ?? defaultValue; }), }; beforeEach(() => { vi.clearAllMocks(); spawnerService = new AgentSpawnerService(mockConfigService as unknown as ConfigService); const queueService = { addTask: vi.fn().mockResolvedValue(undefined), } as unknown as QueueService; const lifecycleService = { getAgentLifecycleState: vi.fn(), } as unknown as AgentLifecycleService; const killswitchService = { killAgent: vi.fn(), killAllAgents: vi.fn(), } as unknown as KillswitchService; controller = new AgentsController( queueService, spawnerService, lifecycleService, killswitchService ); }); describe("Concurrent spawning", () => { it("should spawn multiple agents simultaneously without conflicts", async () => { // Spawn 5 agents in parallel const spawnPromises = Array.from({ length: 5 }, (_, i) => controller.spawn({ taskId: `concurrent-task-${String(i)}`, agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: `feature/task-${String(i)}`, workItems: [`US-${String(i).padStart(3, "0")}`], }, }) ); const results = await Promise.all(spawnPromises); // All should succeed expect(results).toHaveLength(5); results.forEach((result) => { expect(result.agentId).toBeDefined(); expect(result.status).toBe("spawning"); }); // All IDs should be unique const ids = new Set(results.map((r) => r.agentId)); expect(ids.size).toBe(5); // All should appear in the list const agents = spawnerService.listAgentSessions(); expect(agents).toHaveLength(5); }); it("should assign unique IDs to every agent even under concurrent load", async () => { const allIds = new Set(); const batchSize = 10; // Spawn agents in batches for (let batch = 0; batch < 3; batch++) { const promises = Array.from({ length: batchSize }, (_, i) => controller.spawn({ taskId: `batch-${String(batch)}-task-${String(i)}`, agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: "main", workItems: [`US-${String(batch * batchSize + i)}`], }, }) ); const results = await Promise.all(promises); results.forEach((r) => allIds.add(r.agentId)); } // All 30 IDs should be unique expect(allIds.size).toBe(30); // All 30 should be listed const agents = spawnerService.listAgentSessions(); expect(agents).toHaveLength(30); }); }); describe("Mixed agent types concurrently", () => { it("should handle mixed worker/reviewer/tester agents concurrently", async () => { const types = ["worker", "reviewer", "tester"] as const; const promises = types.flatMap((agentType, typeIndex) => Array.from({ length: 3 }, (_, i) => controller.spawn({ taskId: `mixed-${agentType}-${String(i)}`, agentType, context: { repository: "https://git.example.com/repo.git", branch: `branch-${String(typeIndex * 3 + i)}`, workItems: [`US-${String(typeIndex * 3 + i)}`], }, }) ) ); const results = await Promise.all(promises); expect(results).toHaveLength(9); const agents = spawnerService.listAgentSessions(); expect(agents).toHaveLength(9); // Verify type distribution const typeCounts = agents.reduce( (acc, a) => { acc[a.agentType] = (acc[a.agentType] ?? 0) + 1; return acc; }, {} as Record ); expect(typeCounts["worker"]).toBe(3); expect(typeCounts["reviewer"]).toBe(3); expect(typeCounts["tester"]).toBe(3); }); }); describe("Agent isolation", () => { it("should isolate agent contexts from each other", async () => { const agent1 = await controller.spawn({ taskId: "isolated-task-1", agentType: "worker", context: { repository: "https://git.example.com/repo-a.git", branch: "main", workItems: ["US-001"], skills: ["typescript"], }, }); const agent2 = await controller.spawn({ taskId: "isolated-task-2", agentType: "reviewer", context: { repository: "https://git.example.com/repo-b.git", branch: "develop", workItems: ["US-002"], skills: ["python"], }, }); // Verify sessions are independent const session1 = spawnerService.getAgentSession(agent1.agentId); const session2 = spawnerService.getAgentSession(agent2.agentId); expect(session1?.context.repository).toBe("https://git.example.com/repo-a.git"); expect(session2?.context.repository).toBe("https://git.example.com/repo-b.git"); expect(session1?.context.branch).toBe("main"); expect(session2?.context.branch).toBe("develop"); }); it("should not leak state between concurrent agent operations", async () => { // Spawn agents with different task contexts const spawnPromises = Array.from({ length: 5 }, (_, i) => controller.spawn({ taskId: `leak-test-${String(i)}`, agentType: "worker", context: { repository: `https://git.example.com/repo-${String(i)}.git`, branch: `branch-${String(i)}`, workItems: [`US-${String(i).padStart(3, "0")}`], }, }) ); const results = await Promise.all(spawnPromises); // Verify each agent has its own isolated context results.forEach((result, i) => { const session = spawnerService.getAgentSession(result.agentId); expect(session?.taskId).toBe(`leak-test-${String(i)}`); expect(session?.context.repository).toBe(`https://git.example.com/repo-${String(i)}.git`); expect(session?.context.branch).toBe(`branch-${String(i)}`); }); }); }); });