chore(repo): restructure as monorepo with server and client workspaces
- 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
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user