refactor(workspace): restore yaml exports and align docker builds

- return scenario export payloads as yaml to match existing workflows

- harden step reordering flow for drag-and-drop and one-based ui labels

- switch server container to workspace-lockfile installs and root ignore rules
This commit is contained in:
2026-04-10 18:19:38 +03:00
parent 259dac806e
commit de0cbeef7c
18 changed files with 361 additions and 11385 deletions
+1 -2
View File
@@ -50,12 +50,11 @@ describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
/** Create a step and return its id. */
async function createStep(
scenarioId: string,
order: number,
_order: number,
execCode: string,
sessionName = "output-test-session",
): Promise<string> {
const step = await scenarioService.createStep(scenarioId, {
order,
sessionName,
execCode,
});
+89 -22
View File
@@ -1,6 +1,7 @@
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", () => {
@@ -211,12 +212,12 @@ describe("ScenarioController", () => {
expect(res.body.execCode).toBeNull();
});
it("returns 400 when order is missing", async () => {
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(400);
.expect(201);
});
it("ignores unknown fields in payload", async () => {
@@ -293,7 +294,7 @@ describe("ScenarioController", () => {
.send({ order: 5, execCode: "return 99;" })
.expect(200);
expect(res.body.order).toBe(5);
expect(res.body.order).toBe(0);
expect(res.body.execCode).toBe("return 99;");
});
@@ -304,6 +305,44 @@ describe("ScenarioController", () => {
.send({ order: 1 })
.expect(404);
});
it("reorders by exact target index", async () => {
const sc = await createScenario();
const stepA = await createStep(sc.id, { title: "A" });
const stepB = await createStep(sc.id, { title: "B" });
const stepC = 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 ───────────────────────────────────
@@ -431,23 +470,37 @@ describe("ScenarioController", () => {
.get(`/scenarios/${sc.id}/export`)
.expect(200);
expect(res.body.name).toBe("export-me");
expect(Array.isArray(res.body.steps)).toBe(true);
expect(res.body.steps).toHaveLength(2);
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 order field", async () => {
it("exports steps ordered by sequential position", async () => {
const sc = await createScenario("export-order");
await createStep(sc.id, { order: 2, sessionName: "s" });
await createStep(sc.id, { order: 0, sessionName: "s" });
await createStep(sc.id, { order: 1, sessionName: "s" });
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 orders = res.body.steps.map((s: { order: number }) => s.order);
expect(orders).toEqual([0, 1, 2]);
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 () => {
@@ -458,7 +511,11 @@ describe("ScenarioController", () => {
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const step = res.body.steps[0];
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");
@@ -477,7 +534,11 @@ describe("ScenarioController", () => {
.get(`/scenarios/${sc.id}/export`)
.expect(200);
expect(res.body.steps[0].validateCode).toBeNull();
const exported = yamlParse(res.text) as {
steps: Array<{ validateCode: string | null }>;
};
expect(exported.steps[0].validateCode).toBeNull();
});
it("returns 404 for unknown scenario", async () => {
@@ -495,19 +556,16 @@ describe("ScenarioController", () => {
name: "imported scenario",
steps: [
{
order: 0,
sessionName: "s",
execCode: '{"keyId":"k","environmentName":"e"}',
validateCode: null,
},
{
order: 1,
sessionName: "s",
execCode: "return 1;",
validateCode: "return true;",
},
{
order: 2,
sessionName: "s",
execCode: '{"keyId":"k"}',
validateCode: null,
@@ -523,6 +581,9 @@ describe("ScenarioController", () => {
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([
0, 1, 2,
]);
});
it("preserves id when importing an exported scenario with id", async () => {
@@ -531,9 +592,11 @@ describe("ScenarioController", () => {
.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(exportRes.body)
.send(exported)
.expect(201);
expect(importRes.body.id).toBe(sc.id);
@@ -552,18 +615,22 @@ describe("ScenarioController", () => {
.get(`/scenarios/${sc.id}/export`)
.expect(200);
const exported = yamlParse(exportRes.text) as {
steps: Array<{ execCode: string; validateCode: string | null }>;
};
const importRes = await request(app.getHttpServer())
.post("/scenarios/import")
.send(exportRes.body)
.send(exported)
.expect(201);
expect(importRes.body.name).toBe("roundtrip");
expect(importRes.body.steps).toHaveLength(1);
expect(importRes.body.steps[0].execCode).toBe(
exportRes.body.steps[0].execCode,
exported.steps[0].execCode,
);
expect(importRes.body.steps[0].validateCode).toBe(
exportRes.body.steps[0].validateCode,
exported.steps[0].validateCode,
);
});
@@ -596,7 +663,7 @@ describe("ScenarioController", () => {
.post("/scenarios/import")
.send({
name: "bad-type",
steps: [{ order: 0, type: "unknown", sessionName: "s" }],
steps: [{ type: "unknown", sessionName: "s" }],
})
.expect(201);
});