--- 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/