/** * E2E Test: Killswitch * * Tests the emergency stop mechanism for terminating agents. * Verifies single agent kill, kill-all, and cleanup operations. * * Covers issue #227 (ORCH-126) */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { KillswitchService } from "../../src/killswitch/killswitch.service"; 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 { ConfigService } from "@nestjs/config"; describe("E2E: Killswitch", () => { let controller: AgentsController; let spawnerService: AgentSpawnerService; let killswitchService: KillswitchService; 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); killswitchService = { killAgent: vi.fn().mockResolvedValue(undefined), killAllAgents: vi.fn().mockResolvedValue({ total: 3, killed: 3, failed: 0, }), } as unknown as KillswitchService; const queueService = { addTask: vi.fn().mockResolvedValue(undefined), } as unknown as QueueService; const lifecycleService = { getAgentLifecycleState: vi.fn(), } as unknown as AgentLifecycleService; controller = new AgentsController( queueService, spawnerService, lifecycleService, killswitchService ); }); describe("Single agent kill", () => { it("should kill a single agent by ID", async () => { // Spawn an agent first const spawnResult = await controller.spawn({ taskId: "kill-test-001", agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: "main", workItems: ["US-001"], }, }); // Kill the agent const result = await controller.killAgent(spawnResult.agentId); expect(result.message).toContain("killed successfully"); expect(killswitchService.killAgent).toHaveBeenCalledWith(spawnResult.agentId); }); it("should handle kill of non-existent agent gracefully", async () => { (killswitchService.killAgent as ReturnType).mockRejectedValue( new Error("Agent not found") ); await expect(controller.killAgent("non-existent")).rejects.toThrow("Agent not found"); }); }); describe("Kill all agents", () => { it("should kill all active agents", async () => { // Spawn multiple agents to verify they exist before kill-all const spawned = []; for (let i = 0; i < 3; i++) { const result = await controller.spawn({ taskId: `kill-all-test-${String(i)}`, agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: "main", workItems: [`US-${String(i)}`], }, }); spawned.push(result); } // Verify agents were spawned expect(spawnerService.listAgentSessions()).toHaveLength(3); // Kill all (mock returns hardcoded result matching spawn count) const result = await controller.killAllAgents(); expect(result.total).toBe(3); expect(result.killed).toBe(3); expect(result.failed).toBe(0); expect(killswitchService.killAllAgents).toHaveBeenCalled(); }); it("should report partial failures in kill-all", async () => { (killswitchService.killAllAgents as ReturnType).mockResolvedValue({ total: 3, killed: 2, failed: 1, errors: ["Agent abc123 unresponsive"], }); const result = await controller.killAllAgents(); expect(result.total).toBe(3); expect(result.killed).toBe(2); expect(result.failed).toBe(1); expect(result.errors).toContain("Agent abc123 unresponsive"); }); }); describe("Kill during lifecycle states", () => { it("should be able to kill agent in spawning state", async () => { const spawnResult = await controller.spawn({ taskId: "kill-spawning-test", agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: "main", workItems: ["US-001"], }, }); // Verify agent is spawning const agents = spawnerService.listAgentSessions(); const agent = agents.find((a) => a.agentId === spawnResult.agentId); expect(agent?.state).toBe("spawning"); // Kill should succeed even in spawning state const result = await controller.killAgent(spawnResult.agentId); expect(result.message).toContain("killed successfully"); }); }); });