# Build and test

> Source-first vs release builds, browser and extension bundles, default and opt-in test suites (live-e2e, skill-behavior, plugin-e2e), and suite trigger mapping.

- Repository: pbakaus/impeccable
- GitHub: https://github.com/pbakaus/impeccable
- Human docs: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4
- Complete Markdown: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4/llms-full.txt

## Source Files

- `package.json`
- `scripts/build.js`
- `scripts/run-tests.mjs`
- `scripts/test-suites.mjs`
- `docs/DEVELOP.md`
- `tests/build.test.js`
- `tests/skill-behavior/README.md`

---

---
title: "Build and test"
description: "Source-first vs release builds, browser and extension bundles, default and opt-in test suites (live-e2e, skill-behavior, plugin-e2e), and suite trigger mapping."
---

The repository builds design skills from `skill/` through `scripts/build.js`, regenerates detector and extension bundles with separate Node scripts, and runs tests through `scripts/run-tests.mjs` against named suites defined in `scripts/test-suites.mjs`. Default development uses a **source-first** build that validates `dist/` without rewriting tracked harness folders; a **release** build additionally syncs root provider skills and the committed `plugin/` subtree.

## Prerequisites

| Requirement | Notes |
|---|---|
| **Bun** | Build orchestration and Bun-runner unit tests |
| **Node.js `>=22.18.0`** | `package.json` engines; CI matrix uses `22.18.0` and `24` |
| **Git** | CI change detection and plugin E2E isolation |
| **Optional: Playwright Chromium** | Live E2E suites (`npx playwright install chromium`) |
| **Optional: Puppeteer Chrome** | Detector browser tests (`bunx puppeteer browsers install chrome`) |
| **Optional: `claude` CLI** | Plugin loader E2E; suite skips when missing |
| **Optional: provider API keys in `.env`** | Skill-behavior and billed opt-in E2E |

## Build commands

| Script | What it does |
|---|---|
| `bun run build` | Source-first skills build (`scripts/build.js --skip-root-sync`), then copies `dist/` → `build/_data/dist` |
| `bun run build:release` | Full skills build **with** root harness + `plugin/` sync, then copies `dist/` → `build/_data/dist` |
| `bun run build:skills` | Skills only, skip root sync |
| `bun run build:skills:release` | Skills only, with root sync |
| `bun run build:browser` | Browser detector IIFE → `cli/engine/detect-antipatterns-browser.js` |
| `bun run build:extension` | Extension detector + `antipatterns.json` + zips under `dist/` |
| `bun run clean` | Remove `dist` and `build` |
| `bun run rebuild` / `rebuild:release` | Clean then source-first or release build |

### Source-first vs release

```text
skill/ + scripts/lib/transformers/*
        │
        ▼
 scripts/build.js
        │
        ├─ always: dist/<provider>/…, dist/universal/, ZIPs, OpenAI plugin stage
        │          gates: counts, plugin versions, manifest shape, prose
        │
        ├─ --skip-root-sync (default for `bun run build`)
        │     no rewrite of .claude/skills, .cursor/skills, …, or plugin/
        │
        └─ root sync (`bun run build:release`)
              mirror skills into provider harness dirs (except .codex)
              sync hook manifests, agents where configured
              rebuild committed plugin/ for marketplace installs
```

- **Source-first (`bun run build`)** is the normal contributor path. Edit `skill/`, `scripts/`, `cli/`, `extension/`, `tests/`. Validate under `dist/` without staging harness churn.
- **Release (`bun run build:release`)** rewrites tracked harness directories (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, …) and `plugin/`. Use only when intentionally refreshing installable artifacts (releases, build-system work, or the `sync-generated-output` workflow on `main`).
- Codex provider output stays under `dist/`; it is **not** synced to a root `.codex/` tree.
- After feature work lands on `main`, `.github/workflows/sync-generated-output.yml` runs `bun run build:release` and commits generated harness output separately.

### What the skills build produces

| Output | Role |
|---|---|
| `dist/<provider>/<configDir>/skills/…` | Per-provider transformed skills |
| `dist/universal/` | All provider config dirs assembled for install ZIPs |
| `dist/*.zip` | Individual + universal bundles (`createAllZips`) |
| `dist/openai/` / OpenAI plugin zip | Staged via `stageOpenAIPlugin` on every build |
| Root harness dirs + `plugin/` | Only when root sync is enabled |
| `build/_data/dist` | Copy of `dist` for Cloudflare Pages Functions consumers |

Provider transforms come from `createTransformer` + `PROVIDERS` in `scripts/lib/transformers/`. Placeholders such as `{{model}}`, `{{config_file}}`, `{{command_prefix}}`, and `{{available_commands}}` are substituted per provider from `skill/SKILL.src.md`.

### Build gates (hard fail)

`scripts/build.js` exits non-zero when any of these fail:

| Gate | Checks |
|---|---|
| `validateSkillFrontmatter` | Description length ≤ 1024 |
| `generateCounts` | Command count from skill router table and detector rule count match README / AGENTS / plugin manifests (and site index when present) |
| `validatePluginVersions` | `.claude-plugin/plugin.json`, marketplace, `plugin/` manifest, and bundled skill frontmatter versions agree |
| `validatePluginManifestShape` | Known Claude Code loader contract (`scripts/lib/validate-plugin-manifest.js`; no silent `agents` key) |
| `validateProse` | README surfaces: em dashes, `--` substitute, denylisted marketing phrases (`docs/STYLE.md`) |
| `validateSkillProse` | `skill/**/*.md`: em dashes plus a tighter phrase denylist (technical words like `seamless` allowed in skill text) |

### Browser and extension bundles

After changing detector rules under `cli/engine/`:

```bash
bun run build:browser
bun run build:extension
```

| Command | Generated artifacts |
|---|---|
| `build:browser` | `cli/engine/detect-antipatterns-browser.js` (IIFE around browser-safe modules); optionally mirrors to `site/public/js/…` if that tree exists |
| `build:extension` | `extension/detector/detect.js`, `extension/detector/antipatterns.json`, `dist/extension.zip`, Firefox packaging under `dist/` |

CI rebuilds these when the detector suite is selected, then asserts no unexpected diff in tracked outputs:

```bash
git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin \
  cli/engine/detect-antipatterns-browser.js extension/detector
```

Rule changes also need fixture tests under `tests/fixtures/antipatterns/` and both browser and jsdom adapter wiring. See [Contributing](/contributing) and [Detector rules](/detector-rules).

## Test runner

Entry points:

```bash
bun run test                          # default suites
node scripts/run-tests.mjs --list     # suite inventory
node scripts/run-tests.mjs --help
node scripts/run-tests.mjs core live  # named suites
node scripts/run-tests.mjs all        # default + all opt-in
node scripts/run-tests.mjs all-local  # same as default
```

`scripts/run-tests.mjs` expands suite aliases via `expandSuites()` and runs each suite’s command list:

| Runner | Invocation |
|---|---|
| `bun` | `bun test <files…>` |
| `node` | `node --test --test-concurrency=N` (default concurrency **4**), optional `--test-timeout` / `--test-force-exit` |

Some commands inject env (for example `IMPECCABLE_CLI_REMOTE_E2E=1` for remote CLI E2E).

## Default suites

`DEFAULT_SUITES` = `core`, `detector`, `live`, `framework`, `plugin-e2e`.

These run for `bun run test` and the `default` / `all-local` aliases.

| Suite | npm script | Description | Special needs |
|---|---|---|---|
| `core` | `test:core` | Build, transformers, CLI helpers, context, hooks, pin, doctor, release, storage unit tests (Bun + Node file lists) | — |
| `detector` | `test:detector` | Text, jsdom fixtures, Puppeteer browser paths, extension build | Puppeteer Chrome (`needsPuppeteer`) |
| `live` | `test:live` | Fast live-mode unit + local-server tests (not full browser fixture sweeps) | — |
| `framework` | `test:framework` | Static framework-fixture coverage (inject, CSP, wrap, generated-file detection) | — |
| `plugin-e2e` | `test:plugin-e2e` | Install committed `./plugin` into sandboxed Claude Code; assert skills, agents, hooks via `claude plugin details` | `claude` on PATH (else skip); 300s timeout |

**Plugin E2E contract:** unit guards in `validate-plugin-manifest` pin known loader keys; the real-CLI suite catches loader surprises (for example a declared `agents` key that loaded zero agents). Isolation sets `CLAUDE_CONFIG_DIR` / `HOME` into a temp dir so the developer’s real config is never touched.

## Opt-in suites

`OPT_IN_SUITES` (not in the default local `bun run test` sweep unless you pass `all` or name them):

| Suite | npm script | Description | Cost / deps |
|---|---|---|---|
| `cli-remote-e2e` | `test:cli-remote-e2e` | Install/update smoke against impeccable.style | Network |
| `live-e2e` | `test:live-e2e` | Playwright full live cycle across framework fixtures with a deterministic fake agent | ~2 min; real `npm install` per fixture; Playwright Chromium |
| `live-e2e-accept-cleanup` | `test:live-e2e-accept-cleanup` | Provider-backed post-accept cleanup regression | Bills a provider key; Playwright |
| `new-work-e2e` | `test:new-work-e2e` | Playwright offline smoke for concept/serve-question decision UI + fake image generator | Playwright; no API cost |
| `skill-behavior` | `test:skill-behavior` | LLM-backed Setup / routing scenarios against real models | ~5 min; keys in `.env`; bills providers |
| `live-svelte-adapter-deepseek` | `test:live-svelte-adapter-deepseek` | DeepSeek-backed Svelte adapter browser sweep | DeepSeek + Playwright; long timeout |

Additional named suites (not default, not in `OPT_IN_SUITES` expand list for `all` unless listed): `cli-e2e` (local universal-bundle CLI tests), `live-e2e-agent` (insert-mode fake-agent helpers).

### Suite trigger mapping (what change owes which suite)

Canonical triggers live in `scripts/test-suites.mjs` (`triggers` regexes). Every suite also self-triggers when one of its own test files changes.

| Area changed | Suite(s) to run |
|---|---|
| `scripts/` (except detector/extension builders), `skill/SKILL.src.md`, skill scripts/refs, `cli/bin/`, READMEs | `core` |
| `cli/engine/`, `extension/`, detector fixtures, `build-browser-detector` / `build-extension` | `detector` (+ rebuild browser/extension) |
| `skill/scripts/live*`, `skill/reference/live.md`, `tests/live-*` | `live` |
| `tests/framework-fixtures/**`, inject/wrap/CSP helpers | `framework` |
| `plugin/`, `skill/agents/`, `scripts/build.js`, plugin manifest validator | `plugin-e2e` |
| `skill/scripts/live/**`, runtime fixtures, `tests/live-e2e*` | `live-e2e` |
| `live-accept` / `live-browser` / `live-server` / `live-wrap` / SvelteKit adapter | also `live-e2e-accept-cleanup` when verifying accept cleanup |
| `live/sveltekit-adapter.mjs`, `live/svelte-component.mjs` | `live-svelte-adapter-deepseek` |
| `SKILL.src.md` Setup, `context.mjs`, Setup-adjacent refs | `skill-behavior` |
| `serve-question.mjs`, `generate-image.mjs`, `concept-seed.mjs` | `new-work-e2e` |
| `cli/bin/commands/skills.mjs` | `cli-remote-e2e` |

Common infra paths (`package.json`, lockfile, `run-tests.mjs`, `test-suites.mjs`, CI workflow) match every suite that lists them.

### Live E2E controls

```bash
bun run test:live-e2e
IMPECCABLE_E2E_ONLY=vite8-react-modal bun run test:live-e2e
IMPECCABLE_E2E_DEBUG=1 bun run test:live-e2e
IMPECCABLE_E2E_AGENT=llm bun run test:live-e2e   # optional real-model agent; costs money
```

| Env var | Role |
|---|---|
| `IMPECCABLE_E2E_ONLY` | Comma-separated fixture names |
| `IMPECCABLE_E2E_SCENARIOS` | Scenario filter (CI smoke uses `core`) |
| `IMPECCABLE_E2E_DEBUG` | Dump page DOM + dev-server tail on failure |
| `IMPECCABLE_E2E_AGENT` | `fake` (default) or `llm` |
| `IMPECCABLE_E2E_LLM_PROVIDER` / `IMPECCABLE_E2E_LLM_MODEL` | LLM agent selection |
| `IMPECCABLE_E2E_TEST_TIMEOUT_MS` | Per-test timeout override |
| `IMPECCABLE_E2E_ARTIFACT_DIR` | Failure artifact root |

Fixtures with a `runtime` block under `tests/framework-fixtures/` install deps, boot a real framework server, and drive Playwright. Schema notes: `tests/framework-fixtures/README.md`.

### Skill-behavior controls

```bash
bun run test:skill-behavior
IMPECCABLE_SKILL_BEHAVIOR_MODELS=claude-sonnet-5 bun run test:skill-behavior
IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1 bun run test:skill-behavior
```

Default models (`tests/skill-behavior/providers.mjs`): `claude-sonnet-5`, `gpt-5.6-luna`, `gemini-3.5-flash`, `deepseek-v4-flash`. Missing keys skip that provider cleanly. Assertions key on the tool-call **trace** (context load, reference files, writes), not free-form prose. Scenario baseline tables: `tests/skill-behavior/README.md`.

## CI selection

`.github/workflows/ci.yml` + `scripts/ci-test-plan.mjs`:

| Event | Plan behavior |
|---|---|
| **push / workflow_dispatch** | Force deterministic suites; opt-ins mainly via dispatch or path triggers |
| **pull_request** | Path-triggered suites via `matchesSuiteTriggers` against the PR diff |
| **schedule (nightly)** | Full `live_e2e` matrix; deliberately **does not** enable billed opt-ins (`skill_behavior`, accept-cleanup, deepseek) |

Local without `GITHUB_EVENT_NAME` / `CI_CHANGED_FILES`: plan treats as “no changes” and forces deterministic suite selection for summary purposes.

CI always runs `test:core` and `bun run build` on the Node matrix. Detector / live / framework jobs are conditional on the plan. Detector path also runs `build:browser`, `build:extension`, and `web-ext` lint for the Firefox package. Opt-in jobs include remote CLI E2E, live-e2e smoke groups (fixture subsets), full live-e2e on schedule/dispatch, skill-behavior, accept-cleanup, and DeepSeek Svelte adapter when selected.

## Contributor workflow

<Steps>
  <Step title="Edit source, not harness output">
    Change `skill/`, `scripts/`, `cli/`, `extension/`, or `tests/`. Do not hand-edit generated provider trees under `.claude/skills/` (and siblings) unless the task is a release or build-system sync.
  </Step>
  <Step title="Validate with source-first build">
    Run `bun run build` after skill, transformer, or count-affecting edits. Fix prose, version, and count failures before iterating on tests.
  </Step>
  <Step title="Rebuild detector surfaces when rules change">
    Run `bun run build:browser` and `bun run build:extension`, then `bun run test:detector`.
  </Step>
  <Step title="Run the default suite">
    Run `bun run test` (or scoped `test:core` / `test:live` / …). Include `test:plugin-e2e` when touching `plugin/` or the build’s plugin assembly (already part of default).
  </Step>
  <Step title="Run owed opt-in suites">
    Match the trigger table: live script changes → `test:live-e2e`; Setup/`context.mjs` → `test:skill-behavior`; skills CLI remote path → `test:cli-remote-e2e`.
  </Step>
  <Step title="Release builds only when intentional">
    Use `bun run build:release` when you mean to refresh tracked harness + `plugin/` output. Keep that churn out of ordinary feature PRs.
  </Step>
</Steps>

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Build fails on “stale count” | Router table or `ANTIPATTERNS` count drifted from README / plugin descriptions | Update user-facing counts to match source, or fix the source count claim |
| Build fails on plugin version | Marketplace / `plugin/` / skill frontmatter disagree with `.claude-plugin/plugin.json` | Align versions; run `bun run build:release` when regenerating `plugin/` |
| Build fails on prose / skill prose | Em dash or denylisted phrase | Edit copy per `docs/STYLE.md`; do not bypass the regex |
| Detector tests hang under Bun | jsdom path is slow under Bun | Prefer `node` via `run-tests.mjs` / documented `node --test` paths for fixture suites |
| Live E2E missing browser | Playwright Chromium not installed | `npx playwright install chromium` |
| Plugin E2E skipped | No `claude` CLI | Install Claude Code CLI or accept skip for local runs |
| Skill-behavior all skipped | No provider keys in repo-root `.env` | Set at least one of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_CLOUD_API_KEY`, `DEEPSEEK_API_KEY` |
| Unexpected harness diff in CI | Source-first build still regenerated tracked browser/extension outputs, or local dirty harness | Rebuild the surface that owns the file; only stage harness diffs for intentional sync |

## Related pages

<CardGroup>
  <Card title="Contributing" href="/contributing">
    Source-of-truth layout, generated harness policy, anti-pattern TDD order, and component release tags.
  </Card>
  <Card title="Provider harnesses" href="/provider-harnesses">
    Supported harness directories, install aliases, and capability matrix for generated skills.
  </Card>
  <Card title="Plugin agents" href="/plugin-agents">
    Bundled agents, plugin manifest contract, and hooks packaged for Claude Code / Grok installs.
  </Card>
  <Card title="Live framework recipes" href="/live-framework-recipes">
    Fixture layouts and live config shapes exercised by framework and live-e2e suites.
  </Card>
  <Card title="Detector rules" href="/detector-rules">
    Rule registry, engines, and finding shape that browser and extension builds package.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    Detect and install CLI surfaces validated by core and CLI E2E suites.
  </Card>
</CardGroup>
