feat(scenario): add scenario runs, run steps, and step ordering
- add ScenarioRunEntity (pending|in_progress|pass|fail) and ScenarioRunStepEntity (waiting|pending|in_progress|pass|fail) - add POST /scenarios/:id/run — creates run with first step pending, rest waiting - add order field to ScenarioStepEntity and ScenarioRunStepEntity for deterministic sequential execution - findOne and createRun now sort steps/stepRuns by order ASC
This commit is contained in:
+3
-1
@@ -10,6 +10,8 @@ import { EnvironmentModule } from './environment/environment.module';
|
||||
import { McpModule } from './mcp/mcp.module';
|
||||
import { ScenarioEntity } from './scenario/scenario.entity';
|
||||
import { ScenarioStepEntity } from './scenario/scenario-step.entity';
|
||||
import { ScenarioRunEntity } from './scenario/scenario-run.entity';
|
||||
import { ScenarioRunStepEntity } from './scenario/scenario-run-step.entity';
|
||||
import { ScenarioModule } from './scenario/scenario.module';
|
||||
|
||||
@Module({
|
||||
@@ -23,7 +25,7 @@ import { ScenarioModule } from './scenario/scenario.module';
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'better-sqlite3',
|
||||
database: config.get<string>('DB_PATH', 'data/sessions.db'),
|
||||
entities: [SessionEntity, EnvironmentEntity, ScenarioEntity, ScenarioStepEntity],
|
||||
entities: [SessionEntity, EnvironmentEntity, ScenarioEntity, ScenarioStepEntity, ScenarioRunEntity, ScenarioRunStepEntity],
|
||||
synchronize: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { StepType } from '../scenario-step.entity';
|
||||
|
||||
export class CreateScenarioStepDto {
|
||||
@ApiProperty({ example: 0, description: 'Execution order (ascending)' })
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
order: number;
|
||||
|
||||
@ApiProperty({ enum: ['login', 'exec'], example: 'exec' })
|
||||
@IsIn(['login', 'exec'])
|
||||
type: StepType;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { StepType } from '../scenario-step.entity';
|
||||
|
||||
export class UpdateScenarioStepDto {
|
||||
@ApiPropertyOptional({ example: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
order?: number;
|
||||
|
||||
@ApiPropertyOptional({ enum: ['login', 'exec'] })
|
||||
@IsOptional()
|
||||
@IsIn(['login', 'exec'])
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ScenarioRunEntity } from './scenario-run.entity';
|
||||
import { ScenarioStepEntity } from './scenario-step.entity';
|
||||
|
||||
export type RunStepStatus = 'waiting' | 'pending' | 'in_progress' | 'pass' | 'fail';
|
||||
|
||||
@Entity('scenario_run_steps')
|
||||
export class ScenarioRunStepEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
runId: number;
|
||||
|
||||
@ManyToOne(() => ScenarioRunEntity, (run) => run.stepRuns, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'runId' })
|
||||
run: ScenarioRunEntity;
|
||||
|
||||
@Column()
|
||||
scenarioStepId: number;
|
||||
|
||||
@ManyToOne(() => ScenarioStepEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'scenarioStepId' })
|
||||
scenarioStep: ScenarioStepEntity;
|
||||
|
||||
@Column({ type: 'text', default: 'waiting' })
|
||||
status: RunStepStatus;
|
||||
|
||||
@Column({ default: 0 })
|
||||
order: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ScenarioEntity } from './scenario.entity';
|
||||
import { ScenarioRunStepEntity } from './scenario-run-step.entity';
|
||||
|
||||
export type RunStatus = 'pending' | 'in_progress' | 'pass' | 'fail';
|
||||
|
||||
@Entity('scenario_runs')
|
||||
export class ScenarioRunEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
scenarioId: number;
|
||||
|
||||
@ManyToOne(() => ScenarioEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'scenarioId' })
|
||||
scenario: ScenarioEntity;
|
||||
|
||||
@Column({ type: 'text', default: 'pending' })
|
||||
status: RunStatus;
|
||||
|
||||
@OneToMany(() => ScenarioRunStepEntity, (rs) => rs.run, {
|
||||
cascade: true,
|
||||
eager: false,
|
||||
})
|
||||
stepRuns: ScenarioRunStepEntity[];
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export class ScenarioStepEntity {
|
||||
@JoinColumn({ name: 'scenarioId' })
|
||||
scenario: ScenarioEntity;
|
||||
|
||||
@Column({ default: 0 })
|
||||
order: number;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
type: StepType;
|
||||
|
||||
|
||||
@@ -111,4 +111,14 @@ export class ScenarioController {
|
||||
) {
|
||||
return this.scenarioService.removeStep(id, stepId);
|
||||
}
|
||||
|
||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@Post(':id/run')
|
||||
@ApiOperation({ summary: 'Create a new run for a scenario' })
|
||||
@ApiResponse({ status: 201, description: 'Run created with step runs' })
|
||||
@ApiResponse({ status: 404, description: 'Scenario not found' })
|
||||
createRun(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.scenarioService.createRun(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ScenarioEntity } from './scenario.entity';
|
||||
import { ScenarioStepEntity } from './scenario-step.entity';
|
||||
import { ScenarioRunEntity } from './scenario-run.entity';
|
||||
import { ScenarioRunStepEntity } from './scenario-run-step.entity';
|
||||
import { ScenarioService } from './scenario.service';
|
||||
import { ScenarioController } from './scenario.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ScenarioEntity, ScenarioStepEntity])],
|
||||
imports: [TypeOrmModule.forFeature([ScenarioEntity, ScenarioStepEntity, ScenarioRunEntity, ScenarioRunStepEntity])],
|
||||
controllers: [ScenarioController],
|
||||
providers: [ScenarioService],
|
||||
exports: [ScenarioService],
|
||||
|
||||
@@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ScenarioEntity } from './scenario.entity';
|
||||
import { ScenarioStepEntity } from './scenario-step.entity';
|
||||
import { ScenarioRunEntity } from './scenario-run.entity';
|
||||
import { ScenarioRunStepEntity } from './scenario-run-step.entity';
|
||||
import { CreateScenarioDto } from './dto/create-scenario.dto';
|
||||
import { UpdateScenarioDto } from './dto/update-scenario.dto';
|
||||
import { CreateScenarioStepDto } from './dto/create-scenario-step.dto';
|
||||
@@ -23,6 +25,10 @@ export class ScenarioService {
|
||||
private readonly scenarioRepo: Repository<ScenarioEntity>,
|
||||
@InjectRepository(ScenarioStepEntity)
|
||||
private readonly stepRepo: Repository<ScenarioStepEntity>,
|
||||
@InjectRepository(ScenarioRunEntity)
|
||||
private readonly runRepo: Repository<ScenarioRunEntity>,
|
||||
@InjectRepository(ScenarioRunStepEntity)
|
||||
private readonly runStepRepo: Repository<ScenarioRunStepEntity>,
|
||||
) {}
|
||||
|
||||
// ── Scenarios ─────────────────────────────────────────────────────────────
|
||||
@@ -46,7 +52,7 @@ export class ScenarioService {
|
||||
const scenario = await this.scenarioRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['steps'],
|
||||
order: { steps: { id: 'ASC' } },
|
||||
order: { steps: { order: 'ASC' } },
|
||||
});
|
||||
if (!scenario) throw new NotFoundException(`Scenario ${id} not found`);
|
||||
return scenario;
|
||||
@@ -93,4 +99,33 @@ export class ScenarioService {
|
||||
await this.findStep(scenarioId, stepId);
|
||||
await this.stepRepo.delete(stepId);
|
||||
}
|
||||
|
||||
// ── Runs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async createRun(scenarioId: number): Promise<ScenarioRunEntity> {
|
||||
const scenario = await this.findOne(scenarioId);
|
||||
|
||||
const run = await this.runRepo.save(
|
||||
this.runRepo.create({ scenarioId, status: 'pending' }),
|
||||
);
|
||||
|
||||
const stepRuns = scenario.steps.map((step, index) =>
|
||||
this.runStepRepo.create({
|
||||
runId: run.id,
|
||||
scenarioStepId: step.id,
|
||||
order: step.order,
|
||||
status: index === 0 ? 'pending' : 'waiting',
|
||||
description: null,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.runStepRepo.save(stepRuns);
|
||||
|
||||
return this.runRepo.findOne({
|
||||
where: { id: run.id },
|
||||
relations: ['stepRuns'],
|
||||
order: { stepRuns: { order: 'ASC' } },
|
||||
}) as Promise<ScenarioRunEntity>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user