import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { JobEventsService } from "./job-events.service"; import { PrismaService } from "../prisma/prisma.service"; import { NotFoundException } from "@nestjs/common"; import { JOB_CREATED, STEP_STARTED, AI_TOKENS_USED } from "./event-types"; describe("JobEventsService", () => { let service: JobEventsService; let prisma: PrismaService; const mockPrismaService = { runnerJob: { findUnique: vi.fn(), }, jobStep: { findUnique: vi.fn(), }, jobEvent: { create: vi.fn(), findMany: vi.fn(), count: vi.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ JobEventsService, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); service = module.get(JobEventsService); prisma = module.get(PrismaService); }); afterEach(() => { vi.clearAllMocks(); }); describe("emitEvent", () => { const jobId = "job-123"; const mockEvent = { id: "event-123", jobId, stepId: null, type: JOB_CREATED, timestamp: new Date(), actor: "system", payload: { message: "Job created" }, }; it("should create a job event without stepId", async () => { mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.create.mockResolvedValue(mockEvent); const result = await service.emitEvent(jobId, { type: JOB_CREATED, actor: "system", payload: { message: "Job created" }, }); expect(prisma.runnerJob.findUnique).toHaveBeenCalledWith({ where: { id: jobId }, select: { id: true }, }); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, type: JOB_CREATED, timestamp: expect.any(Date), actor: "system", payload: { message: "Job created" }, }, }); expect(result).toEqual(mockEvent); }); it("should create a job event with stepId", async () => { const stepId = "step-123"; const eventWithStep = { ...mockEvent, stepId, type: STEP_STARTED }; mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobStep.findUnique.mockResolvedValue({ id: stepId }); mockPrismaService.jobEvent.create.mockResolvedValue(eventWithStep); const result = await service.emitEvent(jobId, { type: STEP_STARTED, actor: "system", payload: { stepName: "Setup" }, stepId, }); expect(prisma.jobStep.findUnique).toHaveBeenCalledWith({ where: { id: stepId }, select: { id: true }, }); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, step: { connect: { id: stepId } }, type: STEP_STARTED, timestamp: expect.any(Date), actor: "system", payload: { stepName: "Setup" }, }, }); expect(result).toEqual(eventWithStep); }); it("should throw NotFoundException if job does not exist", async () => { mockPrismaService.runnerJob.findUnique.mockResolvedValue(null); await expect( service.emitEvent(jobId, { type: JOB_CREATED, actor: "system", payload: {}, }) ).rejects.toThrow(NotFoundException); }); it("should throw NotFoundException if step does not exist", async () => { const stepId = "step-invalid"; mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobStep.findUnique.mockResolvedValue(null); await expect( service.emitEvent(jobId, { type: STEP_STARTED, actor: "system", payload: {}, stepId, }) ).rejects.toThrow(NotFoundException); }); }); describe("getEventsByJobId", () => { const jobId = "job-123"; const mockEvents = [ { id: "event-1", jobId, stepId: null, type: JOB_CREATED, timestamp: new Date("2026-01-01T10:00:00Z"), actor: "system", payload: {}, }, { id: "event-2", jobId, stepId: "step-1", type: STEP_STARTED, timestamp: new Date("2026-01-01T10:01:00Z"), actor: "system", payload: {}, }, ]; it("should return paginated events for a job", async () => { mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.findMany.mockResolvedValue(mockEvents); mockPrismaService.jobEvent.count.mockResolvedValue(2); const result = await service.getEventsByJobId(jobId, {}); expect(prisma.runnerJob.findUnique).toHaveBeenCalledWith({ where: { id: jobId }, select: { id: true }, }); expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({ where: { jobId }, orderBy: { timestamp: "asc" }, skip: 0, take: 50, }); expect(prisma.jobEvent.count).toHaveBeenCalledWith({ where: { jobId }, }); expect(result).toEqual({ data: mockEvents, meta: { total: 2, page: 1, limit: 50, totalPages: 1, }, }); }); it("should filter events by type", async () => { const filteredEvents = [mockEvents[0]]; mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.findMany.mockResolvedValue(filteredEvents); mockPrismaService.jobEvent.count.mockResolvedValue(1); const result = await service.getEventsByJobId(jobId, { type: JOB_CREATED }); expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({ where: { jobId, type: JOB_CREATED }, orderBy: { timestamp: "asc" }, skip: 0, take: 50, }); expect(result.data).toHaveLength(1); expect(result.meta.total).toBe(1); }); it("should filter events by stepId", async () => { const stepId = "step-1"; const filteredEvents = [mockEvents[1]]; mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.findMany.mockResolvedValue(filteredEvents); mockPrismaService.jobEvent.count.mockResolvedValue(1); const result = await service.getEventsByJobId(jobId, { stepId }); expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({ where: { jobId, stepId }, orderBy: { timestamp: "asc" }, skip: 0, take: 50, }); expect(result.data).toHaveLength(1); }); it("should paginate results correctly", async () => { mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.findMany.mockResolvedValue([mockEvents[1]]); mockPrismaService.jobEvent.count.mockResolvedValue(2); const result = await service.getEventsByJobId(jobId, { page: 2, limit: 1 }); expect(prisma.jobEvent.findMany).toHaveBeenCalledWith({ where: { jobId }, orderBy: { timestamp: "asc" }, skip: 1, take: 1, }); expect(result.data).toHaveLength(1); expect(result.meta.page).toBe(2); expect(result.meta.limit).toBe(1); expect(result.meta.totalPages).toBe(2); }); it("should throw NotFoundException if job does not exist", async () => { mockPrismaService.runnerJob.findUnique.mockResolvedValue(null); await expect(service.getEventsByJobId(jobId, {})).rejects.toThrow(NotFoundException); }); }); describe("convenience methods", () => { const jobId = "job-123"; beforeEach(() => { mockPrismaService.runnerJob.findUnique.mockResolvedValue({ id: jobId }); mockPrismaService.jobEvent.create.mockResolvedValue({ id: "event-123", jobId, stepId: null, type: JOB_CREATED, timestamp: new Date(), actor: "system", payload: {}, }); }); it("should emit job.created event", async () => { await service.emitJobCreated(jobId, { type: "code-task" }); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, type: JOB_CREATED, timestamp: expect.any(Date), actor: "system", payload: { type: "code-task" }, }, }); }); it("should emit job.started event", async () => { await service.emitJobStarted(jobId); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, type: "job.started", timestamp: expect.any(Date), actor: "system", payload: {}, }, }); }); it("should emit step.started event", async () => { const stepId = "step-123"; mockPrismaService.jobStep.findUnique.mockResolvedValue({ id: stepId }); await service.emitStepStarted(jobId, stepId, { name: "Setup" }); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, step: { connect: { id: stepId } }, type: STEP_STARTED, timestamp: expect.any(Date), actor: "system", payload: { name: "Setup" }, }, }); }); it("should emit ai.tokens_used event", async () => { await service.emitAiTokensUsed(jobId, { input: 100, output: 50 }); expect(prisma.jobEvent.create).toHaveBeenCalledWith({ data: { job: { connect: { id: jobId } }, type: AI_TOKENS_USED, timestamp: expect.any(Date), actor: "system", payload: { input: 100, output: 50 }, }, }); }); }); });