---
name: n8n:testmax-assistant-ui
description: >-
Tests an n8n Assistant (Instance AI) UI feature end to end. Drafts the test
paths from the feature and the diff, writes one Playwright script for each
path, runs the paths in parallel on fresh n8n instances with a real LLM, and
compares the main Assistant screens with master to find visual
regressions. Shows live screenshots during the run, and builds one HTML
report with video replays, step screenshots, LLM steps, tokens, cost and
visual diffs. Use when the user wants to test an Instance AI or Assistant
UI feature end to end, check that a UI change does not break existing
screens, or says /testmax-assistant-ui.
---
# Test an Instance AI feature
Use this skill to test a UI feature of the n8n Assistant end to end. In code,
the Assistant is "Instance AI". You draft the test paths, the user approves
them, and the skill runs them all at the same time. A local page shows each run
live. At the end, you judge each run and build one report.
The skill uses the scripts of `testmax-n8n-ai` for the n8n API, the
LangSmith usage, the live page server, and the report template. Do not copy
those scripts.
## What the skill does
```mermaid
flowchart LR
A[Feature + diff] --> B[Draft scenarios.json and scripts]
B --> T[--try and --estimate]
T --> C{User approves plan + cost}
C --> D[run.py]
D --> E[One fresh branch instance for each feature + regression]
D --> F[Fresh branch instance visual]
D --> G[Fresh master instance visual, or a kept run]
E & F & G --> H[Live page]
F & G --> I[Pixel + accessibility tree diff]
E & F & G --> J[summary.txt with triage]
J --> K[You judge: verdicts.json]
K --> L[Report rebuilds itself]
```
- Each run is one scenario: a Playwright script that drives the real
Assistant UI in Chrome, with a real LLM.
- There are three kinds of scenario:
- `feature`: tests a path of the new feature.
- `regression`: tests an existing flow that the feature touches.
- `visual`: captures screens. The skill runs it on the branch and on master,
then compares the captures.
- All instances are fresh: a new data folder, a new owner, and no workflows or
threads. Thus the screens of the branch and master are the same when the
code is the same. A fresh instance starts in about 5 seconds, because it
copies a data folder that is already migrated and seeded.
- Each feature and regression scenario gets its own fresh instance, so the
scenarios do not see each other's data. The visual scenarios share one
branch instance and one master instance.
- The feature flags are the same on all instances. The instances do not ask
PostHog for flags, so no instance gets a random experiment variant. Set the
flags that the feature needs in the plan, with `featureFlags`.
- `run.py` builds this checkout first (with turbo, so a build that is up to
date takes about 1 second). The master instance starts from a git worktree
at `~/.cache/n8n-feature-test/master`, at the merge base of `HEAD` and
`origin/master`. Thus the visual check shows only the changes of this
branch, not the changes that master got after the branch started.
- A master visual run that passed is kept. The next run with the same merge
base, the same script and the same flags uses it again, and does not start
master.
## Requirements
Run `python3 .agents/skills/testmax-assistant-ui/scripts/run.py --check`
before the first run. The commands below use `$S` for this scripts folder.
`--check` checks the tools,
Chrome, the settings, the disk space and the master build, and prints the
plan folder for this branch.
- Google Chrome. The scripts use the Playwright package of the repository.
- The Instance AI model settings in `.env.local` or `.env.ai-surfaces`, as for
your own instance. Fresh instances get the same settings, without the
license, the database, the port and the encryption key.
- `LANGSMITH_API_KEY` and LangSmith tracing, for the tokens and cost. Without
them, the report shows no tokens or cost. Do not set
`N8N_DIAGNOSTICS_ENABLED=false`: then Instance AI sends no traces.
- About 3 GB of disk space for the master worktree and its build.
Never put a key on the command line. Read settings from the env files and the
environment only.
## 1. Understand the feature
Find what changed and what the user wants to test:
```bash
git diff origin/master...HEAD --stat -- packages/frontend packages/@n8n/instance-ai packages/cli/src/modules/instance-ai
git diff origin/master...HEAD --
```
Read the changed Vue components, the stores, and the backend events that they
use. Find the `data-test-id` values of the new elements. If the user did not
describe the feature, ask for one or two sentences about it.
If the plan has visual scenarios, start the master baseline now, in the
background. The first build takes 15 to 20 minutes. Later builds take less
than a minute when the turbo cache has the files. You can draft the plan while
it builds:
```bash
S=.agents/skills/testmax-assistant-ui/scripts
python3 $S/run.py --check # the setup, and the plan folder
python3 $S/baseline.py prepare # in the background
python3 $S/baseline.py status # later: is it ready?
```
## 2. Draft the test paths
Write the plan folder that `--check` prints:
`~/.cache/n8n-feature-test/plans///`. The folder stays after
a reboot. It holds `scenarios.json` and one script for each scenario. The
folder name is the name of the run folders.
Draft these paths:
- **Feature paths.** The main path, then each branch of it: other user
choices, empty and long inputs, cancel and stop, a reload in the middle, a
second thread, errors that the user can see. One path for each scenario.
- **Regression paths.** The existing flows near the change: a plain chat
reply, approvals, the plan review, the preview panel, the thread list. The
library has `smoke-chat` for the plain chat reply.
- **Visual screens.** The screens that the change can affect, on the branch
and on master. Capture each screen in a stable state. Mask all text that
comes from the LLM, and all data that changes between runs. The library has
`visual-assistant-home` and `visual-thread-reply`.
Keep each scenario short and focused. A scenario that fails must show where it
failed. Use more scenarios, not longer ones. A visual scenario must not need
the new feature, because it also runs on master.
Some UI states show only for a moment, for example a status line that folds
away when the run ends. Check them while the run is active, not after it.
If the feature is behind a feature flag or an experiment, set the flag in
`featureFlags`. Without it, the instances use the default value of each flag.
Before you show the plan to the user, check the new scripts and get the
estimate (see [Check the scripts](#check-the-scripts)). Then show the user the
list of paths: the id, the kind, the title and the expected result of each.
Also show the estimate: the number of LLM runs, the cost and the time. Change
the plan until the user approves it.
### scenarios.json
```json
{
"title": "Stop button in the Assistant",
"feature": "The user can stop a run while the Assistant works.",
"scenarios": [
{
"id": "stop-mid-run",
"title": "Stop a run in the middle",
"kind": "feature",
"script": "stop-mid-run.mjs",
"expected": "The stop button shows during the run. A click ends the run without an error message.",
"timeoutMin": 6
},
{
"id": "smoke-chat",
"title": "A prompt gets a reply",
"kind": "regression",
"script": "lib:smoke-chat.mjs",
"expected": "A short prompt gets the reply OK."
}
]
}
```
The plan has these fields: `title` (required), `feature` (the feature, for the
judge), and `featureFlags` (optional). `featureFlags` maps a flag name to
`true`, `false` or a variant name, for example
`{ "": "variant" }`. Each instance gets these flags, on the
branch and on master. They go into `N8N_FEATURE_FLAG_OVERRIDES` and replace
the values in your env files.
Each scenario has these fields:
| Field | Required | Description |
|---|---|---|
| `id` | yes | Lowercase letters, digits and `-`. No `--`. Unique. |
| `title` | yes | A short name for the report. |
| `kind` | yes | `feature`, `regression` or `visual`. |
| `script` | yes | A path relative to `scenarios.json`, or `lib:` for a library script. |
| `expected` | yes | The expected result, for the judge. |
| `autoConfirm` | no | Answer the Assistant's confirmations automatically. Default `true`. Credential and setup cards get "Later". Set `false` when the script answers a confirmation or a card. |
| `isolated` | no | Run the scenario on its own fresh instance. Default `true`. Instances start ahead of their scenario, so this costs almost no time. Set `false` to run the scenario on one branch instance that it shares with the other `false` scenarios. Visual scenarios ignore this field. |
| `timeoutMin` | no | The time limit of the scenario. Default 10. |
| `viewport` | no | The window size, for example `1600x1000` (the default). |
`examples/stop-button/` is a full example plan.
### Scenario scripts
A script exports one async function. It gets the kit `t`:
```js
export default async function (t) {
await t.step('Open the Assistant', () => t.goto());
await t.step('Send a prompt', () => t.send('Reply with exactly the word OK and nothing else.'));
await t.step('Wait until the run ends', () => t.waitForIdle({ timeout: 180_000 }));
await t.check('The reply says OK', () => t.expect(t.ai.assistantMessages().last()).toContainText('OK'));
await t.capture('thread', { mask: [t.ai.assistantMessages()] });
}
```
| Kit member | What it does |
|---|---|
| `t.step(title, fn)` | Runs an action. Records a screenshot and the time. A failure stops the scenario. |
| `t.check(title, fn)` | Runs an assertion, with a screenshot. A failure is recorded, and the scenario continues. |
| `t.note(title)` | Adds a step with a screenshot and no action. |
| `t.goto(path = '/assistant')` | Opens a page and waits for the chat input. Closes the "Try new n8n Assistant" dialog. |
| `t.send(text)` | Types into the chat input, sends, and waits for the thread URL. Records the thread for the trace. |
| `t.waitForIdle({timeout, settle})` | Waits until the thread has no active run. Returns `'idle'`, or `'suspended'` when the run waits for the user. |
| `t.capture(name, {mask, maskText, root, fullPage})` | Saves a screen and its accessibility tree for the visual check. `mask` takes locators, and `maskText` takes texts: all elements with that text. Masked areas show as one color. In the tree, the subtree of a `mask` element and each masked text show as `[masked]`. |
| `t.api(method, path, body)` | Calls the n8n REST API as the signed-in user. Returns `data`. |
| `t.threadStatus(threadId)` | Returns `{active, suspended, background}`: the thread has a run, the run waits for the user, and the number of running background tasks. |
| `t.ai.*` | Locators for the Assistant: `chatInput`, `sendButton`, `stopButton`, `userMessages`, `assistantMessages`, `assistantText(text)`, `panelText(text)`, `sidebarToggle`, `threadList`, `newThreadButton`, `approveButton`, `denyButton`, `planApproveButton`, `previewPanel`, `previewCanvasNodes`, and more in `scripts/kit.mjs`. |
| `t.expect`, `t.page`, `t.baseUrl`, `t.sleep(ms)` | Playwright `expect`, the page, the instance URL, and a pause. |
Rules for scripts:
- Use `data-test-id` locators (`page.getByTestId`). Use text only for text that
the test controls.
- Use `t.expect(...).toBeVisible()` and other assertions that wait. Do not use
`t.sleep` to wait for the UI.
- LLM answers change from run to run. Check the UI state, not the exact words.
Ask for a fixed reply, such as "Reply with exactly the word OK", when you
need a known answer.
- To test an approval, set `"autoConfirm": false`, wait with
`t.waitForIdle()` until it returns `'suspended'`, then click the button.
### Check the scripts
Check the new scripts on fresh instances before you show the plan:
```bash
python3 $S/run.py --plan /scenarios.json --try --only id1,id2
```
`--try` does not run master, does not start the live page, and builds no
report. It prints a table with the status, the steps and the first error of
each run. The runs are in `/tmp/feature-test/try/`. Fix the scripts that fail
because of the script, then try them again. Each LLM scenario in a try costs
money too, so try only the new scripts.
Then get the estimate for the full run:
```bash
python3 $S/run.py --plan /scenarios.json --estimate
```
The estimate uses the cost and time of earlier runs of the same scenario ids.
A scenario without an earlier run shows `*` and the median of all earlier
runs. A master run that is kept shows `(kept)` and costs nothing.
## 3. Run
Open the live page for the user on each run. Do not skip this step. Do this:
1. Find out if your harness has a browser tool that opens a tab in the
editor. In Cursor, this is `browser_navigate` of the `cursor-ide-browser`
MCP server.
2. If it has one, start the run in the background with `--no-open`:
```bash
python3 $S/run.py --plan /scenarios.json --no-open
```
Wait for the `live page: ` line in the output, which comes in a few
seconds. Then open that URL in a new editor tab that the user can see (in
Cursor: `newTab: true` and `position: "active"`). If the tab does not open,
run `open ` (macOS) or `xdg-open ` (Linux).
3. If it has none, start the run in the background without `--no-open`.
`run.py` opens the live page in the default browser:
```bash
python3 $S/run.py --plan /scenarios.json
```
4. Give the user the URL in both cases.
`run.py` does not read `$BROWSER`, so do not set it.
`run.py` does these steps:
1. Prints the estimate and checks the setup. It stops when the setup has an
error, for example a missing Chrome.
2. Copies the plan into a new run folder,
`/tmp/feature-test/