feat(scenarios): add timeoutSeconds to scenarios and steps
- add timeoutSeconds field to scenario and step entities - add timeoutSeconds to create/update DTOs for scenario and step - enforce timeout via scheduler: abort run if step exceeds limit - expose timeoutSeconds in MCP create/update scenario and step tools - add client-side type, API, i18n, and form support for timeoutSeconds - add integration tests for timeout persistence via REST and MCP
This commit is contained in:
@@ -193,7 +193,7 @@ export const scenarios = {
|
||||
body: JSON.stringify({ name, description, environmentId }),
|
||||
});
|
||||
},
|
||||
update(id: string, patch: Partial<Pick<Scenario, 'name' | 'description' | 'environmentId'>>): Promise<Scenario> {
|
||||
update(id: string, patch: Partial<Pick<Scenario, 'name' | 'description' | 'environmentId' | 'timeoutSeconds'>>): Promise<Scenario> {
|
||||
return request(`/scenarios/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
@@ -254,7 +254,7 @@ export const runs = {
|
||||
orderDir: 'ASC' | 'DESC' = 'DESC',
|
||||
): Promise<
|
||||
PaginatedResponse<
|
||||
ScenarioRun & { stepRuns: ScenarioRunStep[]; scenario: { id: string; name: string } }
|
||||
ScenarioRun & { scenario: { id: string; name: string } }
|
||||
>
|
||||
> {
|
||||
const q = new URLSearchParams({ page: String(page), limit: String(limit), orderBy, orderDir });
|
||||
@@ -299,12 +299,14 @@ export const scenarioCredentials = {
|
||||
export interface CreateStepPayload {
|
||||
title?: string;
|
||||
execCode?: string;
|
||||
timeoutSeconds?: number | null;
|
||||
}
|
||||
|
||||
export interface UpdateStepPayload {
|
||||
title?: string;
|
||||
order?: number;
|
||||
execCode?: string;
|
||||
timeoutSeconds?: number | null;
|
||||
}
|
||||
|
||||
export const steps = {
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface ScenarioStep {
|
||||
order: number;
|
||||
title: string | null;
|
||||
execCode: string | null;
|
||||
timeoutSeconds: number | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -85,6 +86,7 @@ export interface Scenario {
|
||||
name: string;
|
||||
description?: string;
|
||||
environmentId?: string | null;
|
||||
timeoutSeconds?: number | null;
|
||||
environment?: Pick<Environment, 'id' | 'name'>;
|
||||
steps?: ScenarioStep[];
|
||||
scenarioCredentials?: ScenarioCredential[];
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
"export_modal_title": "Export Scenario",
|
||||
"export_include_environment": "Include Environment",
|
||||
"export_include_credentials": "Include Credentials",
|
||||
"form_timeout": "Scenario Timeout (seconds)",
|
||||
"form_timeout_placeholder": "Default: 600",
|
||||
"form_timeout_hint": "Max seconds for the entire scenario run (default: 600)",
|
||||
"field_name": "Name",
|
||||
"field_id": "ID"
|
||||
},
|
||||
@@ -222,7 +225,10 @@
|
||||
"form_session_placeholder": "e.g. my-session",
|
||||
"form_session_required": "Session name is required",
|
||||
"form_exec_code": "Exec code",
|
||||
"form_exec_code_placeholder": "return await page.title();"
|
||||
"form_exec_code_placeholder": "return await page.title();",
|
||||
"form_timeout": "Step Timeout (seconds)",
|
||||
"form_timeout_placeholder": "Default: 60",
|
||||
"form_timeout_hint": "Max seconds for this step (default: 60)"
|
||||
},
|
||||
"runs": {
|
||||
"title": "Runs",
|
||||
|
||||
@@ -33,7 +33,7 @@ const STATUS_LABEL: Record<ScenarioRunStatus, string> = {
|
||||
};
|
||||
|
||||
type AllRunRow = ScenarioRun & {
|
||||
scenario: { id: number; name: string };
|
||||
scenario: { id: string; name: string };
|
||||
};
|
||||
|
||||
export function AllRunsPage() {
|
||||
|
||||
@@ -16,6 +16,7 @@ export function CreateStepPage() {
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [title, setTitle] = useState('');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,6 +34,7 @@ export function CreateStepPage() {
|
||||
await steps.create(id!, {
|
||||
title: title.trim() || undefined,
|
||||
execCode: execCode.trim() || undefined,
|
||||
timeoutSeconds: timeoutSeconds.trim() ? Number(timeoutSeconds) : undefined,
|
||||
});
|
||||
toast.success(t('steps.created'));
|
||||
navigate(`/scenarios/${id}`);
|
||||
@@ -76,6 +78,15 @@ export function CreateStepPage() {
|
||||
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
|
||||
<CodeEditor value={execCode} onChange={setExecCode} rows={6} />
|
||||
</div>
|
||||
<Input
|
||||
label={t('steps.form_timeout')}
|
||||
placeholder={t('steps.form_timeout_placeholder')}
|
||||
type="number"
|
||||
min={1}
|
||||
value={timeoutSeconds}
|
||||
onChange={(e) => setTimeoutSeconds(e.target.value)}
|
||||
hint={t('steps.form_timeout_hint')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
|
||||
@@ -17,6 +17,7 @@ export function EditScenarioPage() {
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [environmentId, setEnvironmentId] = useState<string>('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
const [envs, setEnvs] = useState<Environment[]>([]);
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -31,6 +32,7 @@ export function EditScenarioPage() {
|
||||
setName(data.name);
|
||||
setDescription(data.description || '');
|
||||
setEnvironmentId(data.environmentId ?? '');
|
||||
setTimeoutSeconds(data.timeoutSeconds != null ? String(data.timeoutSeconds) : '');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
environments.list(1, 200).then((r) => setEnvs(r?.data ?? [])).catch(() => {});
|
||||
@@ -48,6 +50,7 @@ export function EditScenarioPage() {
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
environmentId: environmentId || null,
|
||||
timeoutSeconds: timeoutSeconds.trim() ? Number(timeoutSeconds) : null,
|
||||
});
|
||||
toast.success(t('scenarios.updated'));
|
||||
navigate(`/scenarios/${id}`);
|
||||
@@ -114,6 +117,15 @@ export function EditScenarioPage() {
|
||||
...envs.map((env) => ({ value: env.id, label: env.name })),
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
label={t('scenarios.form_timeout')}
|
||||
placeholder={t('scenarios.form_timeout_placeholder')}
|
||||
type="number"
|
||||
min={1}
|
||||
value={timeoutSeconds}
|
||||
onChange={(e) => setTimeoutSeconds(e.target.value)}
|
||||
hint={t('scenarios.form_timeout_hint')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
|
||||
@@ -17,6 +17,7 @@ export function EditStepPage() {
|
||||
const [step, setStep] = useState<ScenarioStep | null>(null);
|
||||
const [title, setTitle] = useState('');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -28,6 +29,7 @@ export function EditStepPage() {
|
||||
setStep(st);
|
||||
setTitle(st.title ?? '');
|
||||
setExecCode(st.execCode ?? '');
|
||||
setTimeoutSeconds(st.timeoutSeconds != null ? String(st.timeoutSeconds) : '');
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id, stepId]);
|
||||
@@ -39,6 +41,7 @@ export function EditStepPage() {
|
||||
await steps.update(id!, stepId!, {
|
||||
title: title.trim() || undefined,
|
||||
execCode: execCode.trim() || undefined,
|
||||
timeoutSeconds: timeoutSeconds.trim() ? Number(timeoutSeconds) : null,
|
||||
});
|
||||
toast.success(t('steps.updated'));
|
||||
navigate(`/scenarios/${id}`);
|
||||
@@ -64,7 +67,7 @@ export function EditStepPage() {
|
||||
label: scenario?.name ?? `#${id}`,
|
||||
onClick: () => navigate(`/scenarios/${id}`),
|
||||
},
|
||||
{ label: t('steps.edit_title', { order: step ? step.order + 1 : stepId }) },
|
||||
{ label: t('steps.edit_title', { order: step ? step.order : stepId }) },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -85,6 +88,15 @@ export function EditStepPage() {
|
||||
<label className={styles.fieldLabel}>{t('steps.form_exec_code')}</label>
|
||||
<CodeEditor value={execCode} onChange={setExecCode} rows={6} />
|
||||
</div>
|
||||
<Input
|
||||
label={t('steps.form_timeout')}
|
||||
placeholder={t('steps.form_timeout_placeholder')}
|
||||
type="number"
|
||||
min={1}
|
||||
value={timeoutSeconds}
|
||||
onChange={(e) => setTimeoutSeconds(e.target.value)}
|
||||
hint={t('steps.form_timeout_hint')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
|
||||
@@ -288,7 +288,7 @@ export function ScenarioDetailPage() {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ key: 'order', header: t('scenarios.step_order'), render: (s) => s.order + 1, width: 60 },
|
||||
{ key: 'order', header: t('scenarios.step_order'), render: (s) => s.order, width: 60 },
|
||||
{
|
||||
key: 'title',
|
||||
header: t('scenarios.step_title'),
|
||||
|
||||
Reference in New Issue
Block a user