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>
This commit is contained in:
2026-02-01 21:44:04 -06:00
parent d3058cb3de
commit 3cdcbf6774
9 changed files with 1089 additions and 0 deletions

View File

@@ -194,4 +194,27 @@ export class JobEventsService {
payload,
});
}
/**
* Get all events for a job (no pagination)
* Alias for getEventsByJobId without pagination
*/
async findByJob(
jobId: string
): Promise<Awaited<ReturnType<typeof this.prisma.jobEvent.findMany>>> {
// Verify job exists
const job = await this.prisma.runnerJob.findUnique({
where: { id: jobId },
select: { id: true },
});
if (!job) {
throw new NotFoundException(`RunnerJob with ID ${jobId} not found`);
}
return this.prisma.jobEvent.findMany({
where: { jobId },
orderBy: { timestamp: "asc" },
});
}
}

View File

@@ -324,4 +324,76 @@ export class RunnerJobsService {
}
}
}
/**
* Update job status
*/
async updateStatus(
id: string,
workspaceId: string,
status: RunnerJobStatus,
data?: { result?: unknown; error?: string }
): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.update>>> {
// Verify job exists
const existingJob = await this.prisma.runnerJob.findUnique({
where: { id, workspaceId },
});
if (!existingJob) {
throw new NotFoundException(`RunnerJob with ID ${id} not found`);
}
const updateData: Prisma.RunnerJobUpdateInput = {
status,
};
// Set timestamps based on status
if (status === RunnerJobStatus.RUNNING && !existingJob.startedAt) {
updateData.startedAt = new Date();
}
if (
status === RunnerJobStatus.COMPLETED ||
status === RunnerJobStatus.FAILED ||
status === RunnerJobStatus.CANCELLED
) {
updateData.completedAt = new Date();
}
// Add optional data
if (data?.result !== undefined) {
updateData.result = data.result as Prisma.InputJsonValue;
}
if (data?.error !== undefined) {
updateData.error = data.error;
}
return this.prisma.runnerJob.update({
where: { id, workspaceId },
data: updateData,
});
}
/**
* Update job progress percentage
*/
async updateProgress(
id: string,
workspaceId: string,
progressPercent: number
): Promise<Awaited<ReturnType<typeof this.prisma.runnerJob.update>>> {
// Verify job exists
const existingJob = await this.prisma.runnerJob.findUnique({
where: { id, workspaceId },
});
if (!existingJob) {
throw new NotFoundException(`RunnerJob with ID ${id} not found`);
}
return this.prisma.runnerJob.update({
where: { id, workspaceId },
data: { progressPercent },
});
}
}