Files
stack/apps/api/src/job-events/job-events.controller.spec.ts
Jason Woltje efe624e2c1 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>
2026-02-01 21:16:23 -06:00

135 lines
3.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Test, TestingModule } from "@nestjs/testing";
import { JobEventsController } from "./job-events.controller";
import { JobEventsService } from "./job-events.service";
import { JOB_CREATED } from "./event-types";
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("JobEventsController", () => {
let controller: JobEventsController;
let service: JobEventsService;
const mockJobEventsService = {
getEventsByJobId: 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: [JobEventsController],
providers: [
{
provide: JobEventsService,
useValue: mockJobEventsService,
},
],
})
.overrideGuard(AuthGuard)
.useValue(mockAuthGuard)
.overrideGuard(WorkspaceGuard)
.useValue(mockWorkspaceGuard)
.overrideGuard(PermissionGuard)
.useValue(mockPermissionGuard)
.compile();
controller = module.get<JobEventsController>(JobEventsController);
service = module.get<JobEventsService>(JobEventsService);
});
afterEach(() => {
vi.clearAllMocks();
});
describe("getEvents", () => {
const jobId = "job-123";
const workspaceId = "workspace-123";
const mockEvents = {
data: [
{
id: "event-1",
jobId,
stepId: null,
type: JOB_CREATED,
timestamp: new Date("2026-01-01T10:00:00Z"),
actor: "system",
payload: {},
},
],
meta: {
total: 1,
page: 1,
limit: 50,
totalPages: 1,
},
};
it("should return paginated events for a job", async () => {
mockJobEventsService.getEventsByJobId.mockResolvedValue(mockEvents);
const result = await controller.getEvents(jobId, {}, workspaceId);
expect(service.getEventsByJobId).toHaveBeenCalledWith(jobId, {});
expect(result).toEqual(mockEvents);
});
it("should pass query parameters to service", async () => {
const query = { type: JOB_CREATED, page: 2, limit: 10 };
mockJobEventsService.getEventsByJobId.mockResolvedValue(mockEvents);
await controller.getEvents(jobId, query, workspaceId);
expect(service.getEventsByJobId).toHaveBeenCalledWith(jobId, query);
});
it("should handle filtering by type", async () => {
const query = { type: JOB_CREATED };
mockJobEventsService.getEventsByJobId.mockResolvedValue(mockEvents);
const result = await controller.getEvents(jobId, query, workspaceId);
expect(service.getEventsByJobId).toHaveBeenCalledWith(jobId, query);
expect(result).toEqual(mockEvents);
});
it("should handle pagination parameters", async () => {
const query = { page: 2, limit: 25 };
mockJobEventsService.getEventsByJobId.mockResolvedValue({
...mockEvents,
meta: {
total: 100,
page: 2,
limit: 25,
totalPages: 4,
},
});
const result = await controller.getEvents(jobId, query, workspaceId);
expect(service.getEventsByJobId).toHaveBeenCalledWith(jobId, query);
expect(result.meta.page).toBe(2);
expect(result.meta.limit).toBe(25);
});
});
});