import { ConfigService } from "@nestjs/config"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { AgentSpawnerService } from "./agent-spawner.service"; import { SpawnAgentRequest } from "./types/agent-spawner.types"; describe("AgentSpawnerService", () => { let service: AgentSpawnerService; let mockConfigService: ConfigService; beforeEach(() => { // Create mock ConfigService mockConfigService = { get: vi.fn((key: string) => { if (key === "orchestrator.claude.apiKey") { return "test-api-key"; } return undefined; }), } as unknown as ConfigService; // Create service with mock service = new AgentSpawnerService(mockConfigService); }); describe("constructor", () => { it("should be defined", () => { expect(service).toBeDefined(); }); it("should initialize with Claude API key from config", () => { expect(mockConfigService.get).toHaveBeenCalledWith("orchestrator.claude.apiKey"); }); it("should throw error if Claude API key is missing", () => { const badConfigService = { get: vi.fn(() => undefined), } as unknown as ConfigService; expect(() => new AgentSpawnerService(badConfigService)).toThrow( "CLAUDE_API_KEY is not configured" ); }); }); describe("spawnAgent", () => { const validRequest: SpawnAgentRequest = { taskId: "task-123", agentType: "worker", context: { repository: "https://github.com/test/repo.git", branch: "main", workItems: ["Implement feature X"], }, }; it("should spawn an agent and return agentId", () => { const response = service.spawnAgent(validRequest); expect(response).toBeDefined(); expect(response.agentId).toBeDefined(); expect(typeof response.agentId).toBe("string"); expect(response.state).toBe("spawning"); expect(response.spawnedAt).toBeInstanceOf(Date); }); it("should generate unique agentId for each spawn", () => { const response1 = service.spawnAgent(validRequest); const response2 = service.spawnAgent(validRequest); expect(response1.agentId).not.toBe(response2.agentId); }); it("should track agent session", () => { const response = service.spawnAgent(validRequest); const session = service.getAgentSession(response.agentId); expect(session).toBeDefined(); expect(session?.agentId).toBe(response.agentId); expect(session?.taskId).toBe(validRequest.taskId); expect(session?.agentType).toBe(validRequest.agentType); expect(session?.state).toBe("spawning"); }); it("should validate taskId is provided", () => { const invalidRequest = { ...validRequest, taskId: "", }; expect(() => service.spawnAgent(invalidRequest)).toThrow("taskId is required"); }); it("should validate agentType is valid", () => { const invalidRequest = { ...validRequest, agentType: "invalid" as unknown as "worker", }; expect(() => service.spawnAgent(invalidRequest)).toThrow( "agentType must be one of: worker, reviewer, tester" ); }); it("should validate context.repository is provided", () => { const invalidRequest = { ...validRequest, context: { ...validRequest.context, repository: "", }, }; expect(() => service.spawnAgent(invalidRequest)).toThrow("context.repository is required"); }); it("should validate context.branch is provided", () => { const invalidRequest = { ...validRequest, context: { ...validRequest.context, branch: "", }, }; expect(() => service.spawnAgent(invalidRequest)).toThrow("context.branch is required"); }); it("should validate context.workItems is not empty", () => { const invalidRequest = { ...validRequest, context: { ...validRequest.context, workItems: [], }, }; expect(() => service.spawnAgent(invalidRequest)).toThrow( "context.workItems must not be empty" ); }); it("should accept optional skills in context", () => { const requestWithSkills: SpawnAgentRequest = { ...validRequest, context: { ...validRequest.context, skills: ["typescript", "nestjs"], }, }; const response = service.spawnAgent(requestWithSkills); const session = service.getAgentSession(response.agentId); expect(session?.context.skills).toEqual(["typescript", "nestjs"]); }); it("should accept optional options", () => { const requestWithOptions: SpawnAgentRequest = { ...validRequest, options: { sandbox: true, timeout: 3600000, maxRetries: 3, }, }; const response = service.spawnAgent(requestWithOptions); const session = service.getAgentSession(response.agentId); expect(session?.options).toEqual({ sandbox: true, timeout: 3600000, maxRetries: 3, }); }); it("should handle spawn errors gracefully", () => { // Mock Claude SDK to throw error const errorRequest = { ...validRequest, context: { ...validRequest.context, repository: "invalid-repo-that-will-fail", }, }; // For now, this should not throw but handle gracefully // We'll implement error handling in the service const response = service.spawnAgent(errorRequest); expect(response.agentId).toBeDefined(); }); }); describe("getAgentSession", () => { it("should return undefined for non-existent agentId", () => { const session = service.getAgentSession("non-existent-id"); expect(session).toBeUndefined(); }); it("should return session for existing agentId", () => { const request: SpawnAgentRequest = { taskId: "task-123", agentType: "worker", context: { repository: "https://github.com/test/repo.git", branch: "main", workItems: ["Implement feature X"], }, }; const response = service.spawnAgent(request); const session = service.getAgentSession(response.agentId); expect(session).toBeDefined(); expect(session?.agentId).toBe(response.agentId); }); }); describe("listAgentSessions", () => { it("should return empty array when no agents spawned", () => { const sessions = service.listAgentSessions(); expect(sessions).toEqual([]); }); it("should return all spawned agent sessions", () => { const request1: SpawnAgentRequest = { taskId: "task-1", agentType: "worker", context: { repository: "https://github.com/test/repo1.git", branch: "main", workItems: ["Task 1"], }, }; const request2: SpawnAgentRequest = { taskId: "task-2", agentType: "reviewer", context: { repository: "https://github.com/test/repo2.git", branch: "develop", workItems: ["Task 2"], }, }; service.spawnAgent(request1); service.spawnAgent(request2); const sessions = service.listAgentSessions(); expect(sessions).toHaveLength(2); expect(sessions[0].agentType).toBe("worker"); expect(sessions[1].agentType).toBe("reviewer"); }); }); });