Fixed failing tests in job-steps.service.spec.ts and job-steps.controller.spec.ts caused by undefined Prisma enum imports in the test environment. Root cause: When importing JobStepPhase, JobStepType, and JobStepStatus from @prisma/client in the test environment with mocked Prisma, the enums were undefined, causing "Cannot read properties of undefined" errors. Solution: Used vi.mock() with importOriginal to mock the @prisma/client module and explicitly provide enum values while preserving other exports like PrismaClient. Changes: - Added vi.mock() for @prisma/client in both test files - Defined all three enums (JobStepPhase, JobStepType, JobStepStatus) with their values - Moved imports after the mock setup to ensure proper initialization Test results: All 16 job-steps tests now passing (13 service + 3 controller) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
543 lines
15 KiB
TypeScript
543 lines
15 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { NotFoundException } from "@nestjs/common";
|
|
import { CreateStepDto, UpdateStepDto } from "./dto";
|
|
|
|
// Mock @prisma/client BEFORE importing the service
|
|
vi.mock("@prisma/client", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@prisma/client")>();
|
|
return {
|
|
...actual,
|
|
JobStepPhase: {
|
|
SETUP: "SETUP",
|
|
EXECUTION: "EXECUTION",
|
|
VALIDATION: "VALIDATION",
|
|
CLEANUP: "CLEANUP",
|
|
},
|
|
JobStepType: {
|
|
COMMAND: "COMMAND",
|
|
AI_ACTION: "AI_ACTION",
|
|
GATE: "GATE",
|
|
ARTIFACT: "ARTIFACT",
|
|
},
|
|
JobStepStatus: {
|
|
PENDING: "PENDING",
|
|
RUNNING: "RUNNING",
|
|
COMPLETED: "COMPLETED",
|
|
FAILED: "FAILED",
|
|
SKIPPED: "SKIPPED",
|
|
},
|
|
};
|
|
});
|
|
|
|
// Import after mocking
|
|
import { JobStepsService } from "./job-steps.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
|
|
// Re-import the enums from the mock for use in tests
|
|
import { JobStepPhase, JobStepType, JobStepStatus } from "@prisma/client";
|
|
|
|
describe("JobStepsService", () => {
|
|
let service: JobStepsService;
|
|
let prisma: PrismaService;
|
|
|
|
const mockPrismaService = {
|
|
jobStep: {
|
|
create: vi.fn(),
|
|
findMany: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
update: vi.fn(),
|
|
},
|
|
runnerJob: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
JobStepsService,
|
|
{
|
|
provide: PrismaService,
|
|
useValue: mockPrismaService,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<JobStepsService>(JobStepsService);
|
|
prisma = module.get<PrismaService>(PrismaService);
|
|
|
|
// Clear all mocks before each test
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
describe("create", () => {
|
|
it("should create a job step", async () => {
|
|
const jobId = "job-123";
|
|
const createDto: CreateStepDto = {
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repository",
|
|
type: JobStepType.COMMAND,
|
|
};
|
|
|
|
const mockStep = {
|
|
id: "step-123",
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repository",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.PENDING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
mockPrismaService.jobStep.create.mockResolvedValue(mockStep);
|
|
|
|
const result = await service.create(jobId, createDto);
|
|
|
|
expect(result).toEqual(mockStep);
|
|
expect(prisma.jobStep.create).toHaveBeenCalledWith({
|
|
data: {
|
|
job: { connect: { id: jobId } },
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repository",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.PENDING,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should use provided status when creating step", async () => {
|
|
const jobId = "job-123";
|
|
const createDto: CreateStepDto = {
|
|
ordinal: 2,
|
|
phase: JobStepPhase.EXECUTION,
|
|
name: "Run tests",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
};
|
|
|
|
const mockStep = {
|
|
id: "step-124",
|
|
jobId,
|
|
ordinal: 2,
|
|
phase: JobStepPhase.EXECUTION,
|
|
name: "Run tests",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date(),
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
mockPrismaService.jobStep.create.mockResolvedValue(mockStep);
|
|
|
|
const result = await service.create(jobId, createDto);
|
|
|
|
expect(result).toEqual(mockStep);
|
|
expect(prisma.jobStep.create).toHaveBeenCalledWith({
|
|
data: {
|
|
job: { connect: { id: jobId } },
|
|
ordinal: 2,
|
|
phase: JobStepPhase.EXECUTION,
|
|
name: "Run tests",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("findAllByJob", () => {
|
|
it("should return all steps for a job ordered by ordinal", async () => {
|
|
const jobId = "job-123";
|
|
|
|
const mockSteps = [
|
|
{
|
|
id: "step-1",
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Cloned successfully",
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date("2024-01-01T10:00:00Z"),
|
|
completedAt: new Date("2024-01-01T10:00:05Z"),
|
|
durationMs: 5000,
|
|
},
|
|
{
|
|
id: "step-2",
|
|
jobId,
|
|
ordinal: 2,
|
|
phase: JobStepPhase.EXECUTION,
|
|
name: "Run tests",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date("2024-01-01T10:00:05Z"),
|
|
completedAt: null,
|
|
durationMs: null,
|
|
},
|
|
];
|
|
|
|
mockPrismaService.jobStep.findMany.mockResolvedValue(mockSteps);
|
|
|
|
const result = await service.findAllByJob(jobId);
|
|
|
|
expect(result).toEqual(mockSteps);
|
|
expect(prisma.jobStep.findMany).toHaveBeenCalledWith({
|
|
where: { jobId },
|
|
orderBy: { ordinal: "asc" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("findOne", () => {
|
|
it("should return a single step by ID", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
|
|
const mockStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Cloned successfully",
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date("2024-01-01T10:00:00Z"),
|
|
completedAt: new Date("2024-01-01T10:00:05Z"),
|
|
durationMs: 5000,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(mockStep);
|
|
|
|
const result = await service.findOne(stepId, jobId);
|
|
|
|
expect(result).toEqual(mockStep);
|
|
expect(prisma.jobStep.findUnique).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
});
|
|
});
|
|
|
|
it("should throw NotFoundException when step not found", async () => {
|
|
const stepId = "step-999";
|
|
const jobId = "job-123";
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.findOne(stepId, jobId)).rejects.toThrow(NotFoundException);
|
|
await expect(service.findOne(stepId, jobId)).rejects.toThrow(
|
|
`JobStep with ID ${stepId} not found`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("update", () => {
|
|
it("should update step status", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
const updateDto: UpdateStepDto = {
|
|
status: JobStepStatus.COMPLETED,
|
|
};
|
|
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date("2024-01-01T10:00:00Z"),
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const updatedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.COMPLETED,
|
|
completedAt: new Date("2024-01-01T10:00:05Z"),
|
|
durationMs: 5000,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(updatedStep);
|
|
|
|
const result = await service.update(stepId, jobId, updateDto);
|
|
|
|
expect(result).toEqual(updatedStep);
|
|
expect(prisma.jobStep.update).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
data: { status: JobStepStatus.COMPLETED },
|
|
});
|
|
});
|
|
|
|
it("should update step with output and token usage", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
const updateDto: UpdateStepDto = {
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Analysis complete",
|
|
tokensInput: 1000,
|
|
tokensOutput: 500,
|
|
};
|
|
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 2,
|
|
phase: JobStepPhase.EXECUTION,
|
|
name: "AI Analysis",
|
|
type: JobStepType.AI_ACTION,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: new Date("2024-01-01T10:00:00Z"),
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const updatedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Analysis complete",
|
|
tokensInput: 1000,
|
|
tokensOutput: 500,
|
|
completedAt: new Date("2024-01-01T10:00:10Z"),
|
|
durationMs: 10000,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(updatedStep);
|
|
|
|
const result = await service.update(stepId, jobId, updateDto);
|
|
|
|
expect(result).toEqual(updatedStep);
|
|
expect(prisma.jobStep.update).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
data: {
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Analysis complete",
|
|
tokensInput: 1000,
|
|
tokensOutput: 500,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should throw NotFoundException when step not found", async () => {
|
|
const stepId = "step-999";
|
|
const jobId = "job-123";
|
|
const updateDto: UpdateStepDto = {
|
|
status: JobStepStatus.COMPLETED,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.update(stepId, jobId, updateDto)).rejects.toThrow(NotFoundException);
|
|
});
|
|
});
|
|
|
|
describe("startStep", () => {
|
|
it("should mark step as running and set startedAt", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.PENDING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const startedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.RUNNING,
|
|
startedAt: new Date("2024-01-01T10:00:00Z"),
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(startedStep);
|
|
|
|
const result = await service.startStep(stepId, jobId);
|
|
|
|
expect(result).toEqual(startedStep);
|
|
expect(prisma.jobStep.update).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
data: {
|
|
status: JobStepStatus.RUNNING,
|
|
startedAt: expect.any(Date),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("completeStep", () => {
|
|
it("should mark step as completed and calculate duration", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
|
|
const startTime = new Date("2024-01-01T10:00:00Z");
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: startTime,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const completedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Success",
|
|
completedAt: new Date("2024-01-01T10:00:05Z"),
|
|
durationMs: 5000,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(completedStep);
|
|
|
|
const result = await service.completeStep(stepId, jobId, "Success");
|
|
|
|
expect(result).toEqual(completedStep);
|
|
expect(prisma.jobStep.update).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
data: {
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Success",
|
|
completedAt: expect.any(Date),
|
|
durationMs: expect.any(Number),
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should handle step without startedAt by setting durationMs to null", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.SETUP,
|
|
name: "Clone repo",
|
|
type: JobStepType.COMMAND,
|
|
status: JobStepStatus.PENDING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const completedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.COMPLETED,
|
|
output: "Success",
|
|
completedAt: new Date("2024-01-01T10:00:05Z"),
|
|
durationMs: null,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(completedStep);
|
|
|
|
const result = await service.completeStep(stepId, jobId, "Success");
|
|
|
|
expect(result.durationMs).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("failStep", () => {
|
|
it("should mark step as failed with error output", async () => {
|
|
const stepId = "step-123";
|
|
const jobId = "job-123";
|
|
const error = "Command failed with exit code 1";
|
|
|
|
const startTime = new Date("2024-01-01T10:00:00Z");
|
|
const existingStep = {
|
|
id: stepId,
|
|
jobId,
|
|
ordinal: 1,
|
|
phase: JobStepPhase.VALIDATION,
|
|
name: "Run tests",
|
|
type: JobStepType.GATE,
|
|
status: JobStepStatus.RUNNING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: startTime,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
};
|
|
|
|
const failedStep = {
|
|
...existingStep,
|
|
status: JobStepStatus.FAILED,
|
|
output: error,
|
|
completedAt: new Date("2024-01-01T10:00:03Z"),
|
|
durationMs: 3000,
|
|
};
|
|
|
|
mockPrismaService.jobStep.findUnique.mockResolvedValue(existingStep);
|
|
mockPrismaService.jobStep.update.mockResolvedValue(failedStep);
|
|
|
|
const result = await service.failStep(stepId, jobId, error);
|
|
|
|
expect(result).toEqual(failedStep);
|
|
expect(prisma.jobStep.update).toHaveBeenCalledWith({
|
|
where: { id: stepId, jobId },
|
|
data: {
|
|
status: JobStepStatus.FAILED,
|
|
output: error,
|
|
completedAt: expect.any(Date),
|
|
durationMs: expect.any(Number),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
});
|