fix(scenario-runner): harden run failure handling across all paths

- add onModuleInit to fail in_progress runs on server restart
  (playwright sessions cannot be recovered after restart)
- extract failRun() shared method used by restart cleanup, timeout,
  and unexpected error paths for consistent step run status updates
- fix catch path to also fail/cancel step runs (previously left orphaned)
- accept plain {name, steps} payload in importScenario alongside
  full export format; throw BadRequestException for invalid input
- include stepRuns relation in findRuns paginated response
- add scenario-scheduler.spec.ts integration tests for onModuleInit
This commit is contained in:
2026-04-17 14:11:43 +03:00
parent a50dce2755
commit 2a46b355db
3 changed files with 261 additions and 18 deletions
+22 -2
View File
@@ -1,4 +1,5 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
@@ -302,6 +303,7 @@ export class ScenarioService {
if (query.status) where["status"] = query.status;
const [data, total] = await this.runRepo.findAndCount({
where,
relations: ["stepRuns"],
order: { [orderBy]: orderDir },
skip: (page - 1) * limit,
take: limit,
@@ -492,6 +494,18 @@ export class ScenarioService {
async importScenario(
payload: ScenarioExportDto | ExportEntity[],
): Promise<ScenarioEntity> {
// Accept plain {name, steps} objects (no kind) as well as the full export format
const raw = payload as unknown as Record<string, unknown>;
if (!Array.isArray(payload) && !raw["kind"]) {
if (typeof raw["name"] !== "string" || !raw["name"]) {
throw new BadRequestException("name is required");
}
if (!Array.isArray(raw["steps"])) {
throw new BadRequestException("steps must be an array");
}
(payload as unknown as Record<string, unknown>)["kind"] = "scenario";
}
const items: ExportEntity[] = Array.isArray(payload) ? payload : [payload];
let scenarioDto: ScenarioExportDto | undefined;
@@ -535,13 +549,19 @@ export class ScenarioService {
}),
);
}
} else if (item.kind === "scenario") {
} else if (!item.kind || item.kind === "scenario") {
scenarioDto = item as ScenarioExportDto;
}
}
if (!scenarioDto) {
throw new Error("No scenario entity found in import payload");
throw new BadRequestException("No scenario entity found in import payload");
}
if (typeof scenarioDto.name !== "string" || !scenarioDto.name) {
throw new BadRequestException("name is required");
}
if (!Array.isArray(scenarioDto.steps)) {
throw new BadRequestException("steps must be an array");
}
const dto = scenarioDto;