Gate Tests logo

Gate Tests

OrganizationPopular
vercel
gate-tests

How to use the `@gate` / `@force-gate` test directives instead of `it.skip` or fake-green skip patterns. Use when a test is known-failing under some test-matrix dimension (dev mode, a bundler, an experimental flag like cacheComponents), when converting `if (isNextDev) return` guards or env-var `describe.skip` branches, when adding a condition to test/lib/gate/conditions.ts, or when keying a fixture's experimental flag on a __NEXT_TEST_AXIS letter. Covers directive choice, condition tiers, the test-axis fixture pattern, pitfalls, and verification commands.

Overview

Publishervercel
Repositorynext.js
Skill namegate-tests
Stars
143.1K
Forks
34.7K
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by vercel on GitHub. Read the source before you install it.

Installation

Install the Gate Tests AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/vercel/next.js/tree/canary/.agents/skills/gate-tests
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/vercel/next.js.git /tmp/next.js
mkdir -p .claude/skills
cp -r /tmp/next.js/.agents/skills/gate-tests .claude/skills/gate-tests
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Gate Tests in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Gate Tests on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Gate Tests is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Gating tests with @gate / @force-gate

Full reference: test/lib/gate/README.md. This skill is the decision guide: which directive to reach for, the standard conversion patterns, and how to verify.

Never write these — gate instead

Anti-patternReplacement
it.skip('...') for a known failure// @gate <cond> (or @gate FIXME if no condition explains it)
if (isNextDev) { test('skipped in dev mode', () => {}); return }// @force-gate prefetching (or !dev) on the describe
(flagEnabled ? describe.skip : describe)(...) keyed on process.env// @force-gate <cond> (lazy) on the describe
Duplicating a fixture directory per flag stateone fixture keyed on __NEXT_TEST_AXIS + a @gate/@force-gate
Branching expectations on process.env.__NEXT_CACHE_COMPONENTSif (await gate((c) => c.cacheComponents)) (gate from next-test-utils)

The skip patterns are fake-greens: nothing tells you when the bug they hide is fixed. @gate still runs the body and fails the suite the day the "known failure" starts passing, so stale workarounds get deleted instead of rotting.

Choosing the directive

Ask what kind of difference you're encoding:

  1. A behavior change — both states assert something meaningful. Don't gate the test at all: fork inside the body with the runtime gate() — same condition registry, no inversion — which pinpoints exactly what differs, and also covers it.each, where a pragma cannot attach: if (await gate((c) => c.cacheComponents)) { ... } else { ... }. It mirrors React's gate(flags => ...); a pragma expression string works too (await gate('cacheComponents && !dev')). A suite-level pragma is too coarse here — it hides what is different between the states.
  2. A flag that changes the behavior of existing surface (cacheComponents, optimisticRouting) and the suite is written for one state. // @gate <cond> on the test or describe. The body runs; a false condition inverts the expectation (failure absorbed, a pass fails as "stale gate"). The off state fails for a meaningful reason — the behavior differs — so a pass is real information: the gate is stale, delete it.
  3. A new API — the off state proves nothing. Typically // @force-gate <cond> (lazy) on the describe. An API that throws when its flag is off — or is inert, like useOffline(), which compiles to a hook that always reports online — can only fail vacuously (often slowly, by timing out), and browser e2e time is considerable, so skip the run (and the fixture build) instead of paying for it. Working example: test/e2e/app-dir/use-offline/. This is discretion, not a rule: when the flag changes behavior the suite can observe, the off state is meaningful and @gate buys the staleness check.
  4. @force-gate <cond> also when running the body is impossible, not merely failing: prefetching is off in dev, deploy has no local build output, the fixture cannot even build under the condition.
    • Static condition (!dev, bundler…) → real Jest ○ skipped at collection.
    • Lazy condition on a describe → the fixture build is skipped when false; tests report passed-with-⚠ skipped by @force-gate (Jest cannot skip at runtime). Build-skipping covers start/dev suites where nextTestSetup owns the build — not skipStart suites, not deploy.
  5. Pragmas stack: a common pair is a static // @force-gate prefetching plus a lazy // @gate <flag> on the same describe.

Is the off-state run worth its cost?

Browser e2e time is not free, so weigh what the gated-off run buys. For a behavior flag it usually replaces a run that was already being paid for — a fixture that pins its flags runs identically with and without the axis set, so keying the flag on an axis converts a redundant duplicate into coverage — and it is what proves a pass isn't vacuous: a test that passes with the feature off wasn't testing the feature. Absorbed failures also fail fast, so the off-state run is cheaper than it sounds. For a new API the calculus flips: the off state can only throw, which proves nothing, so use a lazy // @force-gate <flag> on the describe — the fixture build is skipped too, so the off state costs almost nothing.

Conditions

Every name in a pragma must be declared in test/lib/gate/conditions.ts (typos fail the suite at collection). Two tiers:

  • static — the run's shape: dev, start, deploy, mode, turbopack, rspack, webpack, bundler, react18, wasm, linux, macos, windows, ci; the deploy host vercel; specialized CI variants adapter, nodeMiddleware, standaloneOutput, turbopackDev, and turbopackBuild; plus the always-false FIXME/TODO. prod and prefetching are semantic aliases for !dev — prefer the name that states why the suite cannot run. vercel is true only in a deploy run with NEXT_TEST_DEPLOY_TARGET_VERCEL=1, which our CI sets on every Vercel deploy job. Other platforms run the deploy suites through NEXT_TEST_DEPLOY_SCRIPT_PATH to test their adapters, so prefer it over deploy when the reason is Vercel-specific: @force-gate !deploy || vercel skips only other platforms, !vercel skips only Vercel.
  • lazy — a predicate over the fixture's resolved next.config (cacheComponents, ppr, useOffline, output, …).

Adding one is a two-line change; follow the guidance at the top of conditions.ts. The rule that matters: lazy conditions read the resolved config, never process.env — env vars don't survive config resolution (__NEXT_CACHE_COMPONENTS only applies when the fixture doesn't set cacheComponents itself, and resolution implies flags the fixture never mentions).

Pattern: cover both states of an experimental flag

Instead of pinning a flag on (which makes the plain and axis runs identical), key it on a test axis and gate the suite. Axes are lettered (A, B, …) — a fixed enumeration, not a boolean and not a sharding bucket. Key the flag so it is enabled by default — then the suite exercises the feature in plain local runs with no special env, and the axis run covers the off state:

js
// next.config.js — pin every dimension except the one under test
const nextConfig = {
  cacheComponents: true,
  experimental: {
    concurrentRouterQueue: process.env.__NEXT_TEST_AXIS !== 'A',
  },
}
ts
// @gate concurrentRouterQueue
it('fails loudly on link navigation', async () => { ... })

The plain run exercises the feature; the axis-A run covers the off state — the gated tests are expected to fail there, and the suite fails the day they start passing. Working example: test/e2e/app-dir/concurrent-router-queue/ (tests whose expectations hold in both states stay ungated). The same keying pairs with a lazy @force-gate when the off state proves nothing — test/e2e/app-dir/use-offline/ — which skips the redundant axis run (build included) instead of covering it. Axis A aliases __NEXT_CACHE_COMPONENTS for now (see scripts/run-jest.sh) — fine, because these fixtures pin cacheComponents explicitly, so that run's env default is a no-op for them.

Keep exactly one flag varying per fixture. A red shard must attribute to a single dimension.

Pitfalls

  • A pragma the transform can't attach is a hard error: a blank line between pragma and it(, it.each/it.failing, or a pragma inside a JSDoc block. Prose comments must not begin with @gate. A pragma on a skipped test (it.skip, xit, …) errors as ambiguous — remove the skip or the pragma. A skip without a pragma is respected.
  • A describe-level gate does not reach it.each tests.
  • Gated-false bodies that fail by stalling waste the full Jest timeout — and under a lazy gate they fail the suite anyway (the runtime inversion only absorbs thrown errors; a static gate rides Jest's native test.failing, which does absorb timeouts). Bodies that fail via retry() timeouts also make the off-state run slow; a fast first assertion is worth having.
  • Failures cascade in the off state: an absorbed failure mid-body skips the body's cleanup (e.g. a browser context left offline), so later tests may fail for cascade reasons. Acceptable for a tripwire, but don't puzzle over the individual failure messages in a gated-off run.
  • afterEach failures (e.g. redbox matchers) are not gated, only the body is. Hooks under a false lazy @force-gate are the exception: they are skipped with the suite instead of running against a fixture that was never booted.
  • Gated titles are unchanged in the Jest output; the ⚠ gated test failed as expected log line is the only signal.
  • pragma-transform.js bails out early on files containing neither @gate nor @force-gate as substrings — keep both checks if you touch it.

Verify a gated suite in every state it can run in

sh
# plain run (flag on): expect normal passes, no warnings
NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack test/e2e/app-dir/<suite>/<suite>.test.ts

# axis run (flag off): expect `⚠ gated test failed as expected (@gate …)`
__NEXT_TEST_AXIS=A NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack test/e2e/app-dir/<suite>/<suite>.test.ts

# dev (static @force-gate !dev): expect `○ skipped` at collection, no fixture boot
NEXT_SKIP_ISOLATE=1 pnpm test-dev-webpack test/e2e/app-dir/<suite>/<suite>.test.ts

A suite with a lazy @force-gate on the describe should additionally show skipping build behavior (no next build) in the state where the condition is false.

Unit tests for the infrastructure itself: pnpm test-unit test/unit/gate/.

Related skills

  • $flags — adding the experimental flag itself (config-shared, schema, define-env)
  • $router-act — the prefetch-timing patterns most gated suites also use

Frequently asked questions

What does the Gate Tests AI skill do?

How to use the `@gate` / `@force-gate` test directives instead of `it.skip` or fake-green skip patterns. Use when a test is known-failing under some test-matrix dimension (dev mode, a bundler, an experimental flag like cacheComponents), when converting `if (isNextDev) return` guards or env-var `describe.skip` branches, when adding a condition to test/lib/gate/conditions.ts, or when keying a fixture's experimental flag on a __NEXT_TEST_AXIS letter. Covers directive choice, condition tiers, the test-axis fixture pattern, pitfalls, and verification commands.

Why use Gate Tests on TypingMind?

Because you install it once and use it with any model. Gate Tests is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Gate Tests in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/vercel/next.js/tree/canary/.agents/skills/gate-tests. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Gate Tests?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Gate Tests?

As many as you like. As long as a model supports skills, you can use Gate Tests with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Gate Tests AI skill free?

Yes. It is published on GitHub by vercel under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇