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:
@@ -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)).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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 |
|
||||
@@ -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)
|
||||
```
|
||||
Reference in New Issue
Block a user