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:
@@ -141,4 +141,147 @@ describe("McpController", () => {
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── create_scenario / update_scenario timeoutSeconds ──────────────────────
|
||||
|
||||
describe("create_scenario with timeoutSeconds", () => {
|
||||
it("saves timeoutSeconds on the created scenario", async () => {
|
||||
const { status, rpc } = await mcpCall("create_scenario", {
|
||||
name: "mcp-timeout-sc",
|
||||
timeoutSeconds: 300,
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const created = JSON.parse(result.content[0].text) as {
|
||||
name: string;
|
||||
timeoutSeconds: number | null;
|
||||
};
|
||||
expect(created.name).toBe("mcp-timeout-sc");
|
||||
expect(created.timeoutSeconds).toBe(300);
|
||||
});
|
||||
|
||||
it("stores null timeoutSeconds when not provided", async () => {
|
||||
const { rpc } = await mcpCall("create_scenario", {
|
||||
name: "mcp-no-timeout",
|
||||
});
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const created = JSON.parse(result.content[0].text) as {
|
||||
timeoutSeconds: number | null;
|
||||
};
|
||||
expect(created.timeoutSeconds).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("update_scenario timeoutSeconds", () => {
|
||||
it("updates and clears timeoutSeconds", async () => {
|
||||
// create
|
||||
const createRpc = (
|
||||
await mcpCall("create_scenario", { name: "mcp-upd-timeout" })
|
||||
).rpc;
|
||||
const created = JSON.parse(
|
||||
(createRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { id: string };
|
||||
|
||||
// set
|
||||
const setRpc = (
|
||||
await mcpCall("update_scenario", {
|
||||
id: created.id,
|
||||
timeoutSeconds: 120,
|
||||
})
|
||||
).rpc;
|
||||
const updated = JSON.parse(
|
||||
(setRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { timeoutSeconds: number | null };
|
||||
expect(updated.timeoutSeconds).toBe(120);
|
||||
|
||||
// clear
|
||||
const clearRpc = (
|
||||
await mcpCall("update_scenario", {
|
||||
id: created.id,
|
||||
timeoutSeconds: null,
|
||||
})
|
||||
).rpc;
|
||||
const cleared = JSON.parse(
|
||||
(clearRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { timeoutSeconds: number | null };
|
||||
expect(cleared.timeoutSeconds).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── create_scenario_step / update_scenario_step timeoutSeconds ────────────
|
||||
|
||||
describe("create_scenario_step with timeoutSeconds", () => {
|
||||
it("saves timeoutSeconds on the created step", async () => {
|
||||
// create a scenario first
|
||||
const scRpc = (
|
||||
await mcpCall("create_scenario", { name: "mcp-step-timeout-parent" })
|
||||
).rpc;
|
||||
const sc = JSON.parse(
|
||||
(scRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { id: string };
|
||||
|
||||
const { status, rpc } = await mcpCall("create_scenario_step", {
|
||||
scenarioId: sc.id,
|
||||
order: 0,
|
||||
type: "exec",
|
||||
execCode: "return 1;",
|
||||
timeoutSeconds: 45,
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
const result = rpc.result as { content: { text: string }[] };
|
||||
const step = JSON.parse(result.content[0].text) as {
|
||||
timeoutSeconds: number | null;
|
||||
};
|
||||
expect(step.timeoutSeconds).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update_scenario_step timeoutSeconds", () => {
|
||||
it("updates and clears step timeoutSeconds", async () => {
|
||||
// create scenario + step
|
||||
const scRpc = (
|
||||
await mcpCall("create_scenario", { name: "mcp-step-upd-parent" })
|
||||
).rpc;
|
||||
const sc = JSON.parse(
|
||||
(scRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { id: string };
|
||||
|
||||
const stepRpc = (
|
||||
await mcpCall("create_scenario_step", {
|
||||
scenarioId: sc.id,
|
||||
order: 0,
|
||||
type: "exec",
|
||||
})
|
||||
).rpc;
|
||||
const step = JSON.parse(
|
||||
(stepRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { id: string };
|
||||
|
||||
// set
|
||||
const setRpc = (
|
||||
await mcpCall("update_scenario_step", {
|
||||
scenarioId: sc.id,
|
||||
stepId: step.id,
|
||||
timeoutSeconds: 90,
|
||||
})
|
||||
).rpc;
|
||||
const updated = JSON.parse(
|
||||
(setRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { timeoutSeconds: number | null };
|
||||
expect(updated.timeoutSeconds).toBe(90);
|
||||
|
||||
// clear
|
||||
const clearRpc = (
|
||||
await mcpCall("update_scenario_step", {
|
||||
scenarioId: sc.id,
|
||||
stepId: step.id,
|
||||
timeoutSeconds: null,
|
||||
})
|
||||
).rpc;
|
||||
const cleared = JSON.parse(
|
||||
(clearRpc.result as { content: { text: string }[] }).content[0].text,
|
||||
) as { timeoutSeconds: number | null };
|
||||
expect(cleared.timeoutSeconds).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user