Files
liqa/server/test/scenario.controller.spec.ts
T
ars9 4e494ce4fd feat(scenarios): show last run status and timestamp on scenarios list
- extend findAll to join latest run per scenario via correlated subquery
- return lastRunStatus and lastRunAt fields in GET /scenarios response
- add Last Run column with badge and timestamp to ScenariosPage
- add integration tests for null and populated lastRunStatus
2026-04-17 15:13:28 +03:00

910 lines
30 KiB
TypeScript

import { INestApplication } from "@nestjs/common";
import request from "supertest";
import { DataSource } from "typeorm";
import { parse as yamlParse } from "yaml";
import { buildTestApp } from "./app.harness";
describe("ScenarioController", () => {
let app: INestApplication;
beforeAll(async () => {
app = await buildTestApp();
});
afterAll(async () => {
await app.close();
});
// ── helpers ────────────────────────────────────────────────────────────────
async function createScenario(name = "test scenario") {
const res = await request(app.getHttpServer())
.post("/scenarios")
.send({ name })
.expect(201);
return res.body as { id: string; name: string };
}
async function createStep(
scenarioId: string,
overrides: Record<string, unknown> = {},
) {
const res = await request(app.getHttpServer())
.post(`/scenarios/${scenarioId}/steps`)
.send({
order: 0,
sessionName: "test-session",
execCode: "return 1;",
...overrides,
})
.expect(201);
return res.body as { id: string };
}
async function createEnvironment(name = `env-${Math.random()}`) {
const res = await request(app.getHttpServer())
.post("/environments")
.send({ name, data: {} })
.expect(201);
return res.body as { id: string; name: string };
}
async function createRun(scenarioId: string) {
const env = await createEnvironment();
const res = await request(app.getHttpServer())
.post(`/scenarios/${scenarioId}/run`)
.send({ environmentId: env.id })
.expect(201);
return res.body as {
id: string;
status: string;
stepRuns: Array<{ status: string }>;
};
}
// ── POST /scenarios ────────────────────────────────────────────────────────
describe("POST /scenarios", () => {
it("creates a scenario and returns 201", async () => {
const res = await request(app.getHttpServer())
.post("/scenarios")
.send({ name: "my scenario" })
.expect(201);
expect(res.body.id).toBeDefined();
expect(res.body.name).toBe("my scenario");
});
it("returns 400 when name is missing", async () => {
await request(app.getHttpServer())
.post("/scenarios")
.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 ─────────────────────────────────────────────────────────
describe("GET /scenarios", () => {
it("returns paginated result", async () => {
const res = await request(app.getHttpServer())
.get("/scenarios")
.expect(200);
expect(res.body).toHaveProperty("data");
expect(res.body).toHaveProperty("total");
expect(res.body).toHaveProperty("page");
expect(res.body).toHaveProperty("limit");
expect(Array.isArray(res.body.data)).toBe(true);
});
it("respects page and limit params", async () => {
await createScenario("paged-sc-a");
await createScenario("paged-sc-b");
const res = await request(app.getHttpServer())
.get("/scenarios?page=1&limit=1")
.expect(200);
expect(res.body.data).toHaveLength(1);
expect(res.body.limit).toBe(1);
expect(res.body.page).toBe(1);
});
it("returns empty data for out-of-range page", async () => {
const res = await request(app.getHttpServer())
.get("/scenarios?page=9999&limit=20")
.expect(200);
expect(res.body.data).toHaveLength(0);
});
it("returns 400 for invalid pagination params", async () => {
await request(app.getHttpServer()).get("/scenarios?page=0").expect(400);
});
it("orders by name ASC", async () => {
await createScenario("zzz-order-sc");
await createScenario("aaa-order-sc");
const res = await request(app.getHttpServer())
.get("/scenarios?orderBy=name&orderDir=ASC")
.expect(200);
const names: string[] = res.body.data.map(
(s: { name: string }) => s.name,
);
expect(names).toEqual([...names].sort());
});
it("orders by name DESC", async () => {
const res = await request(app.getHttpServer())
.get("/scenarios?orderBy=name&orderDir=DESC")
.expect(200);
const names: string[] = res.body.data.map(
(s: { name: string }) => s.name,
);
expect(names).toEqual([...names].sort().reverse());
});
it("returns 400 for invalid orderDir", async () => {
await request(app.getHttpServer())
.get("/scenarios?orderDir=SIDEWAYS")
.expect(400);
});
it("returns lastRunStatus and lastRunAt as null when scenario has no runs", async () => {
await createScenario("no-runs-sc");
const res = await request(app.getHttpServer())
.get("/scenarios?orderBy=name&orderDir=ASC")
.expect(200);
const item = res.body.data.find(
(s: { name: string }) => s.name === "no-runs-sc",
);
expect(item).toBeDefined();
expect(item.lastRunStatus).toBeNull();
expect(item.lastRunAt).toBeNull();
});
it("returns lastRunStatus and lastRunAt reflecting the most recent run", async () => {
const sc = await createScenario("last-run-status-sc");
await createRun(sc.id);
const res = await request(app.getHttpServer())
.get("/scenarios?orderBy=name&orderDir=ASC")
.expect(200);
const item = res.body.data.find(
(s: { name: string }) => s.name === "last-run-status-sc",
);
expect(item).toBeDefined();
expect(["pending", "in_progress", "pass", "fail"]).toContain(
item.lastRunStatus,
);
expect(typeof item.lastRunAt).toBe("string");
});
});
// ── GET /scenarios/:id ─────────────────────────────────────────────────────
describe("GET /scenarios/:id", () => {
it("returns the scenario with steps array", async () => {
const sc = await createScenario("scenario-get-one");
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}`)
.expect(200);
expect(res.body.id).toBe(sc.id);
expect(Array.isArray(res.body.steps)).toBe(true);
});
it("returns 404 for unknown id", async () => {
await request(app.getHttpServer())
.get("/scenarios/00000000-0000-0000-0000-000000000001")
.expect(404);
});
});
// ── PATCH /scenarios/:id ───────────────────────────────────────────────────
describe("PATCH /scenarios/:id", () => {
it("updates scenario name", async () => {
const sc = await createScenario("patch-me");
const res = await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}`)
.send({ name: "patched" })
.expect(200);
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")
.send({ name: "x" })
.expect(404);
});
});
// ── DELETE /scenarios/:id ──────────────────────────────────────────────────
describe("DELETE /scenarios/:id", () => {
it("deletes and returns 204", async () => {
const sc = await createScenario("delete-me");
await request(app.getHttpServer())
.delete(`/scenarios/${sc.id}`)
.expect(204);
await request(app.getHttpServer()).get(`/scenarios/${sc.id}`).expect(404);
});
it("returns 404 for unknown id", async () => {
await request(app.getHttpServer())
.delete("/scenarios/00000000-0000-0000-0000-000000000001")
.expect(404);
});
});
// ── POST /scenarios/:id/steps ──────────────────────────────────────────────
describe("POST /scenarios/:id/steps", () => {
it("creates a step with required fields", 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.id).toBeDefined();
expect(res.body.order).toBe(1);
});
it("creates a step without execCode", async () => {
const sc = await createScenario();
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ title: "step without exec" })
.expect(201);
expect(res.body.title).toBe("step without exec");
expect(res.body.execCode).toBeNull();
});
it("allows creating a step without explicit order", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ sessionName: "x" })
.expect(201);
});
it("ignores unknown fields in payload", async () => {
const sc = await createScenario();
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ order: 0, type: "unknown", sessionName: "x" })
.expect(201);
expect(res.body).not.toHaveProperty("type");
});
it("allows missing sessionName", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/steps`)
.send({ order: 0, execCode: "return 1;" })
.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")
.send({ order: 0, sessionName: "x" })
.expect(404);
});
it("returns steps ordered by order field", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 2, type: "exec", sessionName: "s" });
await createStep(sc.id, { order: 0, type: "exec", sessionName: "s" });
await createStep(sc.id, { order: 1, type: "exec", sessionName: "s" });
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}`)
.expect(200);
const orders = res.body.steps.map((s: { order: number }) => s.order);
expect(orders).toEqual([1, 2, 3]);
});
});
// ── GET /scenarios/:id/steps/:stepId ──────────────────────────────────────
describe("GET /scenarios/:id/steps/:stepId", () => {
it("returns the step", async () => {
const sc = await createScenario();
const step = await createStep(sc.id);
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/steps/${step.id}`)
.expect(200);
expect(res.body.id).toBe(step.id);
});
it("returns 404 for unknown step", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/steps/00000000-0000-0000-0000-000000000001`)
.expect(404);
});
});
// ── PATCH /scenarios/:id/steps/:stepId ────────────────────────────────────
describe("PATCH /scenarios/:id/steps/:stepId", () => {
it("updates step fields", async () => {
const sc = await createScenario();
const step = await createStep(sc.id, { order: 0, execCode: "return 1;" });
const res = await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}/steps/${step.id}`)
.send({ order: 5, execCode: "return 99;" })
.expect(200);
expect(res.body.order).toBe(0);
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())
.patch(`/scenarios/${sc.id}/steps/00000000-0000-0000-0000-000000000001`)
.send({ order: 1 })
.expect(404);
});
it("reorders by exact target index", async () => {
const sc = await createScenario();
const stepA = await createStep(sc.id, { title: "A" });
await createStep(sc.id, { title: "B" });
await createStep(sc.id, { title: "C" });
const stepD = await createStep(sc.id, { title: "D" });
await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}/steps/${stepA.id}`)
.send({ order: 2 })
.expect(200);
let res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}`)
.expect(200);
expect(res.body.steps.map((s: { title: string }) => s.title)).toEqual([
"B",
"C",
"A",
"D",
]);
expect(res.body.steps.map((s: { order: number }) => s.order)).toEqual([
0, 1, 2, 3,
]);
await request(app.getHttpServer())
.patch(`/scenarios/${sc.id}/steps/${stepD.id}`)
.send({ order: 0 })
.expect(200);
res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}`)
.expect(200);
expect(res.body.steps.map((s: { title: string }) => s.title)).toEqual([
"D",
"B",
"C",
"A",
]);
expect(res.body.steps.map((s: { order: number }) => s.order)).toEqual([
0, 1, 2, 3,
]);
});
});
// ── DELETE /scenarios/:id/steps/:stepId ───────────────────────────────────
describe("DELETE /scenarios/:id/steps/:stepId", () => {
it("deletes the step and returns 204", async () => {
const sc = await createScenario();
const step = await createStep(sc.id);
await request(app.getHttpServer())
.delete(`/scenarios/${sc.id}/steps/${step.id}`)
.expect(204);
await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/steps/${step.id}`)
.expect(404);
});
});
// ── POST /scenarios/:id/run ────────────────────────────────────────────────
describe("POST /scenarios/:id/run", () => {
it("creates a run with stepRuns in correct initial states", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
await createStep(sc.id, { order: 1 });
await createStep(sc.id, { order: 2 });
const res = await createRun(sc.id);
expect(res.status).toBe("pending");
expect(Array.isArray(res.stepRuns)).toBe(true);
expect(res.stepRuns).toHaveLength(3);
const statuses = res.stepRuns.map((s: { status: string }) => s.status);
expect(statuses[0]).toBe("pending");
expect(statuses[1]).toBe("waiting");
expect(statuses[2]).toBe("waiting");
});
it("returns 404 for unknown scenario", async () => {
const env = await createEnvironment();
await request(app.getHttpServer())
.post("/scenarios/00000000-0000-0000-0000-000000000001/run")
.send({ environmentId: env.id })
.expect(404);
});
});
// ── GET /scenarios/:id/runs ────────────────────────────────────────────────
describe("GET /scenarios/:id/runs", () => {
it("returns paginated runs with stepRuns embedded", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
await createRun(sc.id);
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs`)
.expect(200);
expect(res.body.total).toBeGreaterThanOrEqual(1);
expect(Array.isArray(res.body.data[0].stepRuns)).toBe(true);
});
it("filters by status", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
await createRun(sc.id);
const pendingRes = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs?status=pending`)
.expect(200);
expect(pendingRes.body.total).toBeGreaterThanOrEqual(1);
pendingRes.body.data.forEach((r: { status: string }) =>
expect(r.status).toBe("pending"),
);
const passRes = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs?status=pass`)
.expect(200);
expect(passRes.body.total).toBe(0);
});
it("returns 400 for invalid status filter", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/runs?status=invalid`)
.expect(400);
});
it("returns 404 for unknown scenario", async () => {
await request(app.getHttpServer())
.get("/scenarios/00000000-0000-0000-0000-000000000001/runs")
.expect(404);
});
});
// ── GET /scenarios/:id/export ─────────────────────────────────────────────
describe("GET /scenarios/:id/export", () => {
it("returns name and steps array", async () => {
const sc = await createScenario("export-me");
await createStep(sc.id, {
order: 0,
sessionName: "s",
execCode: '{"keyId":"k","environmentName":"e"}',
});
await createStep(sc.id, {
order: 1,
sessionName: "s",
execCode: "return 1;",
});
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(res.text) as {
name: string;
steps: Array<Record<string, unknown>>;
};
expect(exported.name).toBe("export-me");
expect(Array.isArray(exported.steps)).toBe(true);
expect(exported.steps).toHaveLength(2);
});
it("exports steps ordered by sequential position", async () => {
const sc = await createScenario("export-order");
await createStep(sc.id, {
sessionName: "s",
execCode: "return 'first';",
});
await createStep(sc.id, {
sessionName: "s",
execCode: "return 'second';",
});
await createStep(sc.id, {
sessionName: "s",
execCode: "return 'third';",
});
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(res.text) as {
steps: Array<{ execCode: string }>;
};
const codes = exported.steps.map((s: { execCode: string }) => s.execCode);
expect(codes).toEqual([
"return 'first';",
"return 'second';",
"return 'third';",
]);
expect(exported.steps[0]).not.toHaveProperty("order");
});
it("omits internal fields (id, scenarioId, timestamps)", async () => {
const sc = await createScenario("export-shape");
await createStep(sc.id, { order: 0, sessionName: "s" });
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(res.text) as {
steps: Array<Record<string, unknown>>;
};
const step = exported.steps[0];
expect(step).not.toHaveProperty("id");
expect(step).not.toHaveProperty("scenarioId");
expect(step).not.toHaveProperty("createdAt");
expect(step).not.toHaveProperty("updatedAt");
});
it("returns 404 for unknown scenario", async () => {
await request(app.getHttpServer())
.get("/scenarios/00000000-0000-0000-0000-000000000001/export")
.expect(404);
});
});
// ── POST /scenarios/import ────────────────────────────────────────────────
describe("POST /scenarios/import", () => {
it("creates a new scenario with all steps", async () => {
const payload = {
name: "imported scenario",
steps: [
{
sessionName: "s",
execCode: '{"keyId":"k","environmentName":"e"}',
},
{
sessionName: "s",
execCode: "return 1;",
},
{
sessionName: "s",
execCode: '{"keyId":"k"}',
},
],
};
const res = await request(app.getHttpServer())
.post("/scenarios/import")
.send(payload)
.expect(201);
expect(res.body.id).toBeDefined();
expect(res.body.name).toBe("imported scenario");
expect(res.body.steps).toHaveLength(3);
expect(res.body.steps.map((s: { order: number }) => s.order)).toEqual([
1, 2, 3,
]);
});
it("preserves id when importing an exported scenario with id", async () => {
const sc = await createScenario("original");
const exportRes = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(exportRes.text) as Record<string, unknown>;
const importRes = await request(app.getHttpServer())
.post("/scenarios/import")
.send(exported)
.expect(201);
expect(importRes.body.id).toBe(sc.id);
});
it("round-trips a scenario faithfully", async () => {
const sc = await createScenario("roundtrip");
await createStep(sc.id, {
order: 0,
sessionName: "rs",
execCode: "return 42;",
});
const exportRes = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(exportRes.text) as {
steps: Array<{ execCode: string }>;
};
const importRes = await request(app.getHttpServer())
.post("/scenarios/import")
.send(exported)
.expect(201);
expect(importRes.body.name).toBe("roundtrip");
expect(importRes.body.steps).toHaveLength(1);
expect(importRes.body.steps[0].execCode).toBe(exported.steps[0].execCode);
});
it("imports with empty steps array", async () => {
const res = await request(app.getHttpServer())
.post("/scenarios/import")
.send({ name: "empty-import", steps: [] })
.expect(201);
expect(res.body.name).toBe("empty-import");
expect(res.body.steps).toHaveLength(0);
});
it("returns 400 when name is missing", async () => {
await request(app.getHttpServer())
.post("/scenarios/import")
.send({ steps: [] })
.expect(400);
});
it("returns 400 when steps is not an array", async () => {
await request(app.getHttpServer())
.post("/scenarios/import")
.send({ name: "bad", steps: "oops" })
.expect(400);
});
it("ignores unknown step fields during import", async () => {
await request(app.getHttpServer())
.post("/scenarios/import")
.send({
name: "bad-type",
steps: [{ type: "unknown", sessionName: "s" }],
})
.expect(201);
});
});
// ── GET /scenarios/:id/run/:runId ─────────────────────────────────────────
describe("GET /scenarios/:id/run/:runId", () => {
it("returns run with stepRuns (with scenarioStep) and logs array", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
const runRes = await createRun(sc.id);
const runId = runRes.id;
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/run/${runId}`)
.expect(200);
expect(res.body.id).toBe(runId);
expect(res.body.status).toBe("pending");
expect(Array.isArray(res.body.stepRuns)).toBe(true);
expect(res.body.stepRuns[0]).toHaveProperty("scenarioStep");
expect(Array.isArray(res.body.logs)).toBe(true);
});
it("stepRuns are ordered by order ASC", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
await createStep(sc.id, { order: 1 });
await createStep(sc.id, { order: 2 });
const runRes = await createRun(sc.id);
const runId = runRes.id;
const res = await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/run/${runId}`)
.expect(200);
const orders = res.body.stepRuns.map((s: { order: number }) => s.order);
expect(orders).toEqual([1, 2, 3]);
});
it("returns 404 for unknown run", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.get(`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001`)
.expect(404);
});
it("returns 404 when run belongs to a different scenario", async () => {
const sc1 = await createScenario();
const sc2 = await createScenario();
await createStep(sc1.id, { order: 0 });
const runRes = await createRun(sc1.id);
const runId = runRes.id;
await request(app.getHttpServer())
.get(`/scenarios/${sc2.id}/run/${runId}`)
.expect(404);
});
it("returns 404 for unknown scenario", async () => {
await request(app.getHttpServer())
.get(
"/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001",
)
.expect(404);
});
});
// ── POST /scenarios/:id/run/:runId/wait ───────────────────────────────────
describe("POST /scenarios/:id/run/:runId/wait", () => {
it("returns 200 with run data immediately when run is already terminal", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
const runRes = await createRun(sc.id);
const runId = runRes.id;
// Manually mark run as pass so wait resolves immediately
const dataSource = app.get(DataSource);
await dataSource.query(
`UPDATE scenario_runs SET status='pass' WHERE id='${runId}'`,
);
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/run/${runId}/wait`)
.expect(200);
expect(res.body.id).toBe(runId);
expect(res.body.status).toBe("pass");
expect(Array.isArray(res.body.logs)).toBe(true);
expect(Array.isArray(res.body.stepRuns)).toBe(true);
});
it("returns the run in fail state when it has failed", async () => {
const sc = await createScenario();
await createStep(sc.id, { order: 0 });
const runRes = await createRun(sc.id);
const runId = runRes.id;
const dataSource = app.get(DataSource);
await dataSource.query(
`UPDATE scenario_runs SET status='fail' WHERE id='${runId}'`,
);
const res = await request(app.getHttpServer())
.post(`/scenarios/${sc.id}/run/${runId}/wait`)
.expect(200);
expect(res.body.status).toBe("fail");
});
it("returns 404 for unknown run", async () => {
const sc = await createScenario();
await request(app.getHttpServer())
.post(
`/scenarios/${sc.id}/run/00000000-0000-0000-0000-000000000001/wait`,
)
.expect(404);
});
it("returns 404 for unknown scenario", async () => {
await request(app.getHttpServer())
.post(
"/scenarios/00000000-0000-0000-0000-000000000001/run/00000000-0000-0000-0000-000000000001/wait",
)
.expect(404);
});
});
});