> ## Documentation Index
> Fetch the complete documentation index at: https://docs.avocadostudio.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# QA gate: avocado qa

> One command, run last, that checks an integration the way the editor will use it — in a browser, in a frame on the editor's origin, with edited content — and exits non-zero until it is right.

An integration can type-check, build, answer `curl` on every editor route, and still break the moment a person opens it in the Studio. On the two sites this command was designed from, every problem found after the coding agent reported "done" lived somewhere those checks never go:

* a CMS live-preview SDK that threw a full-page error as soon as anything but its own app framed the page;
* template security headers that refused the editor frame;
* a blog post that answered 404 in the preview once its draft held a block type the template no longer rendered;
* bullets a CSS reset had removed, invisible until real content arrived with a list;
* a whole blog post mapped to one block, so clicking anywhere selected everything;
* an image component that crashed on any URL not from its own CDN.

`avocado qa` exercises exactly those paths. It is the last step of the `avocado-integrate` skill, and an agent should not report an integration finished until it exits 0.

```bash theme={null}
# from the site's directory, with its dev server running
npx avocado qa
```

It exits 0 when no check failed, 1 when one did, and 2 for a usage error, so an agent can gate on the exit code alone.

`avocado` is a bin of `@avocadostudio-ai/site-sdk`, so `npx avocado qa` finds it wherever the SDK is installed. The unscoped name `avocado` on npm belongs to an unrelated package, so where the SDK is only a transitive dependency (an Astro site under pnpm, for example) use the scoped form, which always resolves to this command:

```bash theme={null}
npx @avocadostudio-ai/site-sdk qa
```

## What it does

It runs against the running site and the orchestrator the site reads drafts from. Everything it renders comes from a **throwaway session** named `qa-<timestamp>`, seeded with the site's own pages. Your session is never read from or written to, and the throwaway sessions are removed at the end (library mode has no delete route; there they stay in the database, harmlessly).

```mermaid theme={null}
flowchart LR
    P["<b>0 · Preflight</b><br/>framework, Node, package manager,<br/>disk, frame headers, known blockers"]
    S["<b>1 · Static</b><br/>foreign peers, editor script<br/>on public pages, --build"]
    C["<b>2 · Content contract</b><br/>manifest, block types, panel,<br/>manifest.lock; seeds qa-&lt;ts&gt;"]
    R["<b>3 · Editor render</b><br/>every page in a frame on the<br/>editor origin, then fixtures"]
    I["<b>Invasiveness</b><br/>what the integration changed"]
    M["<b>5 · Manual pass</b><br/>checklist"]
    P --> S --> C --> R --> I --> M

    style R fill:#7ED957,stroke:#14532D,color:#0a0a0a
```

Every check reports `pass`, `warn`, `fail`, `skip` or `info`. Any `fail` makes the command exit 1. Warnings do not, but each carries a fix, and an agent should read them.

### 0 · Preflight

| Check | Fails when |
| - | - |
| Framework and version | Next is below 15, has no `app/` directory, or Astro is below 5 |
| Node | Below the framework's minimum (warns against `engines.node`) |
| Package manager | *Warns* on two lockfiles, a `packageManager` pin that disagrees with the lockfile, or a parent `package.json` pinning a different manager — corepack then refuses every call |
| Free disk | Less than the remaining install plus build headroom (`df`) |
| Known blockers | *Warns* on `ContentfulLivePreviewProvider` without `targetOrigin`, Sanity visual editing, the Storyblok bridge, Prismic and Payload previews, existing `draftMode()` use, an Astro `ClientRouter`, and a CSS reset that strips list markers with nothing restoring them |
| Draft secret | *Warns* when no `DRAFT_MODE_SECRET` is found |
| Site and orchestrator | Either does not answer, or the orchestrator refuses the token |
| Registration | *Warns* when the orchestrator's registry has no site with this id, or its registered preview URL is not the site being checked — the editor frames the registered URL |
| Frame headers | The editor render's `frame-ancestors` (or, without one, `X-Frame-Options`) does not admit the editor origin |

### 1 · Static

| Check | Fails when |
| - | - |
| Foreign peers | *Warns* when `next`, `@next/*` or (without React islands) `react` are installed in an Astro site, with their size |
| Public-page script | *Warns* when a public page's scripts carry the preview bridge above the budget (`--js-budget`, 10 KB gzipped). Measured against a dev server the sizes are unminified; the presence is what counts |
| Typecheck and build | Only with `--build`: the site's own `typecheck` and `build` scripts |

### 2 · Content contract

| Check | Fails when |
| - | - |
| Manifest | `/api/editor/blocks` does not validate, declares a type twice, or has field metadata that does not resolve |
| Pages | `/api/editor/pages` has no pages, duplicate slugs, or duplicate block ids within a page |
| Block types | A page holds a type the manifest does not list. Chrome built-ins (a footer) are fine; other built-ins the site omits are a warning |
| Props vs schema | *Warns* when stored props do not match `propsSchema` — the first edit to that block would be refused |
| Panel coverage | *Warns* on every [`panelCoverage`](/integration/coverage) finding |
| Draft endpoint | *Warns* when `/api/editor/draft` answers a bad secret with a 500 |
| `manifest.lock` | *Warns* when block types or fields were removed or changed kind since the last run (see below) |

### 3 · Editor render

Each page is framed from the editor's own origin — the host page is served by intercepting a path on that origin, so the site's real `frame-ancestors` and postMessage target are what gets tested, and it works while the editor itself is running.

| Check | Fails when |
| - | - |
| Load | A page is not HTTP 200 in the frame, or the browser refuses to show it |
| Errors | An uncaught error or rejection in the frame. Known ones (Contentful's "current origin is not supported", a refused frame) come with their fix; console errors are a warning |
| Bridge | *Warns* when no page posts a `site-editor/v1` message to the editor origin — the bridge is not mounted, or the site trusts a different editor origin |
| Block ids | A `data-block-id` on the page is not a block in the draft (the editor's "block is not in the page the orchestrator returned"). Draft blocks with no wrapper are a warning |
| Editable coverage | [`editableCoverage`](/integration/coverage): a field with no marker, or an image marked on a void element |
| Granularity | *Warns* when one block's fields sit in two or more stacked, section-sized regions, or more than four fields of three kinds spread over more than a screen |
| Draft source | A text field set to a unique value in the throwaway session does not appear — the site reads drafts from somewhere else |
| Rich text | One rich-text field per block type, rendered with a fixture holding every node type, shows lists without markers or headings that look like body text. Unstyled links are a warning |
| Image sources | A page breaks when an image field holds a relative path, a stock-photo host, the orchestrator's `/generated-images` URL, or an inline SVG placeholder |
| Preview drift | *Warns* when a split preview route (a middleware rewrite to `/preview-draft`) renders different text, headings or images from the public route, with no draft applied |

The fixtures — rich text, the draft probe, image sources — each go into a throwaway session of their own (`qa-<ts>-fx<n>`), so the plain pass measures your real content.

Without Playwright the stage still runs on the server HTML: load, block ids, coverage, the draft probe, image sources and drift are all in the markup. Errors, granularity and rich-text styles need a browser and are reported as skipped. Playwright is looked for in the project, next to the SDK and in a pnpm workspace; it is not a dependency of the SDK.

```bash theme={null}
npm i -D playwright && npx playwright install chromium
```

### Invasiveness (informational)

A git diff against `--base`, or the merge-base with the default branch, including uncommitted and untracked files — an agent's integration usually is not committed when this runs. Installed skills (`.claude`, `.agents`, `AGENTS.md`, `CLAUDE.md`) and lockfiles are excluded. It reports new and changed files and lines, files outside the integration's own folders, public route files that changed, and — as a warning — library modules that something imported at the base and nothing imports now, the data layer an integration left behind.

### 5 · Manual pass

Printed at the end: ten minutes in the Studio once the gate is green.

* Clicking each visible section selects a block with a sensible name and only its fields
* Fields that cannot be written (asset alt text, slugs, dates) are not offered as editable
* A chat edit and a panel edit both update the preview within a few seconds
* Rich text in the panel looks like rich text on the page (lists, headings, links)
* Asking for a language the site does not have produces a question, not an overwrite
* Publishing one page changes only that page in the CMS, and the public site shows it after reload

## Not yet automated

Printed on every run, so a green summary is not read as covering them:

* the CMS space holds every content type the site queries (needs CMS credentials);
* lint;
* a round trip that is a no-op for every page × locale, a synthetic edit per field kind, and a publish dry run — all three need the site's lens, which the SDK has no endpoint to run;
* rich text in the Studio's property panel (the panel is the editor's, not the site's);
* **session compatibility**: seeding a session from the previous manifest and loading it against the new one. The snapshot half is done — see `manifest.lock` below.

## Files

| File | |
| - | - |
| `.avocado/qa-report.json` | Every check with its evidence. Do not commit it |
| `.avocado/manifest.lock` | The shape of the block manifest at the last run — type → field → kind. **Commit it.** A draft session outlives the code that produced it, so renaming or splitting a block type leaves every draft still holding the old one stale: the preview cannot render it and the editor reports the block as missing. The next run warns, and the diff shows up in review |

## Options

| Flag | Default |
| - | - |
| `--site <url>` | The port in `package.json`'s `dev` script, else `:3000` (Next) or `:4321` (Astro) |
| `--orchestrator <url>` | `ORCHESTRATOR_URL` from the env files, else `http://localhost:4200`. In library mode, `http://localhost:<port>/api/avocado` |
| `--editor-origin <url>` | `NEXT_PUBLIC_EDITOR_ORIGIN` / `AVOCADO_EDITOR_ORIGINS`, else `http://localhost:4100` |
| `--site-id <id>` | `NEXT_PUBLIC_DEFAULT_SITE_ID` / `AVOCADO_SITE_ID`, else the package name |
| `--secret`, `--token` | Read from the env files / `ORCHESTRATOR_ACCESS_TOKEN` |
| `--skip <stages>` | `0`–`5`, or `preflight,static,contract,render,invasiveness` |
| `--build` | Also run the site's `typecheck` and `build` scripts |
| `--max-pages <n>` | Every page |
| `--js-budget <kb>` | `10` — gzipped editor code allowed on a public page |
| `--timeout <ms>` | `20000` per request |
| `--cwd <dir>` | The current directory |
| `--base <ref>` | Merge-base with the default branch |
| `--no-browser`, `--headed`, `--playwright <path>` | Browser use for stage 3 |
| `--json` | Print the JSON report instead of the summary |
| `--report <path>` | `.avocado/qa-report.json` |

The env files are read the way the dev server reads them — `.env`, `.env.development`, `.env.local`, `.env.development.local` — so the command checks the orchestrator, secret and editor origin the site actually uses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.