- move NestJS app into server/ subdirectory - add client/ React+TypeScript (Vite) app with Hello World - update docker-compose to build and run both services - add root package.json declaring npm workspaces - update .gitignore to cover node_modules and dist at all depths
205 lines
7.5 KiB
TypeScript
205 lines
7.5 KiB
TypeScript
/**
|
|
* Integration tests for ScenarioRunStepEntity.output column and the
|
|
* helpers.getStepOutput() API available inside exec step scripts.
|
|
*
|
|
* Strategy:
|
|
* - Seed a session row so the scheduler can create a browser context.
|
|
* - Create a scenario + steps with controlled return values.
|
|
* - Call ScenarioSchedulerService.pickUpPendingRuns() directly to process
|
|
* the run synchronously (no real timers needed).
|
|
* - Assert step-run output persisted and getStepOutput() returns it.
|
|
*/
|
|
|
|
import { INestApplication } from "@nestjs/common";
|
|
import { DataSource } from "typeorm";
|
|
import { buildTestApp } from "./app.harness";
|
|
import { ScenarioService } from "../src/scenario/scenario.service";
|
|
import { ScenarioSchedulerService } from "../src/scenario/scenario-scheduler.service";
|
|
|
|
describe("ScenarioRunStepEntity.output + helpers.getStepOutput", () => {
|
|
let app: INestApplication;
|
|
let dataSource: DataSource;
|
|
let scenarioService: ScenarioService;
|
|
let scheduler: ScenarioSchedulerService;
|
|
|
|
beforeAll(async () => {
|
|
app = await buildTestApp();
|
|
dataSource = app.get(DataSource);
|
|
scenarioService = app.get(ScenarioService);
|
|
scheduler = app.get(ScenarioSchedulerService);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
/** Seed a minimal session so the scheduler can open a browser context. */
|
|
async function seedSession(name = "output-test-session"): Promise<void> {
|
|
await dataSource.query(
|
|
`INSERT OR IGNORE INTO sessions (sessionName, token, cookies, localStorage)
|
|
VALUES ('${name}', 'tok', '[]', '{}')`,
|
|
);
|
|
}
|
|
|
|
/** Create a scenario and return its id. */
|
|
async function createScenario(name = "output-scenario"): Promise<number> {
|
|
const sc = await scenarioService.create({ name });
|
|
return sc.id;
|
|
}
|
|
|
|
/** Create a step and return its id. */
|
|
async function createStep(
|
|
scenarioId: number,
|
|
order: number,
|
|
execCode: string,
|
|
sessionName = "output-test-session",
|
|
): Promise<number> {
|
|
const step = await scenarioService.createStep(scenarioId, {
|
|
order,
|
|
type: "exec",
|
|
sessionName,
|
|
execCode,
|
|
});
|
|
return step.id;
|
|
}
|
|
|
|
/** Trigger a run and process it to completion via the scheduler. */
|
|
async function runScenario(scenarioId: number): Promise<number> {
|
|
const run = await scenarioService.createRun(scenarioId);
|
|
// Drive the scheduler directly — keeps tests synchronous and fast.
|
|
await scheduler.pickUpPendingRuns();
|
|
// Wait for the run to reach a terminal state (max 10 s).
|
|
const result = await scenarioService.waitForRun(scenarioId, run.id, 10_000);
|
|
return result.id;
|
|
}
|
|
|
|
// ── output column ──────────────────────────────────────────────────────────
|
|
|
|
describe("output column", () => {
|
|
it("stores the return value of an exec script as JSON", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("output-basic");
|
|
await createStep(scId, 0, "return 42;");
|
|
|
|
const runId = await runScenario(scId);
|
|
|
|
const row = await dataSource.query(
|
|
`SELECT output, status FROM scenario_run_steps WHERE runId = ${runId}`,
|
|
);
|
|
expect(row[0].status).toBe("pass");
|
|
expect(JSON.parse(row[0].output as string)).toBe(42);
|
|
});
|
|
|
|
it("stores object return values as JSON", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("output-object");
|
|
await createStep(scId, 0, 'return { foo: "bar", n: 7 };');
|
|
|
|
const runId = await runScenario(scId);
|
|
|
|
const row = await dataSource.query(
|
|
`SELECT output FROM scenario_run_steps WHERE runId = ${runId}`,
|
|
);
|
|
expect(JSON.parse(row[0].output as string)).toEqual({ foo: "bar", n: 7 });
|
|
});
|
|
|
|
it("stores null when the script returns undefined/nothing", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("output-undefined");
|
|
await createStep(scId, 0, "const x = 1;");
|
|
|
|
const runId = await runScenario(scId);
|
|
|
|
const row = await dataSource.query(
|
|
`SELECT output FROM scenario_run_steps WHERE runId = ${runId}`,
|
|
);
|
|
expect(row[0].output).toBeNull();
|
|
});
|
|
|
|
it("output is exposed on the stepRuns inside GET /scenarios/:id/run/:runId via findRun", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("output-findrun");
|
|
await createStep(scId, 0, "return 'hello';");
|
|
|
|
const runId = await runScenario(scId);
|
|
const run = await scenarioService.findRun(scId, runId);
|
|
|
|
expect(run.stepRuns[0].output).toBe(JSON.stringify("hello"));
|
|
});
|
|
});
|
|
|
|
// ── helpers.getStepOutput ──────────────────────────────────────────────────
|
|
|
|
describe("helpers.getStepOutput()", () => {
|
|
it("returns output of a previous step by absolute order", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("getStepOutput-absolute");
|
|
await createStep(scId, 0, "return 99;");
|
|
// Step 1 reads step 0's output via absolute index 0
|
|
await createStep(scId, 1, "return await helpers.getStepOutput(0);");
|
|
|
|
const runId = await runScenario(scId);
|
|
const rows = await dataSource.query(
|
|
`SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`,
|
|
);
|
|
|
|
expect(JSON.parse(rows[0].output as string)).toBe(99);
|
|
expect(JSON.parse(rows[1].output as string)).toBe(99);
|
|
});
|
|
|
|
it("returns output of the previous step using relative index -1", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("getStepOutput-relative");
|
|
await createStep(scId, 0, 'return "step-zero";');
|
|
// Step 1 uses relative index -1 to reference step 0
|
|
await createStep(scId, 1, "return await helpers.getStepOutput(-1);");
|
|
|
|
const runId = await runScenario(scId);
|
|
const rows = await dataSource.query(
|
|
`SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`,
|
|
);
|
|
|
|
expect(JSON.parse(rows[1].output as string)).toBe("step-zero");
|
|
});
|
|
|
|
it("returns null for a step that does not exist", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("getStepOutput-missing");
|
|
// Step 0 tries to read step order 99 which does not exist
|
|
await createStep(scId, 0, "return await helpers.getStepOutput(99);");
|
|
|
|
const runId = await runScenario(scId);
|
|
const row = await dataSource.query(
|
|
`SELECT output FROM scenario_run_steps WHERE runId = ${runId}`,
|
|
);
|
|
|
|
expect(row[0].output).toBeNull();
|
|
});
|
|
|
|
it("chains output across three steps", async () => {
|
|
await seedSession();
|
|
const scId = await createScenario("getStepOutput-chain");
|
|
await createStep(scId, 0, "return [1, 2];");
|
|
await createStep(
|
|
scId,
|
|
1,
|
|
"const prev = await helpers.getStepOutput(-1); return [...prev, 3];",
|
|
);
|
|
await createStep(
|
|
scId,
|
|
2,
|
|
"const prev = await helpers.getStepOutput(-1); return [...prev, 4];",
|
|
);
|
|
|
|
const runId = await runScenario(scId);
|
|
const rows = await dataSource.query(
|
|
`SELECT "order", output FROM scenario_run_steps WHERE runId = ${runId} ORDER BY "order"`,
|
|
);
|
|
|
|
expect(JSON.parse(rows[0].output as string)).toEqual([1, 2]);
|
|
expect(JSON.parse(rows[1].output as string)).toEqual([1, 2, 3]);
|
|
expect(JSON.parse(rows[2].output as string)).toEqual([1, 2, 3, 4]);
|
|
});
|
|
});
|
|
});
|