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:
2026-04-17 13:46:30 +03:00
parent 8dbd319bba
commit a50dce2755
19 changed files with 402 additions and 12 deletions
+82
View File
@@ -81,6 +81,29 @@ describe("ScenarioController", () => {
.send({})
.expect(400);
});
it("saves timeoutSeconds when provided", async () => {
const res = await request(app.getHttpServer())
.post("/scenarios")
.send({ name: "timeout-sc", timeoutSeconds: 300 })
.expect(201);
expect(res.body.timeoutSeconds).toBe(300);
});
it("stores null timeoutSeconds when not provided", async () => {
const res = await request(app.getHttpServer())
.post("/scenarios")
.send({ name: "no-timeout-sc" })
.expect(201);
expect(res.body.timeoutSeconds).toBeNull();
});
it("returns 400 for timeoutSeconds below 1", async () => {
await request(app.getHttpServer())
.post("/scenarios")
.send({ name: "bad-timeout", timeoutSeconds: 0 })
.expect(400);
});
});
// ── GET /scenarios ─────────────────────────────────────────────────────────
@@ -181,6 +204,27 @@ describe("ScenarioController", () => {
expect(res.body.name).toBe("patched");
});
it("updates timeoutSeconds", async () => {
const sc = await createScenario("timeout-patch");
const res = await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}`)
.send({ timeoutSeconds: 120 })
.expect(200);
expect(res.body.timeoutSeconds).toBe(120);
});
it("clears timeoutSeconds to null", async () => {
const created = await request(app.getHttpServer())
.post("/scenarios")
.send({ name: "clear-timeout", timeoutSeconds: 120 })
.expect(201);
const res = await request(app.getHttpServer())
.patch(`/scenarios/${created.body.id}`)
.send({ timeoutSeconds: null })
.expect(200);
expect(res.body.timeoutSeconds).toBeNull();
});
it("returns 404 for unknown id", async () => {
await request(app.getHttpServer())
.patch("/scenarios/00000000-0000-0000-0000-000000000001")
@@ -260,6 +304,24 @@ describe("ScenarioController", () => {
.expect(201);
});
it("saves timeoutSeconds on step when provided", async () => {
const sc = await createScenario();
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ execCode: "return 1;", timeoutSeconds: 45 })
.expect(201);
expect(res.body.timeoutSeconds).toBe(45);
});
it("stores null step timeoutSeconds when not provided", async () => {
const sc = await createScenario();
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ execCode: "return 1;" })
.expect(201);
expect(res.body.timeoutSeconds).toBeNull();
});
it("returns 404 for unknown scenario", async () => {
await request(app.getHttpServer())
.post("/scenarios/00000000-0000-0000-0000-000000000001/steps")
@@ -320,6 +382,26 @@ describe("ScenarioController", () => {
expect(res.body.execCode).toBe("return 99;");
});
it("updates step timeoutSeconds", async () => {
const sc = await createScenario();
const step = await createStep(sc.id);
const res = await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}/steps/${step.id}`)
.send({ timeoutSeconds: 90 })
.expect(200);
expect(res.body.timeoutSeconds).toBe(90);
});
it("clears step timeoutSeconds to null", async () => {
const sc = await createScenario();
const step = await createStep(sc.id, { timeoutSeconds: 90 });
const res = await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}/steps/${step.id}`)
.send({ timeoutSeconds: null })
.expect(200);
expect(res.body.timeoutSeconds).toBeNull();
});
it("returns 404 for unknown step", async () => {
const sc = await createScenario();
await request(app.getHttpServer())