import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { StitcherService } from "./stitcher.service"; import { PrismaService } from "../prisma/prisma.service"; import { BullMqService } from "../bullmq/bullmq.service"; import { QUEUE_NAMES } from "../bullmq/queues"; import type { JobDispatchContext, JobDispatchResult } from "./interfaces"; describe("StitcherService", () => { let service: StitcherService; let prismaService: PrismaService; let bullMqService: BullMqService; const mockPrismaService = { runnerJob: { create: vi.fn(), findUnique: vi.fn(), update: vi.fn(), }, jobEvent: { create: vi.fn(), }, }; const mockBullMqService = { addJob: vi.fn(), getQueue: vi.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ StitcherService, { provide: PrismaService, useValue: mockPrismaService }, { provide: BullMqService, useValue: mockBullMqService }, ], }).compile(); service = module.get(StitcherService); prismaService = module.get(PrismaService); bullMqService = module.get(BullMqService); vi.clearAllMocks(); }); describe("dispatchJob", () => { it("should create a RunnerJob and dispatch to queue", async () => { const context: JobDispatchContext = { workspaceId: "workspace-123", type: "code-task", priority: 10, }; const mockJob = { id: "job-123", workspaceId: "workspace-123", type: "code-task", status: "PENDING", priority: 10, progressPercent: 0, createdAt: new Date(), }; mockPrismaService.runnerJob.create.mockResolvedValue(mockJob); mockBullMqService.addJob.mockResolvedValue({ id: "queue-job-123" }); const result = await service.dispatchJob(context); expect(result).toEqual({ jobId: "job-123", queueName: QUEUE_NAMES.MAIN, status: "PENDING", }); expect(mockPrismaService.runnerJob.create).toHaveBeenCalledWith({ data: { workspaceId: "workspace-123", type: "code-task", priority: 10, status: "PENDING", progressPercent: 0, }, }); expect(mockBullMqService.addJob).toHaveBeenCalledWith( QUEUE_NAMES.MAIN, "code-task", expect.objectContaining({ jobId: "job-123", workspaceId: "workspace-123", }), expect.objectContaining({ priority: 10, }) ); }); it("should log job event after dispatch", async () => { const context: JobDispatchContext = { workspaceId: "workspace-123", type: "git-status", }; const mockJob = { id: "job-456", workspaceId: "workspace-123", type: "git-status", status: "PENDING", priority: 5, progressPercent: 0, createdAt: new Date(), }; mockPrismaService.runnerJob.create.mockResolvedValue(mockJob); mockBullMqService.addJob.mockResolvedValue({ id: "queue-job-456" }); await service.dispatchJob(context); expect(mockPrismaService.jobEvent.create).toHaveBeenCalledWith({ data: expect.objectContaining({ jobId: "job-456", type: "job.queued", actor: "stitcher", }), }); }); it("should handle dispatch errors", async () => { const context: JobDispatchContext = { workspaceId: "workspace-123", type: "invalid-type", }; mockPrismaService.runnerJob.create.mockRejectedValue(new Error("Database error")); await expect(service.dispatchJob(context)).rejects.toThrow("Database error"); }); }); describe("applyGuardRails", () => { it("should return allowed for valid capabilities", () => { const result = service.applyGuardRails("runner", ["read"]); expect(result.allowed).toBe(true); }); it("should return not allowed for invalid capabilities", () => { const result = service.applyGuardRails("runner", ["write"]); expect(result.allowed).toBe(false); expect(result.reason).toBeDefined(); }); }); describe("applyQualityRails", () => { it("should return required gates for code tasks", () => { const result = service.applyQualityRails("code-task"); expect(result.required).toBe(true); expect(result.gates).toContain("lint"); expect(result.gates).toContain("typecheck"); expect(result.gates).toContain("test"); }); it("should return no gates for read-only tasks", () => { const result = service.applyQualityRails("git-status"); expect(result.required).toBe(false); expect(result.gates).toHaveLength(0); }); }); describe("trackJobEvent", () => { it("should create job event in database", async () => { const mockEvent = { id: "event-123", jobId: "job-123", type: "job.started", timestamp: new Date(), actor: "stitcher", payload: {}, }; mockPrismaService.jobEvent.create.mockResolvedValue(mockEvent); await service.trackJobEvent("job-123", "job.started", "stitcher", {}); expect(mockPrismaService.jobEvent.create).toHaveBeenCalledWith({ data: { jobId: "job-123", type: "job.started", actor: "stitcher", timestamp: expect.any(Date), payload: {}, }, }); }); }); });