feat(scenario): step output, getStepOutput helper, export/import MCP tools, doc restructure

- add output column to ScenarioRunStepEntity; exec step return value is JSON-serialised and stored
- expose helpers.getStepOutput(order) in exec code; negative order is relative to current step
- add get_scenario_run, wait_for_scenario_run, export_scenario, import_scenario MCP tools
- move Architecture, Key descriptors, MCP tools, Scenario, Development docs to docs/
- delete CONTRIBUTING.md (replaced by docs/development.md)
This commit is contained in:
2026-04-08 19:02:36 +03:00
parent e2813208ba
commit 71bcc8faec
17 changed files with 667 additions and 224 deletions
+34
View File
@@ -0,0 +1,34 @@
# Architecture
```
MCP client (AI agent / VS Code Copilot)
│ HTTP /mcp (Streamable MCP)
┌──────────────────────────────────────┐
│ NestJS app (src/) │
│ │
│ McpService ← all MCP tools │
│ │
│ AuthService login + sessions │
│ SessionService session CRUD │
│ EnvironmentService environment CRUD │
│ BrowserService ad-hoc browsing │
│ CodeExecutorService sandboxed JS │
│ │
│ ScenarioService scenario CRUD │
│ ScenarioSchedulerService │
│ └─ @Interval(1 s) tick loop │
└──────────────────┬───────────────────┘
│ better-sqlite3 (TypeORM)
data/sessions.db
```
- **McpService** — registers every MCP tool and delegates to domain services.
- **AuthService** — drives a Playwright browser to log in via a key descriptor; saves cookies + localStorage as a `Session`.
- **SessionService** — stores and retrieves saved browser sessions.
- **EnvironmentService** — stores named environments (URL maps) used by auth and scenarios.
- **BrowserService** — ad-hoc, stateless `open_url` / `exec_code` operations outside the scenario scheduler.
- **CodeExecutorService** — compiles and runs user-supplied Playwright JS inside a sandboxed `async` function with `page`, `context`, and `helpers` in scope.
- **ScenarioService** — CRUD for scenarios, steps, runs, and run logs; import/export.
- **ScenarioSchedulerService** — polls for pending step runs every second and executes them. All steps in the same run share **one browser instance** (see [scenario.md](scenario.md)).
+143
View File
@@ -0,0 +1,143 @@
# Development
## Prerequisites
- Node.js 20+
- Docker + Docker Compose
Install dependencies:
```bash
npm install
npx playwright install chromium # first time only
```
Copy `.env.example` to `.env` and fill in the required values before running.
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `PORT` | `3000` | HTTP port |
| `KEYS_DIR` | `keys` | Directory containing `*.json` key descriptors |
| `DB_PATH` | `data/sessions.db` | SQLite database file |
| `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` | _(Playwright default)_ | Path to Chromium binary. Set automatically in Docker (`/usr/bin/chromium`). |
---
## Starting the application
**Development** (watch mode, restarts on file changes):
```bash
npm run start:dev
```
**Production build, then start**:
```bash
npm run build
npm run start:prod
```
The application listens on port **3000** by default.
---
## Running tests
```bash
npm run test
```
Run a single spec file:
```bash
npm run test -- --no-coverage test/scenario.controller.spec.ts
```
Watch mode:
```bash
npm run test:watch
```
Enable verbose NestJS log output during tests:
```bash
npm run test:debug
```
---
## Formatting code
[Prettier](https://prettier.io/) is used to format all TypeScript source and test files:
```bash
npm run format
```
This rewrites `src/**/*.ts` and `test/**/*.ts` in place.
---
## Linting
[ESLint](https://eslint.org/) with `typescript-eslint` and `eslint-config-prettier` is used:
```bash
# report issues
npm run lint
# report and auto-fix where possible
npm run lint:fix
```
The project targets zero errors. Run lint before committing.
---
## Building the container
```bash
docker compose build
```
To rebuild without the layer cache:
```bash
docker compose build --no-cache
```
---
## Running with Docker Compose
Start (detached):
```bash
docker compose up -d
```
Build and start in one step:
```bash
docker compose up -d --build
```
The application is exposed at **http://localhost:13000**.
SQLite data is persisted in `./data/` and key files are mounted from `./keys/` — both directories are volume-mounted into the container.
Stop and remove containers:
```bash
docker compose down
```
View logs:
```bash
docker compose logs -f
```
+20
View File
@@ -0,0 +1,20 @@
# Key descriptors
Each file in `KEYS_DIR` is named `<keyId>.json` and takes one of two forms:
**File key (EDS):**
```json
{
"keyFile": "path/to/key.dat",
"password": "secret"
}
```
`keyFile` is resolved relative to `KEYS_DIR`.
**Login + password:**
```json
{
"login": "user@example.com",
"password": "secret"
}
```
+49
View File
@@ -0,0 +1,49 @@
# MCP tools
## Auth / session
| Tool | Description |
|---|---|
| `list_keys` | List available key IDs from `KEYS_DIR` |
| `login` | Log in with a key against an environment; stores resulting session |
| `list_sessions` | Paginated list of saved sessions |
| `delete_session` | Delete a session by ID |
## Environments
| Tool | Description |
|---|---|
| `list_environments` | Paginated list of environments |
| `get_environment` | Get one environment by ID |
| `create_environment` | Create an environment (`name` + `urls` map) |
| `update_environment` | Update name / urls |
| `delete_environment` | Delete by ID |
`urls` is a free-form map. Recognised keys: `id_url` (auth server), `cabinet_url` (cabinet redirect target), `admin_url`.
## Browser (ad-hoc, stateless)
| Tool | Description |
|---|---|
| `open_url` | Navigate to a URL and return page content / reader-mode text |
| `exec_code` | Run Playwright JS in a session's browser context |
## Scenarios
| Tool | Description |
|---|---|
| `list_scenarios` | Paginated list |
| `get_scenario` | Fetch scenario with all steps |
| `create_scenario` | Create a new scenario by name |
| `update_scenario` | Rename |
| `delete_scenario` | Delete (cascades to steps and runs) |
| `create_scenario_step` | Add a step |
| `get_scenario_step` | Fetch one step |
| `update_scenario_step` | Update step fields |
| `delete_scenario_step` | Remove a step |
| `list_scenario_runs` | Paginated run history (with optional status filter) |
| `run_scenario` | Trigger an immediate run |
| `get_scenario_run` | Get a run with all step runs and their outputs |
| `wait_for_scenario_run` | Block until run reaches `pass`/`fail`, return full run |
| `export_scenario` | Export scenario as a portable JSON payload |
| `import_scenario` | Import a scenario from an export payload |
+80
View File
@@ -0,0 +1,80 @@
# Scenario execution
When a scenario is run, the scheduler creates a `ScenarioRun` with one `ScenarioRunStep` per step. Steps are executed **in order** (by `step.order`), one per scheduler tick.
**All steps in the same run share a single Playwright browser instance.** The browser is created on the first `exec` or `sign` step (using session cookies + localStorage), reused for every subsequent step, and closed when the run finishes (pass or fail).
## Step types
### `login`
Authenticates in a **separate, ephemeral browser** and saves the resulting session to the database. Subsequent exec/sign steps load that session into the shared browser.
`execCode` must be a JSON object:
```json
{ "keyId": "3137411915", "environmentName": "liquio-diia-stg" }
```
### `exec`
Runs arbitrary Playwright JS on the **shared page**. The page carries over state from the previous step (URL, form inputs, cookies, etc.).
`execCode` is executed as the body of an `async` function with these variables in scope:
- `page` — current Playwright `Page`
- `context` — current Playwright `BrowserContext`
- `helpers.dumpDom(selector?)` — returns a simplified DOM string
- `helpers.getStepOutput(order)` — returns the JSON-parsed `output` of a previous step. Positive values are absolute step orders; negative values are relative to the current step (`-1` = previous step).
Any value returned from `execCode` is JSON-serialised and stored as the `output` field on the `ScenarioRunStep` record. Subsequent steps can read it via `helpers.getStepOutput()`.
```js
// example execCode
await page.goto('https://cabinet.example.com/tasks/create/987825');
await page.locator('input[name="institution"]').fill('caltech');
await page.locator('button:has-text("Далі")').click();
await page.waitForLoadState('networkidle');
return page.url(); // stored in ScenarioRunStep.output
```
### `sign`
Performs EDS signing on the **current shared page** using a key from `KEYS_DIR`. The step clicks the sign widget, selects the file key method, uploads the key file, enters the password, and submits — all without requiring the step author to handle key material in their code.
`execCode` must be a JSON object:
```json
{ "keyId": "3137411915" }
```
## Validation code
Any step type optionally accepts `validateCode` — Playwright JS that runs on the shared page **after** the main action and must return a result evaluated as:
```js
return { success: true, description: 'Application created' };
// or
return { success: false, description: 'Expected success banner not found' };
// or simply
return someBoolean;
```
If `success` is false the step (and the whole run) fails with the provided `description`.
## Run statuses
`pending``in_progress``pass` | `fail`
Individual step runs also expose `waiting` (not yet reached) and `cancelled` (run failed before this step).
## Adding a new scenario (example workflow)
```
1. create_environment (if not already present)
2. create_scenario → get scenarioId
3. create_scenario_step type=login, order=0
4. create_scenario_step type=exec, order=1 (navigate + fill form)
5. create_scenario_step type=sign, order=2
6. create_scenario_step type=exec, order=3 (validateCode checks success banner)
7. run_scenario → get runId
8. wait_for_scenario_run (blocks until pass/fail, returns full run with outputs)
```