import { AgentsController } from "./agents.controller"; import { QueueService } from "../../queue/queue.service"; import { AgentSpawnerService } from "../../spawner/agent-spawner.service"; import { AgentLifecycleService } from "../../spawner/agent-lifecycle.service"; import { KillswitchService } from "../../killswitch/killswitch.service"; import { AgentEventsService } from "./agent-events.service"; import { AgentMessagesService } from "./agent-messages.service"; import { AgentControlService } from "./agent-control.service"; import { AgentTreeService } from "./agent-tree.service"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; describe("AgentsController", () => { let controller: AgentsController; let queueService: { addTask: ReturnType; }; let spawnerService: { spawnAgent: ReturnType; listAgentSessions: ReturnType; getAgentSession: ReturnType; }; let lifecycleService: { getAgentLifecycleState: ReturnType; registerSpawnedAgent: ReturnType; }; let killswitchService: { killAgent: ReturnType; killAllAgents: ReturnType; }; let eventsService: { subscribe: ReturnType; getInitialSnapshot: ReturnType; createHeartbeat: ReturnType; getRecentEvents: ReturnType; }; let messagesService: { getMessages: ReturnType; getReplayMessages: ReturnType; getMessagesAfter: ReturnType; }; let controlService: { injectMessage: ReturnType; pauseAgent: ReturnType; resumeAgent: ReturnType; }; let treeService: { getTree: ReturnType; }; beforeEach(() => { // Create mock services queueService = { addTask: vi.fn().mockResolvedValue(undefined), }; spawnerService = { spawnAgent: vi.fn(), listAgentSessions: vi.fn(), getAgentSession: vi.fn(), }; lifecycleService = { getAgentLifecycleState: vi.fn(), registerSpawnedAgent: vi.fn().mockResolvedValue(undefined), }; killswitchService = { killAgent: vi.fn(), killAllAgents: vi.fn(), }; eventsService = { subscribe: vi.fn().mockReturnValue(() => {}), getInitialSnapshot: vi.fn().mockResolvedValue({ type: "stream.snapshot", timestamp: new Date().toISOString(), agents: 0, tasks: 0, }), createHeartbeat: vi.fn().mockReturnValue({ type: "task.processing", timestamp: new Date().toISOString(), data: { heartbeat: true }, }), getRecentEvents: vi.fn().mockReturnValue([]), }; messagesService = { getMessages: vi.fn(), getReplayMessages: vi.fn().mockResolvedValue([]), getMessagesAfter: vi.fn().mockResolvedValue([]), }; controlService = { injectMessage: vi.fn().mockResolvedValue(undefined), pauseAgent: vi.fn().mockResolvedValue(undefined), resumeAgent: vi.fn().mockResolvedValue(undefined), }; treeService = { getTree: vi.fn().mockResolvedValue([]), }; // Create controller with mocked services controller = new AgentsController( queueService as unknown as QueueService, spawnerService as unknown as AgentSpawnerService, lifecycleService as unknown as AgentLifecycleService, killswitchService as unknown as KillswitchService, eventsService as unknown as AgentEventsService, messagesService as unknown as AgentMessagesService, controlService as unknown as AgentControlService, treeService as unknown as AgentTreeService ); }); afterEach(() => { vi.clearAllMocks(); }); it("should be defined", () => { expect(controller).toBeDefined(); }); describe("getAgentTree", () => { it("should return tree entries", async () => { const entries = [ { sessionId: "agent-1", parentSessionId: null, status: "running", agentType: "worker", taskSource: "internal", spawnedAt: "2026-03-07T00:00:00.000Z", completedAt: null, }, ]; treeService.getTree.mockResolvedValue(entries); await expect(controller.getAgentTree()).resolves.toEqual(entries); expect(treeService.getTree).toHaveBeenCalledTimes(1); }); }); describe("listAgents", () => { it("should return empty array when no agents exist", () => { // Arrange spawnerService.listAgentSessions.mockReturnValue([]); // Act const result = controller.listAgents(); // Assert expect(spawnerService.listAgentSessions).toHaveBeenCalled(); expect(result).toEqual([]); }); it("should return all agent sessions with mapped status", () => { // Arrange const sessions = [ { agentId: "agent-1", taskId: "task-1", agentType: "worker" as const, state: "running" as const, context: { repository: "repo", branch: "main", workItems: [], }, spawnedAt: new Date("2026-02-05T12:00:00Z"), }, { agentId: "agent-2", taskId: "task-2", agentType: "reviewer" as const, state: "completed" as const, context: { repository: "repo", branch: "main", workItems: [], }, spawnedAt: new Date("2026-02-05T11:00:00Z"), completedAt: new Date("2026-02-05T11:30:00Z"), }, { agentId: "agent-3", taskId: "task-3", agentType: "tester" as const, state: "failed" as const, context: { repository: "repo", branch: "main", workItems: [], }, spawnedAt: new Date("2026-02-05T10:00:00Z"), error: "Test execution failed", }, ]; spawnerService.listAgentSessions.mockReturnValue(sessions); // Act const result = controller.listAgents(); // Assert expect(spawnerService.listAgentSessions).toHaveBeenCalled(); expect(result).toHaveLength(3); expect(result[0]).toEqual({ agentId: "agent-1", taskId: "task-1", status: "running", agentType: "worker", spawnedAt: "2026-02-05T12:00:00.000Z", completedAt: undefined, error: undefined, }); expect(result[1]).toEqual({ agentId: "agent-2", taskId: "task-2", status: "completed", agentType: "reviewer", spawnedAt: "2026-02-05T11:00:00.000Z", completedAt: "2026-02-05T11:30:00.000Z", error: undefined, }); expect(result[2]).toEqual({ agentId: "agent-3", taskId: "task-3", status: "failed", agentType: "tester", spawnedAt: "2026-02-05T10:00:00.000Z", completedAt: undefined, error: "Test execution failed", }); }); it("should handle errors gracefully", () => { // Arrange spawnerService.listAgentSessions.mockImplementation(() => { throw new Error("Service unavailable"); }); // Act & Assert expect(() => controller.listAgents()).toThrow("Failed to list agents: Service unavailable"); }); }); describe("spawn", () => { const validRequest = { taskId: "task-123", agentType: "worker" as const, context: { repository: "https://github.com/org/repo.git", branch: "main", workItems: ["US-001", "US-002"], skills: ["typescript", "nestjs"], }, }; it("should spawn agent and queue task successfully", async () => { // Arrange const agentId = "agent-abc-123"; const spawnedAt = new Date(); spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt, }); queueService.addTask.mockResolvedValue(undefined); // Act const result = await controller.spawn(validRequest); // Assert expect(spawnerService.spawnAgent).toHaveBeenCalledWith(validRequest); expect(queueService.addTask).toHaveBeenCalledWith(validRequest.taskId, validRequest.context, { priority: 5, }); expect(lifecycleService.registerSpawnedAgent).toHaveBeenCalledWith( agentId, validRequest.taskId ); expect(result).toEqual({ agentId, status: "spawning", }); }); it("should return queued status when agent is queued", async () => { // Arrange const agentId = "agent-abc-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); queueService.addTask.mockResolvedValue(undefined); // Act const result = await controller.spawn(validRequest); // Assert expect(result.status).toBe("spawning"); }); it("should handle reviewer agent type", async () => { // Arrange const reviewerRequest = { ...validRequest, agentType: "reviewer" as const, }; const agentId = "agent-reviewer-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); queueService.addTask.mockResolvedValue(undefined); // Act const result = await controller.spawn(reviewerRequest); // Assert expect(spawnerService.spawnAgent).toHaveBeenCalledWith(reviewerRequest); expect(result.agentId).toBe(agentId); }); it("should handle tester agent type", async () => { // Arrange const testerRequest = { ...validRequest, agentType: "tester" as const, }; const agentId = "agent-tester-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); queueService.addTask.mockResolvedValue(undefined); // Act const result = await controller.spawn(testerRequest); // Assert expect(spawnerService.spawnAgent).toHaveBeenCalledWith(testerRequest); expect(result.agentId).toBe(agentId); }); it("should handle missing optional skills", async () => { // Arrange const requestWithoutSkills = { taskId: "task-123", agentType: "worker" as const, context: { repository: "https://github.com/org/repo.git", branch: "main", workItems: ["US-001"], }, }; const agentId = "agent-abc-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); queueService.addTask.mockResolvedValue(undefined); // Act const result = await controller.spawn(requestWithoutSkills); // Assert expect(result.agentId).toBe(agentId); }); it("should propagate errors from spawner service", async () => { // Arrange const error = new Error("Spawner failed"); spawnerService.spawnAgent.mockImplementation(() => { throw error; }); // Act & Assert await expect(controller.spawn(validRequest)).rejects.toThrow("Spawner failed"); expect(queueService.addTask).not.toHaveBeenCalled(); }); it("should propagate errors from queue service", async () => { // Arrange const agentId = "agent-abc-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); const error = new Error("Queue failed"); queueService.addTask.mockRejectedValue(error); // Act & Assert await expect(controller.spawn(validRequest)).rejects.toThrow("Queue failed"); }); it("should use default priority of 5", async () => { // Arrange const agentId = "agent-abc-123"; spawnerService.spawnAgent.mockReturnValue({ agentId, state: "spawning", spawnedAt: new Date(), }); queueService.addTask.mockResolvedValue(undefined); // Act await controller.spawn(validRequest); // Assert expect(queueService.addTask).toHaveBeenCalledWith(validRequest.taskId, validRequest.context, { priority: 5, }); }); }); describe("agent control endpoints", () => { const agentId = "0b64079f-4487-42b9-92eb-cf8ea0042a64"; it("should inject an operator message", async () => { const req = { apiKey: "control-key" }; const result = await controller.injectAgentMessage( agentId, { message: "pause and summarize" }, req ); expect(controlService.injectMessage).toHaveBeenCalledWith( agentId, "control-key", "pause and summarize" ); expect(result).toEqual({ message: `Message injected into agent ${agentId}` }); }); it("should default operator id when request api key is missing", async () => { await controller.injectAgentMessage(agentId, { message: "continue" }, {}); expect(controlService.injectMessage).toHaveBeenCalledWith(agentId, "operator", "continue"); }); it("should pause an agent", async () => { const result = await controller.pauseAgent(agentId, {}, { apiKey: "ops-user" }); expect(controlService.pauseAgent).toHaveBeenCalledWith(agentId, "ops-user"); expect(result).toEqual({ message: `Agent ${agentId} paused` }); }); it("should resume an agent", async () => { const result = await controller.resumeAgent(agentId, {}, { apiKey: "ops-user" }); expect(controlService.resumeAgent).toHaveBeenCalledWith(agentId, "ops-user"); expect(result).toEqual({ message: `Agent ${agentId} resumed` }); }); }); describe("getAgentMessages", () => { it("should return paginated message history", async () => { const agentId = "0b64079f-4487-42b9-92eb-cf8ea0042a64"; const query = { limit: 25, skip: 10, }; const response = { messages: [ { id: "msg-1", sessionId: agentId, role: "agent", content: "hello", provider: "internal", timestamp: new Date("2026-03-07T03:00:00.000Z"), metadata: {}, }, ], total: 101, }; messagesService.getMessages.mockResolvedValue(response); const result = await controller.getAgentMessages(agentId, query); expect(messagesService.getMessages).toHaveBeenCalledWith(agentId, 25, 10); expect(result).toEqual(response); }); it("should use default pagination values", async () => { const agentId = "0b64079f-4487-42b9-92eb-cf8ea0042a64"; const query = { limit: 50, skip: 0, }; messagesService.getMessages.mockResolvedValue({ messages: [], total: 0 }); await controller.getAgentMessages(agentId, query); expect(messagesService.getMessages).toHaveBeenCalledWith(agentId, 50, 0); }); }); describe("getRecentEvents", () => { it("should return recent events with default limit", () => { eventsService.getRecentEvents.mockReturnValue([ { type: "task.completed", timestamp: "2026-02-17T15:00:00.000Z", taskId: "task-123", }, ]); const result = controller.getRecentEvents(); expect(eventsService.getRecentEvents).toHaveBeenCalledWith(100); expect(result).toEqual({ events: [ { type: "task.completed", timestamp: "2026-02-17T15:00:00.000Z", taskId: "task-123", }, ], }); }); it("should parse and pass custom limit", () => { controller.getRecentEvents("25"); expect(eventsService.getRecentEvents).toHaveBeenCalledWith(25); }); it("should fallback to default when limit is invalid", () => { controller.getRecentEvents("invalid"); expect(eventsService.getRecentEvents).toHaveBeenCalledWith(100); }); }); });