import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { RejectionHandlerService } from "./rejection-handler.service"; import { PrismaService } from "../prisma/prisma.service"; import { Logger } from "@nestjs/common"; import type { RejectionContext, RejectionResult, EscalationConfig, EscalationRule, } from "./interfaces"; describe("RejectionHandlerService", () => { let service: RejectionHandlerService; let prismaService: PrismaService; const mockRejectionContext: RejectionContext = { taskId: "task-123", workspaceId: "workspace-456", agentId: "agent-789", attemptCount: 3, failures: [ { gateName: "type-check", failureType: "compilation-error", message: "Type error in module", attempts: 2, }, { gateName: "test-gate", failureType: "test-failure", message: "5 tests failed", attempts: 1, }, ], originalTask: "Implement user authentication", startedAt: new Date("2026-01-31T10:00:00Z"), rejectedAt: new Date("2026-01-31T12:30:00Z"), }; const mockPrismaService = { taskRejection: { create: vi.fn(), findMany: vi.fn(), update: vi.fn(), }, }; const mockLogger = { log: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ RejectionHandlerService, { provide: PrismaService, useValue: mockPrismaService, }, { provide: Logger, useValue: mockLogger, }, ], }).compile(); service = module.get(RejectionHandlerService); prismaService = module.get(PrismaService); // Clear all mocks before each test vi.clearAllMocks(); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("handleRejection", () => { it("should handle rejection and return result", async () => { const mockRejection = { id: "rejection-1", taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: mockRejectionContext.attemptCount, failures: mockRejectionContext.failures as any, originalTask: mockRejectionContext.originalTask, startedAt: mockRejectionContext.startedAt, rejectedAt: mockRejectionContext.rejectedAt, escalated: true, manualReview: true, resolvedAt: null, resolution: null, }; mockPrismaService.taskRejection.create.mockResolvedValue(mockRejection); mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]); mockPrismaService.taskRejection.update.mockResolvedValue(mockRejection); const result = await service.handleRejection(mockRejectionContext); expect(result.handled).toBe(true); expect(result.manualReviewRequired).toBe(true); expect(mockPrismaService.taskRejection.create).toHaveBeenCalled(); }); it("should handle rejection without escalation for low attempt count", async () => { const lowAttemptContext: RejectionContext = { ...mockRejectionContext, attemptCount: 1, startedAt: new Date("2026-01-31T12:00:00Z"), rejectedAt: new Date("2026-01-31T12:30:00Z"), // 30 minutes - under maxWaitTime }; const mockRejection = { id: "rejection-2", taskId: lowAttemptContext.taskId, workspaceId: lowAttemptContext.workspaceId, agentId: lowAttemptContext.agentId, attemptCount: lowAttemptContext.attemptCount, failures: lowAttemptContext.failures as any, originalTask: lowAttemptContext.originalTask, startedAt: lowAttemptContext.startedAt, rejectedAt: lowAttemptContext.rejectedAt, escalated: false, manualReview: false, resolvedAt: null, resolution: null, }; mockPrismaService.taskRejection.create.mockResolvedValue(mockRejection); mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]); mockPrismaService.taskRejection.update.mockResolvedValue(mockRejection); const result = await service.handleRejection(lowAttemptContext); expect(result.handled).toBe(true); expect(result.escalated).toBe(false); }); }); describe("logRejection", () => { it("should log rejection to database", async () => { prismaService.taskRejection.create.mockResolvedValue({ id: "rejection-3", taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: mockRejectionContext.attemptCount, failures: mockRejectionContext.failures as any, originalTask: mockRejectionContext.originalTask, startedAt: mockRejectionContext.startedAt, rejectedAt: mockRejectionContext.rejectedAt, escalated: false, manualReview: false, resolvedAt: null, resolution: null, }); await service.logRejection(mockRejectionContext); expect(prismaService.taskRejection.create).toHaveBeenCalledWith({ data: expect.objectContaining({ taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: mockRejectionContext.attemptCount, }), }); }); }); describe("determineEscalation", () => { it("should determine escalation rules based on max attempts", () => { const config: EscalationConfig = { rules: [ { condition: "max-attempts", action: "notify", target: "admin@example.com", priority: "high", }, ], notifyOnRejection: true, autoReassign: false, maxWaitTime: 60, }; const rules = service.determineEscalation(mockRejectionContext, config); expect(rules).toHaveLength(1); expect(rules[0].condition).toBe("max-attempts"); expect(rules[0].action).toBe("notify"); expect(rules[0].priority).toBe("high"); }); it("should determine escalation for time exceeded", () => { const longRunningContext: RejectionContext = { ...mockRejectionContext, startedAt: new Date("2026-01-31T08:00:00Z"), rejectedAt: new Date("2026-01-31T12:00:00Z"), }; const config: EscalationConfig = { rules: [ { condition: "time-exceeded", action: "block", priority: "critical", }, ], notifyOnRejection: true, autoReassign: false, maxWaitTime: 120, // 2 hours }; const rules = service.determineEscalation(longRunningContext, config); expect(rules.length).toBeGreaterThan(0); expect(rules.some((r) => r.condition === "time-exceeded")).toBe(true); }); it("should determine escalation for critical failures", () => { const criticalContext: RejectionContext = { ...mockRejectionContext, failures: [ { gateName: "security-scan", failureType: "critical-vulnerability", message: "SQL injection detected", attempts: 1, }, ], }; const config: EscalationConfig = { rules: [ { condition: "critical-failure", action: "block", priority: "critical", }, ], notifyOnRejection: true, autoReassign: false, maxWaitTime: 60, }; const rules = service.determineEscalation(criticalContext, config); expect(rules.some((r) => r.condition === "critical-failure")).toBe(true); }); }); describe("executeEscalation", () => { it("should execute notification escalation", async () => { const rules: EscalationRule[] = [ { condition: "max-attempts", action: "notify", target: "admin@example.com", priority: "high", }, ]; // Mock sendNotification vi.spyOn(service, "sendNotification").mockReturnValue(); await service.executeEscalation(mockRejectionContext, rules); expect(service.sendNotification).toHaveBeenCalledWith( mockRejectionContext, "admin@example.com", "high" ); }); it("should execute block escalation", async () => { const rules: EscalationRule[] = [ { condition: "critical-failure", action: "block", priority: "critical", }, ]; vi.spyOn(service, "markForManualReview").mockResolvedValue(); await service.executeEscalation(mockRejectionContext, rules); expect(service.markForManualReview).toHaveBeenCalledWith( mockRejectionContext.taskId, expect.any(String) ); }); }); describe("sendNotification", () => { it("should send notification with context and priority", () => { service.sendNotification(mockRejectionContext, "admin@example.com", "high"); // Verify logging occurred expect(true).toBe(true); // Placeholder - actual implementation will log }); }); describe("markForManualReview", () => { it("should mark task for manual review", async () => { const mockRejection = { id: "rejection-4", taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: mockRejectionContext.attemptCount, failures: mockRejectionContext.failures as any, originalTask: mockRejectionContext.originalTask, startedAt: mockRejectionContext.startedAt, rejectedAt: mockRejectionContext.rejectedAt, escalated: false, manualReview: false, resolvedAt: null, resolution: null, }; mockPrismaService.taskRejection.findMany.mockResolvedValue([mockRejection]); mockPrismaService.taskRejection.update.mockResolvedValue({ ...mockRejection, escalated: true, manualReview: true, }); await service.markForManualReview(mockRejectionContext.taskId, "Max attempts exceeded"); expect(mockPrismaService.taskRejection.findMany).toHaveBeenCalledWith({ where: { taskId: mockRejectionContext.taskId }, orderBy: { rejectedAt: "desc" }, take: 1, }); expect(mockPrismaService.taskRejection.update).toHaveBeenCalledWith({ where: { id: mockRejection.id }, data: { manualReview: true, escalated: true, }, }); }); }); describe("getRejectionHistory", () => { it("should retrieve rejection history for a task", async () => { const mockHistory = [ { id: "rejection-5", taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: 1, failures: [], originalTask: mockRejectionContext.originalTask, startedAt: new Date("2026-01-31T09:00:00Z"), rejectedAt: new Date("2026-01-31T10:00:00Z"), escalated: false, manualReview: false, resolvedAt: null, resolution: null, }, { id: "rejection-6", taskId: mockRejectionContext.taskId, workspaceId: mockRejectionContext.workspaceId, agentId: mockRejectionContext.agentId, attemptCount: 2, failures: [], originalTask: mockRejectionContext.originalTask, startedAt: new Date("2026-01-31T10:30:00Z"), rejectedAt: new Date("2026-01-31T11:30:00Z"), escalated: false, manualReview: false, resolvedAt: null, resolution: null, }, ]; prismaService.taskRejection.findMany.mockResolvedValue(mockHistory); const history = await service.getRejectionHistory(mockRejectionContext.taskId); expect(history).toHaveLength(2); expect(prismaService.taskRejection.findMany).toHaveBeenCalledWith({ where: { taskId: mockRejectionContext.taskId }, orderBy: { rejectedAt: "desc" }, }); }); }); describe("generateRejectionReport", () => { it("should generate a formatted rejection report", () => { const report = service.generateRejectionReport(mockRejectionContext); expect(report).toContain("Task Rejection Report"); expect(report).toContain(mockRejectionContext.taskId); expect(report).toContain(mockRejectionContext.attemptCount.toString()); expect(report).toContain("type-check"); expect(report).toContain("test-gate"); }); it("should include failure details in report", () => { const report = service.generateRejectionReport(mockRejectionContext); mockRejectionContext.failures.forEach((failure) => { expect(report).toContain(failure.gateName); expect(report).toContain(failure.message); }); }); }); describe("getDefaultEscalationConfig", () => { it("should return default escalation configuration", () => { const config = service.getDefaultEscalationConfig(); expect(config.rules).toBeDefined(); expect(config.rules.length).toBeGreaterThan(0); expect(config.notifyOnRejection).toBeDefined(); expect(config.autoReassign).toBeDefined(); expect(config.maxWaitTime).toBeGreaterThan(0); }); it("should include all escalation conditions in default config", () => { const config = service.getDefaultEscalationConfig(); const conditions = config.rules.map((r) => r.condition); expect(conditions).toContain("max-attempts"); expect(conditions).toContain("critical-failure"); expect(conditions).toContain("time-exceeded"); }); }); });