Suite generation and regeneration

The flawless-react, flawless-nextjs, flawless-api-fixtures and flawless-regen skills in full — what each one knows, the task description that triggers it, and the mistakes it exists to stop.

Four of the fourteen skills in .agents/skills/ cover the loop of writing a suite and running the tool over it again. flawless-react is the umbrella — what an agent loads when asked to put tests on a component with no other context. flawless-nextjs and flawless-api-fixtures are the two things that mode actually consists of: the derived provider wrapper, and where a fixture body comes from. flawless-regen is what an agent should load before touching a suite someone has already adopted. All are plain markdown with two frontmatter keys, name and description; the description is the trigger, and if the task matches, the whole body goes into context.

flawless-react

When Claude loads it

Its description covers generating and maintaining React/Next.js test suites, and names three situations: adding component, e2e or load tests to a React codebase; a generated flawless suite that is red; and “scaffold tests for this component” work. It advertises its contents — task selection, Next.js mode, provider wrappers, MSW fixtures from an OpenAPI spec, colocated specs, route e2e, crust load pipes, the programmatic API, findings — then names all thirteen siblings and opens with a table mapping a situation to the skill that answers it, so an agent that lands here on the wrong question is redirected.

What it knows

It opens with the premise the rest depends on: point the tool at a .tsx file or glob and it reads the component’s props, state, hooks, children and fetches, then writes a runnable suite — and most of what it emits is regenerated. Only what the tool cannot rebuild is protected: budgets.json, host/main.tsx, provider.fixtures.ts, e2e/global-setup.ts, e2e/params.json, mocks/handlers.ts, colocated specs. Its rule for a hand-hardened component spec is --colocate; specs written inside the out dir are rewritten.

It carries the seven-task table with each output and prerequisite: unit-browser (Vitest + Testing Library, plus Playwright browser specs when perf-harness is also selected), perf-harness (the host/ bundle, Profiler/react-scan suite, heap snapshots), perf-static (findings only, no files to run), a11y and react-rules (Biome in the target project), e2e (a running app and seeded data), load (crust). Under --nextjs [path] the tool detects the router, picks a provider, writes under <root>/flawless/, merges devDeps and flawless:* scripts into package.json, and appends gitignore entries — in every mode, not only under --nextjs.

It explains why a provider wrapper is derived at all: an app-router layout.tsx is usually an async server component importing next/font or next/headers, unusable under jsdom, so flawless builds flawless/provider.tsx from the JSX chain enclosing the children slot, keeping only "use client" components and literal attributes and turning what it drops into a TODO(flawless) comment. For catch-all APIs (Hono, tRPC) that static route walking cannot see, it points at --api openapi.json, with --swagger-url=off to skip the live spec probe. Those two summaries are pointers now: the wrapper’s rules belong to flawless-nextjs, the fixture’s to flawless-api-fixtures.

It then lists exactly what a generated unit spec asserts: the rendered root tag when the analyser could pin it down; children and className passthrough only when the value provably reaches the DOM, because accepting a prop is not rendering it; for a cva or tv component, that every option of every key renders a distinct class list and never a specific Tailwind token, because cn() is usually tailwind-merge; one test per interaction, with negative assertions on every other handler prop; success / loading / error / empty cases per useEffect fetch.

# one component, unit specs only
flawless-react src/components/Widget.tsx --tasks unit-browser --no-interactive

# a Next.js app: real fixtures, colocated specs, route e2e and load pipes
flawless-react "src/app/dashboard/page.tsx" --nextjs . --api openapi.json --swagger-url=off --tasks unit-browser,perf-static,a11y,react-rules,e2e,load --colocate --no-interactive

What it stops you getting wrong

A test.skip with a reason is the intended output. When nothing is derivable the generator emits a skip naming which preconditions failed, and the skill is explicit: do not “fix” it by deleting the assertion or hand-writing one. Fix the generator, or give the component the data-testid or fixture the skip asks for. Same for TODO(flawless) markers — a marker rather than a silently passing assertion. Run the suite, fill the TODOs, rerun.

The mock allowlist is short on purpose. Client specs mock next/navigation, next/headers, server-only, next/link, next/font/* and any "use server" module — nothing else. A broader mock breaks the suite while it still looks green: mocking a cn util strips every className off the markup, and mocking a UI component replaces the element the interaction tests click.

Load numbers are vacuous by default. LOAD_BASE must point at a production build, not next dev, because the dev server compiles routes on demand and the first request to each measures the compiler. And LOAD_AUTH is a header line — for a cookie-session app that is cookie: session=<value>, not a Bearer token. The failure you will not notice: an unauthenticated request to a dashboard route redirects to the login page, which answers 200, so every pipe passes while measuring the login form.

flawless-nextjs

When Claude loads it

A Next app’s generated suite is red at import or at mount, a provider is missing from the wrap, Cannot read properties of null (reading 'useState'), or pnpm run dev died after installing test tooling. It is the mode’s own manual: the app-router resolver, the derived wrapper, the auth stub, module stubs, the React pins, the package.json merge and environment loading.

What it knows

The wrapper is derived, not read. A layout.tsx is normally an async server component, so the tool walks the JSX that actually encloses {children} and keeps "use client" components with literal attributes. Every drop is written down where it will be read — a dropped attribute becomes a TODO(flawless) beside the wrapper, a dropped provider layer becomes a block at the top of the file with its reason and its fix — so the file means “the providers that could be mounted”, not “the app’s providers”.

The interesting drop is the deliberate one: a layer whose module directly imports an auth library is skipped rather than mounted, because the stub the unit and browser planes install for those same libraries is a signed-out snapshot — isSignedIn: false, user: null, status: "unauthenticated", method-shaped keys resolving to another signed snapshot. Mount a provider that gates its children on a session against that and it renders nothing, and every spec below inherits a blank screen. So the red spec an agent is staring at is usually one below the drop, and the fix is provider.fixtures.ts or --provider <file>, never an edit to provider.tsx — which --force erases.

The rest is the machinery around a mount: the module-stub ladder where a named stub outranks a pattern stub, why next/script gets an inert in-place stand-in and next/head deliberately does not, why the suite’s React is pinned to the app’s (detectAppReact) and why React 17 drags Testing Library back to ^12.1.5 — and repairs a stale 13+ range a previous run left behind. The package.json merge keeps your indentation and key order so generation produces a clean diff, and records the deps it adds in pnpm.ignoredBuiltDependencies, because pnpm 10 otherwise refuses the build-script decision and ERR_PNPM_IGNORED_BUILDS kills the app’s own dev server. Two .env orders are documented because there are two runtimes: the Vitest config’s, where a real environment variable beats every file and .env.example is last on purpose, and the host bundle’s, which follows Next’s development precedence.

# the app root is resolved, not assumed: apps/web, suite at apps/web/flawless
flawless-react "src/app/dashboard/page.tsx" --nextjs apps/web --tasks unit-browser,e2e

What it stops you getting wrong

Never hand-edit flawless/provider.tsx to add a provider, and never read a provider drop’s TODO(flawless) as settled while specs under that layer fail on a missing context — that is the same drop, still open. Never mock a project module to make a spec green: mocking a cn utility strips every className the assertions read, and mocking a UI primitive replaces the element an interaction test clicks. And never treat a shim as a fix: a library shim repairs the mount, not the component, so an API misuse the analyser could not see stays a red spec — correctly.

flawless-api-fixtures

When Claude loads it

Choosing between --api and --swagger-url, or a fixture body looks invented — an object of zeroes, a 1970 timestamp, an array with one item. It is the story of where a mock response comes from, told once for the specs, the MSW handlers and the mock server.

What it knows

A spec on disk (--api openapi.json) or a live one (--swagger-url, probed automatically in Next mode with a 1.5 s timeout, --swagger-url=off to skip it) beats static inference. The probe never throws: a non-2xx, a wrong content type or a dead port is reported and the run continues on inferred routes. Without any spec the tool walks app/**/api/**/route.ts, which is exactly the case a Hono or tRPC catch-all defeats — that is what --api exists for.

Then the ownership table, because the two files behave oppositely: flawless/swagger.json is rewritten every run, while flawless/mocks/handlers.ts keeps your edits — it regenerates only when the API spec moved and the file still matches its generated stamp, and --force overrides even hand edits — it is the file you edit, and the reason a body you fixed once stays fixed. The three MSW wiring points are named and bounded: setupServer in the Vitest setup, a service worker started before the host mounts for browser and perf specs, and neither of them reaching a server component, which is the honest reason e2e runs against the real app.

And synthesis, which explains most empty-looking fixtures: per media type the tool takes example, then the first examples[*].value, then builds a body from the schema — every property of an object, exactly one item for an array, "" for a string except date-time, 0 for a number, false for a boolean, the first member of an enum, allOf branches merged, $ref resolved only when it is local. A coded status is preferred and a default response is used last and counted as 200.

flawless-react "src/app/dashboard/page.tsx" --nextjs . --api openapi.json --swagger-url=off

What it stops you getting wrong

A fixture of zeroes is a spec with no examples, not a generator bug — the fix is an example in the spec, or the hand-edit in handlers.ts that nothing will overwrite, or both. Do not assert a synthesised value as a contract: it is a shape placeholder. Do not hand-edit the handlers and expect a re-run to refresh them — that file is yours after the first run, by design.

flawless-regen

When Claude loads it

Its description scopes it to regenerating a suite safely and names the lifecycle words it teaches — generated, refreshed, kept — plus --force’s merge semantics, the colocated-spec provenance stamp, --changed’s affected-test narrowing, and proving a run happened from the result JSON and REPORT.md rather than stdout. Three triggers: before any regeneration over an adopted suite, after upgrading the tool, when a --changed run appears to have done nothing.

What it knows

Its centre is a four-row ownership table. Write-once files — budgets.json, host/main.tsx (the wrapper only; the host/registry.ts beside it regenerates), provider.fixtures.ts, e2e/global-setup.ts, mocks/handlers.ts, mock/routes.ts — are kept on a re-run and kept under --force. Merge-kept files (e2e/params.json, load/routes.pipes) are merged under force, so edited rates, hand-added flows and pipes you commented out survive. Colocated specs are stamp-checked. Everything else — out-dir specs, configs, generated prose, REPORT.md, sidecars — is rewritten either way.

The no-force run exits 0. Every task including perf-harness refreshes its derived files as a fresh scaffold would; the suite’s package.json is merged so your fields win, and a flawless:* script you customised is kept unless its value is one of the tool’s own spellings. Emission is deterministic, so a same-version refresh usually diffs nothing; after an upgrade expect the derived files and the version banner on line one of playwright.config.ts to move.

The stamp is the part with teeth. Every colocated spec opens with // @flawless-generated sha256:&lt;hex16&gt; flawless-react@&lt;version&gt;, hashing everything below the line and recomputed after the run’s style pass so a formatted spec still reads as untouched. Under --force: stamp matches body means rewritten; a mismatch means you edited it, so it is kept and listed as hand-edited; no stamp means kept and listed as pre-stamp. The escape hatch for both is to delete the file and re-run.

--changed narrows only the run: vitest related walks the module graph, Playwright gets an anchored --grep, e2e runs when an affected component renders on a route. The perf harness is kept, reporting status: "kept" and naming the version that generated the suite against the version now running.

flawless-react "src/**/*.tsx" --nextjs . --tasks perf-harness,unit-browser,perf-static,a11y,react-rules,e2e,load --changed --no-interactive

What it stops you getting wrong

--force is not “refresh harder”. A plain re-run already rewrites every derived file. Force adds exactly one thing: the rewrite of untouched colocated specs, e2e route specs and the provider wrapper. Its “Never” list says to decide whether you want that before asking, and never to force an adopted suite with uncommitted work — the stamp protects hand-edited colocated specs, but e2e route specs and the provider wrapper have no such guard.

--changed needs an explicit --tasks. A bare --no-interactive defaults to perf-harness alone, and a harness-only selection has no unit or e2e suites for the affected run to run — which is exactly why a --changed run can look like it did nothing.

Never take stdout as evidence. Write evidence to files and check the disk: call runFlawless or runCore and write the exit code, out dir and results to a result file; REPORT.md is rewritten every run, so a missing REPORT.md diff means the run did not happen or ran somewhere else; and git status --porcelain must match the expected touch surface for the run kind. In Next.js mode the default out dir resolves against the target root, so a run from the wrong cwd rewrites another project’s scripts and reports success.

Installing them

The installer writes them where an agent reads them, in either scope:

flawless-react skills install --select flawless-react,flawless-nextjs,flawless-api-fixtures,flawless-regen
flawless-react skills install          # a menu, when you are not sure yet
flawless-react skills list             # current / edited / missing, in both scopes

Each goes to .agents/skills/<name>/SKILL.md plus its own link at .claude/skills/<name>, and project scope wins when both a project and a user copy exist. npm install asks whether to install them; an interactive flawless-react run asks once at the end and stops asking as soon as the project has one. FLAWLESS_NO_SKILLS=1 silences both.

A stale copy is worse than none. The repository copy is the source of truth and it moves when the tool moves — flawless-regen alone describes a contract that changed twice. An outdated copy is loaded with exactly the same confidence as a current one, and it will be wrong in the situation you reached for it. That is what skills list is for: a file whose bytes differ from the copy embedded in your binary is reported as edited, and install refuses to overwrite it without --force. Re-run the installer after every upgrade; if you keep both scopes, check both.