/** * 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 { 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 { const sc = await scenarioService.create({ name }); return sc.id; } /** Create a step and return its id. */ async function createStep( scenarioId: string, _order: number, execCode: string, sessionName = "output-test-session", ): Promise { const step = await scenarioService.createStep(scenarioId, { sessionName, execCode, }); return step.id; } /** Trigger a run and process it to completion via the scheduler. */ async function runScenario(scenarioId: string): Promise { 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]); }); }); });