import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { CleanupService } from "./cleanup.service"; import { DockerSandboxService } from "../spawner/docker-sandbox.service"; import { WorktreeManagerService } from "../git/worktree-manager.service"; import { ValkeyService } from "../valkey/valkey.service"; import type { AgentState } from "../valkey/types/state.types"; describe("CleanupService", () => { let service: CleanupService; let mockDockerService: { cleanup: ReturnType; isEnabled: ReturnType; }; let mockWorktreeService: { cleanupWorktree: ReturnType; }; let mockValkeyService: { deleteAgentState: ReturnType; publishEvent: ReturnType; }; const mockAgentState: AgentState = { agentId: "agent-123", status: "running", taskId: "task-456", startedAt: new Date().toISOString(), metadata: { containerId: "container-abc", repository: "/path/to/repo", }, }; beforeEach(() => { // Create mocks mockDockerService = { cleanup: vi.fn(), isEnabled: vi.fn().mockReturnValue(true), }; mockWorktreeService = { cleanupWorktree: vi.fn(), }; mockValkeyService = { deleteAgentState: vi.fn(), publishEvent: vi.fn(), }; service = new CleanupService( mockDockerService as unknown as DockerSandboxService, mockWorktreeService as unknown as WorktreeManagerService, mockValkeyService as unknown as ValkeyService ); }); afterEach(() => { vi.clearAllMocks(); }); describe("cleanup", () => { it("should perform full cleanup successfully", async () => { // Arrange mockDockerService.cleanup.mockResolvedValue(undefined); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: true }, worktree: { success: true }, state: { success: true }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: true, worktree: true, state: true, }, }) ); }); it("should continue cleanup if Docker cleanup fails", async () => { // Arrange mockDockerService.cleanup.mockRejectedValue(new Error("Docker error")); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: false, error: "Docker error" }, worktree: { success: true }, state: { success: true }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: false, // Failed worktree: true, state: true, }, }) ); }); it("should continue cleanup if worktree cleanup fails", async () => { // Arrange mockDockerService.cleanup.mockResolvedValue(undefined); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: false, error: "Git error", }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: true }, worktree: { success: false, error: "Git error" }, state: { success: true }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: true, worktree: false, // Failed state: true, }, }) ); }); it("should continue cleanup if state deletion fails", async () => { // Arrange mockDockerService.cleanup.mockResolvedValue(undefined); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockRejectedValue(new Error("Valkey error")); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: true }, worktree: { success: true }, state: { success: false, error: "Valkey error" }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: true, worktree: true, state: false, // Failed }, }) ); }); it("should skip Docker cleanup if no containerId", async () => { // Arrange const stateWithoutContainer: AgentState = { ...mockAgentState, metadata: { repository: "/path/to/repo", }, }; mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(stateWithoutContainer); // Assert expect(result).toEqual({ docker: { success: false }, worktree: { success: true }, state: { success: true }, }); expect(mockDockerService.cleanup).not.toHaveBeenCalled(); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: false, // Skipped (no containerId) worktree: true, state: true, }, }) ); }); it("should skip Docker cleanup if sandbox is disabled", async () => { // Arrange mockDockerService.isEnabled.mockReturnValue(false); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: false }, worktree: { success: true }, state: { success: true }, }); expect(mockDockerService.cleanup).not.toHaveBeenCalled(); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: false, // Skipped (sandbox disabled) worktree: true, state: true, }, }) ); }); it("should skip worktree cleanup if no repository", async () => { // Arrange const stateWithoutRepo: AgentState = { ...mockAgentState, metadata: { containerId: "container-abc", }, }; mockDockerService.cleanup.mockResolvedValue(undefined); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(stateWithoutRepo); // Assert expect(result).toEqual({ docker: { success: true }, worktree: { success: false }, state: { success: true }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).not.toHaveBeenCalled(); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: true, worktree: false, // Skipped (no repository) state: true, }, }) ); }); it("should handle agent state with no metadata", async () => { // Arrange const stateWithoutMetadata: AgentState = { agentId: "agent-123", status: "running", taskId: "task-456", startedAt: new Date().toISOString(), }; mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act const result = await service.cleanup(stateWithoutMetadata); // Assert expect(result).toEqual({ docker: { success: false }, worktree: { success: false }, state: { success: true }, }); expect(mockDockerService.cleanup).not.toHaveBeenCalled(); expect(mockWorktreeService.cleanupWorktree).not.toHaveBeenCalled(); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: false, worktree: false, state: true, }, }) ); }); it("should emit cleanup event even if event publishing fails", async () => { // Arrange mockDockerService.cleanup.mockResolvedValue(undefined); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: true }); mockValkeyService.deleteAgentState.mockResolvedValue(undefined); mockValkeyService.publishEvent.mockRejectedValue(new Error("Event publish failed")); // Act - should not throw const result = await service.cleanup(mockAgentState); // Assert expect(result).toEqual({ docker: { success: true }, worktree: { success: true }, state: { success: true }, }); expect(mockValkeyService.publishEvent).toHaveBeenCalled(); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); }); it("should handle all cleanup steps failing", async () => { // Arrange mockDockerService.cleanup.mockRejectedValue(new Error("Docker error")); mockWorktreeService.cleanupWorktree.mockResolvedValue({ success: false, error: "Git error", }); mockValkeyService.deleteAgentState.mockRejectedValue(new Error("Valkey error")); mockValkeyService.publishEvent.mockResolvedValue(undefined); // Act - should not throw const result = await service.cleanup(mockAgentState); // Assert - all cleanup attempts were made expect(result).toEqual({ docker: { success: false, error: "Docker error" }, worktree: { success: false, error: "Git error" }, state: { success: false, error: "Valkey error" }, }); expect(mockDockerService.cleanup).toHaveBeenCalledWith("container-abc"); expect(mockWorktreeService.cleanupWorktree).toHaveBeenCalledWith( "/path/to/repo", "agent-123", "task-456" ); expect(mockValkeyService.deleteAgentState).toHaveBeenCalledWith("agent-123"); expect(mockValkeyService.publishEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "agent.cleanup", agentId: "agent-123", taskId: "task-456", cleanup: { docker: false, worktree: false, state: false, }, }) ); }); }); });