DEV Community

Sherotree
Sherotree

Posted on

Frontend CI Gates That Catch AI-Generated Regressions

Green CI after an agent PR used to make me relax. Then I merged a “small” fix that renamed a shared prop, skipped an a11y lint because the rule was warn-only, and shipped a modal that trapped focus forever — all while every check stayed green.

The suite wasn’t lying. It was answering the wrong questions. Agents are great at satisfying whatever your pipeline actually enforces. If the gates are soft, the diffs look confident and the regressions still land.

So I stopped treating CI as a vibe check and started treating it as a blast-radius fence for AI-touched frontend work.

What AI regressions actually look like

The failures I care about rarely show up as “test file missing.” They look like:

  • Contract drift — export renamed, optional prop made required, event payload shape quietly changed
  • A11y regressions — interactive div, missing label, broken focus order, live region that never announces
  • Dependency sprawl — new package for a one-liner the repo already has a util for
  • Token / theme leaks — hardcoded hex next to the design-token import the agent “cleaned up”
  • Mock theater — new unit tests that assert the mock was called, not that the user path works
  • Bundle surprises — a “tiny helper” that pulls half of date-fns or a chart lib into a leaf route

Unit coverage can stay flat or even rise while any of those ship. That’s why gates have to sit outside “did something assert?”

Gate 1: Typecheck is non-negotiable (and strict)

I want tsc --noEmit (or the project’s equivalent) as a hard fail, not a soft job. Soft typecheck is how agents “fix” by adding as any, // @ts-ignore, or widening props until the error goes away.

Rules that help:

  • Fail on new @ts-ignore / @ts-expect-error without a ticket id in the comment
  • Ban any in changed files via eslint (@typescript-eslint/no-explicit-any as error on src/**)
  • Prefer exactOptionalPropertyTypes / strict null checks so optional props can’t silently become T | undefined soup

When an agent PR needs an escape hatch, the comment has to name why. “To make CI green” is not a why.

Gate 2: Lint rules aimed at agent habits

Generic eslint is not enough. I add a small set aimed at the diffs agents love:

  • No raw hex / rgb in component files when a tokens package exists
  • No new eslint-disable without a scoped rule name and a one-line reason
  • No dangerouslySetInnerHTML unless the path is allowlisted
  • JSX a11y as error for label, role, keyboard, and autofocus rules — warn-only is how regressions pass
  • Import boundaries — leaf features can’t import from other features’ internals; agents ignore folder manners unless CI fails

I keep the rule list short. A 200-rule wall trains the agent to spray disables. Ten sharp fails teach the habit.

Gate 3: Contract tests for public UI APIs

For shared components and hooks, I keep a thin contract suite the agent is not allowed to “simplify away”:

  • Export names and required props (type-level or runtime)
  • Stable data-testid / roles used by Playwright
  • Event payload shapes for onChange / onSubmit

These tests are boring on purpose. They’re the fence around rewrite energy. If the agent rewrites a Button internals, the contract still has to hold — or the PR splits into an intentional breaking change with a migration note.

Gate 4: One real path in Playwright (not five shallow ones)

After the fake-green lesson, I require at least one e2e (or component-playwright) path for any user-facing change in a critical flow. The gate isn’t “Playwright job exists.” It’s:

  • The PR touches features/checkout/** → checkout happy path must pass
  • The PR touches Modal / Dialog primitives → open, Tab cycle, Escape, restore focus must pass

I map folders to required specs in CI. Agents learn quickly: if you edit the folder, you inherit the path. No orphan unit greens.

Gate 5: A11y checks that fail the build

I run axe (or equivalent) on the stories / routes the PR claims to change. Soft a11y reports get ignored under deadline pressure; agents will too.

Pragmatic setup that works for me:

  • Storybook test-runner + axe on changed stories, error on serious/critical
  • Or Playwright + axe on the same critical paths as Gate 4
  • Fail on new aria-hidden on focusable nodes and on buttons without accessible names

I don’t try to axe the entire app on every PR. Scope to the blast radius. Agents respond to scoped hard fails better than global noise.

Gate 6: Bundle / import budget on touched routes

AI loves helpful imports. CI should love budgets:

  • Size limit (or relative budget) on entry chunks for routes the PR touches
  • Forbidden package list for UI packages that already have first-party utils (lodash*, giant icon packs, duplicate date libs)
  • Optional: knip / depcruise for unused and circular imports on changed packages

A 3kb “fix” that adds 80kb gzipped is still a regression. Put that in the gate title so reviewers see it without opening the bundle report.

Gate 7: Diff-shaped checks (cheap, high signal)

A few grep-level checks catch a surprising amount of agent damage:

  • New files under src/ without a corresponding test or story when the folder already has a pattern
  • Deleted snapshots without a CHANGELOG / PR note (optional; teams vary)
  • Changes to tokens/ or components/ui/ require a label (design-system) or a second reviewer
  • PR body must include a “what stayed the same” bullet list for AI-assisted changes (enforced by a bot comment, not a human memory)

None of these replace judgment. They force the conversation before merge.

How I wire this for agent PRs specifically

Human PRs and agent PRs can share the same pipeline. I still tag agent-assisted work so the gates can be slightly stricter:

  1. Label ai-assisted (or detect conventional PR body marker)
  2. On that label: no new any, no new eslint-disable, a11y serious = fail, folder→Playwright map required
  3. Require the agent (or author) to paste the acceptance checks that were in the prompt into the PR description

The point isn’t bureaucracy. It’s making the prompt’s contract visible next to the diff. Reviewers stop re-deriving intent from vibes.

What I deliberately do not gate

  • Pixel-perfect visual diffs on every PR (too flaky; I save visual for design-system primitives)
  • Full e2e suite on every leaf copy change (map critical folders; don’t boil the ocean)
  • Coverage % thresholds as the main signal (agents inflate coverage; see the fake-green post)

Gates should be few, hard, and aligned with user-visible risk. Soft gates train everyone — human and model — to ignore CI.

A minimal starter checklist

If you’re adding AI to a frontend repo this month, I’d land these first:

  1. Strict typecheck + ban new any / bare ignores on changed files
  2. jsx-a11y (+ axe on critical paths) as error
  3. Folder → one Playwright path mapping for money / auth / editor flows
  4. Import boundary + forbidden dependency list
  5. Bundle budget on touched routes
  6. PR template line: “AI-assisted? Acceptance checks: …”

That’s enough to catch most of the regressions I’ve actually shipped. You can fancy it up later.

Takeaway

Agents optimize for the fitness function you give them. If CI only asks “are there tests?” you’ll get tests. If CI asks “did the public contract, keyboard path, a11y tree, and route budget survive?” you’ll get diffs that respect those fences.

I still review. The gates just make sure “LGTM” isn’t the first line of defense against a confident, green, wrong PR.

Top comments (0)