- Create comprehensive E2E test suite for job orchestration - Add test fixtures for Discord, BullMQ, and Prisma mocks - Implement 9 end-to-end test scenarios covering: * Happy path: webhook → job → step execution → completion * Event emission throughout job lifecycle * Step failure and retry handling * Job failure after max retries * Discord command parsing and job creation * WebSocket status updates integration * Job cancellation workflow * Job retry mechanism * Progress percentage tracking - Add helper methods to services for simplified testing: * JobStepsService: start(), complete(), fail(), findByJob() * RunnerJobsService: updateStatus(), updateProgress() * JobEventsService: findByJob() - Configure vitest.e2e.config.ts for E2E test execution - All 9 E2E tests passing - All 1405 unit tests passing - Quality gates: typecheck, lint, build all passing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
236 lines
6.7 KiB
TypeScript
236 lines
6.7 KiB
TypeScript
import { vi } from "vitest";
|
|
import { RunnerJobStatus, JobStepStatus, JobStepPhase, JobStepType } from "@prisma/client";
|
|
import type { PrismaService } from "../../src/prisma/prisma.service";
|
|
|
|
/**
|
|
* Create a mock RunnerJob
|
|
*/
|
|
export function createMockRunnerJob(
|
|
overrides?: Partial<{
|
|
id: string;
|
|
workspaceId: string;
|
|
type: string;
|
|
status: RunnerJobStatus;
|
|
priority: number;
|
|
progressPercent: number;
|
|
result: unknown;
|
|
error: string | null;
|
|
createdAt: Date;
|
|
startedAt: Date | null;
|
|
completedAt: Date | null;
|
|
agentTaskId: string | null;
|
|
}>
|
|
) {
|
|
return {
|
|
id: "job-123",
|
|
workspaceId: "workspace-123",
|
|
type: "code-task",
|
|
status: RunnerJobStatus.PENDING,
|
|
priority: 10,
|
|
progressPercent: 0,
|
|
result: null,
|
|
error: null,
|
|
createdAt: new Date(),
|
|
startedAt: null,
|
|
completedAt: null,
|
|
agentTaskId: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a mock JobStep
|
|
*/
|
|
export function createMockJobStep(
|
|
overrides?: Partial<{
|
|
id: string;
|
|
jobId: string;
|
|
ordinal: number;
|
|
phase: JobStepPhase;
|
|
name: string;
|
|
type: JobStepType;
|
|
status: JobStepStatus;
|
|
output: string | null;
|
|
tokensInput: number | null;
|
|
tokensOutput: number | null;
|
|
startedAt: Date | null;
|
|
completedAt: Date | null;
|
|
durationMs: number | null;
|
|
}>
|
|
) {
|
|
return {
|
|
id: "step-123",
|
|
jobId: "job-123",
|
|
ordinal: 1,
|
|
phase: JobStepPhase.VALIDATION,
|
|
name: "Validate requirements",
|
|
type: JobStepType.TOOL,
|
|
status: JobStepStatus.PENDING,
|
|
output: null,
|
|
tokensInput: null,
|
|
tokensOutput: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
durationMs: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a mock JobEvent
|
|
*/
|
|
export function createMockJobEvent(
|
|
overrides?: Partial<{
|
|
id: string;
|
|
jobId: string;
|
|
stepId: string | null;
|
|
type: string;
|
|
timestamp: Date;
|
|
actor: string;
|
|
payload: unknown;
|
|
}>
|
|
) {
|
|
return {
|
|
id: "event-123",
|
|
jobId: "job-123",
|
|
stepId: null,
|
|
type: "job.created",
|
|
timestamp: new Date(),
|
|
actor: "stitcher",
|
|
payload: {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a mock Prisma service with commonly used methods
|
|
*/
|
|
export function createMockPrismaService(): Partial<PrismaService> {
|
|
const jobs = new Map<string, ReturnType<typeof createMockRunnerJob>>();
|
|
const steps = new Map<string, ReturnType<typeof createMockJobStep>>();
|
|
const events: ReturnType<typeof createMockJobEvent>[] = [];
|
|
|
|
return {
|
|
runnerJob: {
|
|
create: vi.fn().mockImplementation(({ data }) => {
|
|
// Use a counter to ensure unique IDs even if called in quick succession
|
|
const timestamp = Date.now();
|
|
const randomSuffix = Math.floor(Math.random() * 1000);
|
|
const job = createMockRunnerJob({
|
|
id: `job-${timestamp}-${randomSuffix}`,
|
|
workspaceId: data.workspaceId || data.workspace?.connect?.id,
|
|
type: data.type,
|
|
status: data.status,
|
|
priority: data.priority,
|
|
progressPercent: data.progressPercent,
|
|
});
|
|
jobs.set(job.id, job);
|
|
return Promise.resolve(job);
|
|
}),
|
|
findUnique: vi.fn().mockImplementation(({ where, include }) => {
|
|
const job = jobs.get(where.id);
|
|
if (!job) return Promise.resolve(null);
|
|
|
|
const result = { ...job };
|
|
if (include?.steps) {
|
|
(result as never)["steps"] = Array.from(steps.values()).filter((s) => s.jobId === job.id);
|
|
}
|
|
if (include?.events) {
|
|
(result as never)["events"] = events.filter((e) => e.jobId === job.id);
|
|
}
|
|
return Promise.resolve(result);
|
|
}),
|
|
findMany: vi.fn().mockImplementation(({ where }) => {
|
|
const allJobs = Array.from(jobs.values());
|
|
if (!where) return Promise.resolve(allJobs);
|
|
|
|
return Promise.resolve(
|
|
allJobs.filter((job) => {
|
|
if (where.workspaceId && job.workspaceId !== where.workspaceId) return false;
|
|
if (where.status && job.status !== where.status) return false;
|
|
return true;
|
|
})
|
|
);
|
|
}),
|
|
update: vi.fn().mockImplementation(({ where, data }) => {
|
|
const job = jobs.get(where.id);
|
|
if (!job) return Promise.resolve(null);
|
|
|
|
const updated = { ...job, ...data };
|
|
jobs.set(job.id, updated);
|
|
return Promise.resolve(updated);
|
|
}),
|
|
count: vi.fn().mockImplementation(() => Promise.resolve(jobs.size)),
|
|
} as never,
|
|
jobStep: {
|
|
create: vi.fn().mockImplementation(({ data }) => {
|
|
const step = createMockJobStep({
|
|
id: `step-${Date.now()}`,
|
|
jobId: data.jobId || data.job?.connect?.id,
|
|
ordinal: data.ordinal,
|
|
phase: data.phase,
|
|
name: data.name,
|
|
type: data.type,
|
|
status: data.status,
|
|
});
|
|
steps.set(step.id, step);
|
|
return Promise.resolve(step);
|
|
}),
|
|
findUnique: vi.fn().mockImplementation(({ where }) => {
|
|
const step = steps.get(where.id);
|
|
return Promise.resolve(step || null);
|
|
}),
|
|
findMany: vi.fn().mockImplementation(({ where }) => {
|
|
const allSteps = Array.from(steps.values());
|
|
if (!where) return Promise.resolve(allSteps);
|
|
|
|
return Promise.resolve(allSteps.filter((step) => step.jobId === where.jobId));
|
|
}),
|
|
update: vi.fn().mockImplementation(({ where, data }) => {
|
|
const step = steps.get(where.id);
|
|
if (!step) return Promise.resolve(null);
|
|
|
|
const updated = { ...step, ...data };
|
|
steps.set(step.id, updated);
|
|
return Promise.resolve(updated);
|
|
}),
|
|
} as never,
|
|
jobEvent: {
|
|
create: vi.fn().mockImplementation(({ data }) => {
|
|
const event = createMockJobEvent({
|
|
id: `event-${Date.now()}`,
|
|
jobId: data.jobId || data.job?.connect?.id,
|
|
stepId: data.stepId || data.step?.connect?.id || null,
|
|
type: data.type,
|
|
timestamp: data.timestamp || new Date(),
|
|
actor: data.actor,
|
|
payload: data.payload,
|
|
});
|
|
events.push(event);
|
|
return Promise.resolve(event);
|
|
}),
|
|
findMany: vi.fn().mockImplementation(({ where, orderBy }) => {
|
|
let filtered = events;
|
|
if (where?.jobId) {
|
|
filtered = filtered.filter((e) => e.jobId === where.jobId);
|
|
}
|
|
if (orderBy?.timestamp) {
|
|
filtered = [...filtered].sort((a, b) =>
|
|
orderBy.timestamp === "asc"
|
|
? a.timestamp.getTime() - b.timestamp.getTime()
|
|
: b.timestamp.getTime() - a.timestamp.getTime()
|
|
);
|
|
}
|
|
return Promise.resolve(filtered);
|
|
}),
|
|
} as never,
|
|
workspace: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
id: "workspace-123",
|
|
slug: "test-workspace",
|
|
name: "Test Workspace",
|
|
}),
|
|
} as never,
|
|
};
|
|
}
|