feat(#168): Implement job steps tracking
Implement JobStepsModule for granular step tracking within runner jobs. Features: - Create and track job steps (SETUP, EXECUTION, VALIDATION, CLEANUP) - Track step status transitions (PENDING → RUNNING → COMPLETED/FAILED) - Record token usage for AI_ACTION steps - Calculate step duration automatically - GET endpoints for listing and retrieving steps Implementation: - JobStepsService: CRUD operations, status tracking, duration calculation - JobStepsController: GET /runner-jobs/:jobId/steps endpoints - DTOs: CreateStepDto, UpdateStepDto with validation - Full unit test coverage (16 tests) Quality gates: - Build: ✅ Passed - Lint: ✅ Passed - Tests: ✅ 16/16 passed - Coverage: ✅ 100% statements, 100% functions, 100% lines, 83.33% branches Also fixed pre-existing TypeScript strict mode issue in job-events DTO. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
147
apps/api/src/job-steps/job-steps.controller.spec.ts
Normal file
147
apps/api/src/job-steps/job-steps.controller.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { JobStepsController } from "./job-steps.controller";
|
||||
import { JobStepsService } from "./job-steps.service";
|
||||
import { JobStepPhase, JobStepType, JobStepStatus } from "@prisma/client";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard } from "../common/guards/workspace.guard";
|
||||
import { PermissionGuard } from "../common/guards/permission.guard";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
|
||||
describe("JobStepsController", () => {
|
||||
let controller: JobStepsController;
|
||||
let service: JobStepsService;
|
||||
|
||||
const mockJobStepsService = {
|
||||
findAllByJob: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
startStep: vi.fn(),
|
||||
completeStep: vi.fn(),
|
||||
failStep: vi.fn(),
|
||||
};
|
||||
|
||||
const mockAuthGuard = {
|
||||
canActivate: vi.fn((context: ExecutionContext) => {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
request.user = {
|
||||
id: "user-123",
|
||||
workspaceId: "workspace-123",
|
||||
};
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
const mockWorkspaceGuard = {
|
||||
canActivate: vi.fn(() => true),
|
||||
};
|
||||
|
||||
const mockPermissionGuard = {
|
||||
canActivate: vi.fn(() => true),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [JobStepsController],
|
||||
providers: [
|
||||
{
|
||||
provide: JobStepsService,
|
||||
useValue: mockJobStepsService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue(mockAuthGuard)
|
||||
.overrideGuard(WorkspaceGuard)
|
||||
.useValue(mockWorkspaceGuard)
|
||||
.overrideGuard(PermissionGuard)
|
||||
.useValue(mockPermissionGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<JobStepsController>(JobStepsController);
|
||||
service = module.get<JobStepsService>(JobStepsService);
|
||||
|
||||
// Clear all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return all steps for a job", 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,
|
||||
},
|
||||
];
|
||||
|
||||
mockJobStepsService.findAllByJob.mockResolvedValue(mockSteps);
|
||||
|
||||
const result = await controller.findAll(jobId);
|
||||
|
||||
expect(result).toEqual(mockSteps);
|
||||
expect(service.findAllByJob).toHaveBeenCalledWith(jobId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should return a single step by ID", async () => {
|
||||
const jobId = "job-123";
|
||||
const stepId = "step-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,
|
||||
};
|
||||
|
||||
mockJobStepsService.findOne.mockResolvedValue(mockStep);
|
||||
|
||||
const result = await controller.findOne(jobId, stepId);
|
||||
|
||||
expect(result).toEqual(mockStep);
|
||||
expect(service.findOne).toHaveBeenCalledWith(stepId, jobId);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user