feat(scenario): run logs, lint/format tooling, CONTRIBUTING

- add ScenarioRunLogEntity to persist step script output to DB
- stepLogger dual-writes to NestJS logger and DB (fire-and-forget)
- add GET /scenarios/:id/run/:runId returning run, stepRuns and logs
- add POST /scenarios/:id/run/:runId/wait (polls until terminal state)
- 9 new integration tests for the two endpoints (136 total)
- add eslint with typescript-eslint and eslint-config-prettier
- add npm scripts: format, lint, lint:fix
- resolve all lint errors across src and test (no any types)
- add CONTRIBUTING.md covering dev workflow
This commit is contained in:
2026-04-08 18:10:42 +03:00
parent b0c02a1cdc
commit 9a9ccbfe37
67 changed files with 3809 additions and 1412 deletions
+50 -44
View File
@@ -1,9 +1,9 @@
import { INestApplication } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import request from 'supertest';
import { buildTestApp } from './app.harness';
import { SessionEntity } from '../src/session/session.entity';
import { Repository } from 'typeorm';
import { INestApplication } from "@nestjs/common";
import { getRepositoryToken } from "@nestjs/typeorm";
import request from "supertest";
import { buildTestApp } from "./app.harness";
import { SessionEntity } from "../src/session/session.entity";
import { Repository } from "typeorm";
/**
* Browser controller integration tests.
@@ -12,24 +12,26 @@ import { Repository } from 'typeorm';
* validation rejections (no browser launched) and session-not-found paths,
* which are safe to run in a headless CI environment.
*/
describe('BrowserController', () => {
describe("BrowserController", () => {
let app: INestApplication;
let sessionRepo: Repository<SessionEntity>;
const FAKE_SESSION = 'test-browser-session';
const FAKE_SESSION = "test-browser-session";
beforeAll(async () => {
app = await buildTestApp();
sessionRepo = app.get<Repository<SessionEntity>>(getRepositoryToken(SessionEntity));
sessionRepo = app.get<Repository<SessionEntity>>(
getRepositoryToken(SessionEntity),
);
// Seed a session with minimal but valid JSON so the browser code can
// deserialise it (it will still fail to open a real page, tested separately)
await sessionRepo.save(
sessionRepo.create({
sessionName: FAKE_SESSION,
token: 'fake-token',
cookies: '[]',
localStorage: '{}',
token: "fake-token",
cookies: "[]",
localStorage: "{}",
}),
);
});
@@ -40,29 +42,29 @@ describe('BrowserController', () => {
// ── POST /open ─────────────────────────────────────────────────────────────
describe('POST /open', () => {
it('returns 400 when body is empty', async () => {
await request(app.getHttpServer()).post('/open').send({}).expect(400);
describe("POST /open", () => {
it("returns 400 when body is empty", async () => {
await request(app.getHttpServer()).post("/open").send({}).expect(400);
});
it('returns 400 when url is missing', async () => {
it("returns 400 when url is missing", async () => {
await request(app.getHttpServer())
.post('/open')
.post("/open")
.send({ sessionName: FAKE_SESSION })
.expect(400);
});
it('returns 404 when session does not exist', async () => {
it("returns 404 when session does not exist", async () => {
await request(app.getHttpServer())
.post('/open')
.send({ sessionName: 'no-such-session', url: 'https://example.com' })
.post("/open")
.send({ sessionName: "no-such-session", url: "https://example.com" })
.expect(404);
});
it('succeeds without a session (sessionless open)', async () => {
it("succeeds without a session (sessionless open)", async () => {
const res = await request(app.getHttpServer())
.post('/open')
.send({ url: 'https://example.com' })
.post("/open")
.send({ url: "https://example.com" })
.expect(201);
expect(res.body).toMatchObject({
url: expect.any(String),
@@ -71,55 +73,59 @@ describe('BrowserController', () => {
});
});
it('returns only selector content when selector is provided', async () => {
it("returns only selector content when selector is provided", async () => {
const res = await request(app.getHttpServer())
.post('/open')
.send({ url: 'https://example.com', selector: '#mock' })
.post("/open")
.send({ url: "https://example.com", selector: "#mock" })
.expect(201);
expect(res.body.content).toBe('<div id="mock">mock content</div>');
});
it('returns selector text content in reader mode', async () => {
it("returns selector text content in reader mode", async () => {
const res = await request(app.getHttpServer())
.post('/open')
.send({ url: 'https://example.com', selector: '#mock', readerMode: true })
.post("/open")
.send({
url: "https://example.com",
selector: "#mock",
readerMode: true,
})
.expect(201);
expect(res.body.content).toBe('mock content');
expect(res.body.content).toBe("mock content");
});
});
// ── POST /exec ─────────────────────────────────────────────────────────────
describe('POST /exec', () => {
it('returns 400 when body is empty', async () => {
await request(app.getHttpServer()).post('/exec').send({}).expect(400);
describe("POST /exec", () => {
it("returns 400 when body is empty", async () => {
await request(app.getHttpServer()).post("/exec").send({}).expect(400);
});
it('returns 400 when code is missing', async () => {
it("returns 400 when code is missing", async () => {
await request(app.getHttpServer())
.post('/exec')
.post("/exec")
.send({ sessionName: FAKE_SESSION })
.expect(400);
});
it('returns 400 when code has a syntax error', async () => {
it("returns 400 when code has a syntax error", async () => {
await request(app.getHttpServer())
.post('/exec')
.send({ sessionName: FAKE_SESSION, code: 'this is not valid {{{' })
.post("/exec")
.send({ sessionName: FAKE_SESSION, code: "this is not valid {{{" })
.expect(400);
});
it('returns 404 when session does not exist', async () => {
it("returns 404 when session does not exist", async () => {
await request(app.getHttpServer())
.post('/exec')
.send({ sessionName: 'no-such-session', code: 'return 1;' })
.post("/exec")
.send({ sessionName: "no-such-session", code: "return 1;" })
.expect(404);
});
it('succeeds without a session (sessionless exec)', async () => {
it("succeeds without a session (sessionless exec)", async () => {
const res = await request(app.getHttpServer())
.post('/exec')
.send({ code: 'return 42;' })
.post("/exec")
.send({ code: "return 42;" })
.expect(201);
expect(res.body).toEqual({ result: 42 });
});