Files
stack/apps/api/test/fixtures/mock-prisma.fixture.ts
Jason Woltje 3cdcbf6774 feat(#175): Implement E2E test harness
- 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>
2026-02-01 21:44:04 -06:00

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,
};
}