test(#226,#227,#228): Add E2E integration tests for agent orchestration
Add comprehensive E2E test suites covering: - Full agent lifecycle (spawn → running → completed/failed) - 7 tests - Killswitch emergency stop mechanism (single/all/partial) - 5 tests - Concurrent agent spawning and isolation - 5 tests Includes vitest config for integration test runner with 30s timeout. Fixes #226 Fixes #227 Fixes #228 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
154
apps/orchestrator/tests/integration/killswitch.e2e-spec.ts
Normal file
154
apps/orchestrator/tests/integration/killswitch.e2e-spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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 { CleanupService } from "../../src/killswitch/cleanup.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<string, unknown> = {
|
||||
"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<typeof vi.fn>).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
|
||||
for (let i = 0; i < 3; i++) {
|
||||
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)}`],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Kill all
|
||||
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<typeof vi.fn>).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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user