Files
stack/apps/orchestrator/tests/integration/agent-lifecycle.e2e-spec.ts
Jason Woltje c68b541b6f
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(#226): Remediate code review findings for E2E tests
- Fix CRITICAL: Remove unused imports (Test, TestingModule, CleanupService)
- Fix CRITICAL: Remove unused mockValkeyService declaration
- Fix IMPORTANT: Rename misleading test describe/names to match actual behavior
- Fix IMPORTANT: Verify spawned agents exist before kill-all assertion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 13:26:21 -06:00

243 lines
7.7 KiB
TypeScript

/**
* E2E Test: Full Agent Lifecycle
*
* Tests the complete lifecycle of an agent from spawn to completion/failure.
* Uses mocked services to simulate the full flow without external dependencies.
*
* Lifecycle: spawn → running → completed/failed/killed
*
* Covers issue #226 (ORCH-125)
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { ConfigService } from "@nestjs/config";
import { AgentSpawnerService } from "../../src/spawner/agent-spawner.service";
import { AgentLifecycleService } from "../../src/spawner/agent-lifecycle.service";
import { QueueService } from "../../src/queue/queue.service";
import { KillswitchService } from "../../src/killswitch/killswitch.service";
import { AgentsController } from "../../src/api/agents/agents.controller";
import type { AgentState } from "../../src/valkey/types";
describe("E2E: Full Agent Lifecycle", () => {
let controller: AgentsController;
let spawnerService: AgentSpawnerService;
let lifecycleService: AgentLifecycleService;
let queueService: QueueService;
const mockConfigService = {
get: vi.fn((key: string, defaultValue?: unknown) => {
const config: Record<string, unknown> = {
"orchestrator.claude.apiKey": "test-api-key",
"orchestrator.queue.name": "test-queue",
"orchestrator.queue.maxRetries": 3,
"orchestrator.queue.baseDelay": 100,
"orchestrator.queue.maxDelay": 1000,
"orchestrator.valkey.host": "localhost",
"orchestrator.valkey.port": 6379,
};
return config[key] ?? defaultValue;
}),
};
beforeEach(async () => {
vi.clearAllMocks();
// Create real spawner service with mock config
spawnerService = new AgentSpawnerService(mockConfigService as unknown as ConfigService);
// Create mock lifecycle service
lifecycleService = {
transitionToRunning: vi.fn(),
transitionToCompleted: vi.fn(),
transitionToFailed: vi.fn(),
getAgentLifecycleState: vi.fn(),
} as unknown as AgentLifecycleService;
// Create mock queue service
queueService = {
addTask: vi.fn().mockResolvedValue(undefined),
getStats: vi.fn(),
} as unknown as QueueService;
const killswitchService = {
killAgent: vi.fn(),
killAllAgents: vi.fn(),
} as unknown as KillswitchService;
controller = new AgentsController(
queueService,
spawnerService,
lifecycleService,
killswitchService
);
});
describe("Happy path: spawn → queue → track", () => {
it("should spawn an agent, register it, and queue the task", async () => {
// Step 1: Spawn agent
const spawnResult = await controller.spawn({
taskId: "e2e-task-001",
agentType: "worker",
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: ["US-001"],
skills: ["typescript"],
},
});
expect(spawnResult.agentId).toBeDefined();
expect(spawnResult.status).toBe("spawning");
// Step 2: Verify agent appears in list
const agents = spawnerService.listAgentSessions();
expect(agents).toHaveLength(1);
expect(agents[0].state).toBe("spawning");
expect(agents[0].taskId).toBe("e2e-task-001");
// Step 3: Verify agent status
const session = spawnerService.getAgentSession(spawnResult.agentId);
expect(session).toBeDefined();
expect(session?.state).toBe("spawning");
expect(session?.agentType).toBe("worker");
// Step 4: Verify task was queued
expect(queueService.addTask).toHaveBeenCalledWith(
"e2e-task-001",
expect.objectContaining({
repository: "https://git.example.com/repo.git",
branch: "main",
}),
{ priority: 5 }
);
});
it("should track multiple agents spawned sequentially", async () => {
// Spawn 3 agents
const agents = [];
for (let i = 0; i < 3; i++) {
const result = await controller.spawn({
taskId: `e2e-task-${String(i).padStart(3, "0")}`,
agentType: "worker",
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: [`US-${String(i).padStart(3, "0")}`],
},
});
agents.push(result);
}
// Verify all 3 agents are listed
const listedAgents = spawnerService.listAgentSessions();
expect(listedAgents).toHaveLength(3);
// Verify each agent has unique ID
const agentIds = listedAgents.map((a) => a.agentId);
const uniqueIds = new Set(agentIds);
expect(uniqueIds.size).toBe(3);
});
});
describe("Failure path: spawn → running → failed", () => {
it("should handle agent spawn with invalid parameters", async () => {
await expect(
controller.spawn({
taskId: "",
agentType: "worker",
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: ["US-001"],
},
})
).rejects.toThrow("taskId is required");
});
it("should reject invalid agent types", async () => {
await expect(
controller.spawn({
taskId: "e2e-task-001",
agentType: "invalid" as "worker",
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: ["US-001"],
},
})
).rejects.toThrow("agentType must be one of");
});
});
describe("Multi-type agents", () => {
it("should support worker, reviewer, and tester agent types", async () => {
const types = ["worker", "reviewer", "tester"] as const;
for (const agentType of types) {
const result = await controller.spawn({
taskId: `e2e-task-${agentType}`,
agentType,
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: ["US-001"],
},
});
expect(result.agentId).toBeDefined();
expect(result.status).toBe("spawning");
}
const agents = spawnerService.listAgentSessions();
expect(agents).toHaveLength(3);
const agentTypes = agents.map((a) => a.agentType);
expect(agentTypes).toContain("worker");
expect(agentTypes).toContain("reviewer");
expect(agentTypes).toContain("tester");
});
});
describe("Agent status tracking", () => {
it("should track spawn timestamp", async () => {
const before = new Date();
const result = await controller.spawn({
taskId: "e2e-task-time",
agentType: "worker",
context: {
repository: "https://git.example.com/repo.git",
branch: "main",
workItems: ["US-001"],
},
});
const after = new Date();
const agents = spawnerService.listAgentSessions();
const agent = agents.find((a) => a.agentId === result.agentId);
expect(agent).toBeDefined();
const spawnedAt = new Date(agent!.spawnedAt);
expect(spawnedAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(spawnedAt.getTime()).toBeLessThanOrEqual(after.getTime());
});
it("should return correct status for each agent", async () => {
// Mock lifecycle to return specific states
const mockState: AgentState = {
agentId: "mock-agent-1",
taskId: "e2e-task-001",
status: "running",
startedAt: new Date().toISOString(),
};
(lifecycleService.getAgentLifecycleState as ReturnType<typeof vi.fn>).mockResolvedValue(
mockState
);
const status = await controller.getAgentStatus("mock-agent-1");
expect(status.status).toBe("running");
expect(status.taskId).toBe("e2e-task-001");
});
});
});