All cases

2026-06 · DevOps

An autonomous mobile QA rig for a thirty-game library

Most players arrive on a phone and the QA team is me. A nightly LLM-judged smoke sweep files issues; a deterministic Playwright gate blocks merges. Both start from a cold profile, because that is the only state where the worst bug lived.

Directing AI systemsVerification and trustShipping under constraint

33

games in the manifest, swept in 2D and 3D

0

secrets the merge gate needs

2

rungs: judgment and enforcement, split

before

No capture of the install prompt sitting over a game's control dock survives from before the rig existed. The bug lives on as the gate's dock-reachability assertion, which fails if anything ever covers a control again.

during

RECURRENCE's cover screen on a phone-sized viewport, captured from a fresh browser context with no dismissed prompts.

The frame the nightly judge reads: cold profile, cover state, nothing dismissed.

2026-09-20 · The smoke harness's own capture: headless Chromium with SwiftShader WebGL, a fresh browser context, 390 by 844 at 3x

after

RECURRENCE in its 3D render mode during play, the board filling the viewport and the control dock along the bottom unobstructed.

Playing state in 3D on software WebGL, the dock unobstructed: the invariant the gate asserts.

2026-09-20 · The smoke harness's own capture: headless Chromium with SwiftShader WebGL, a fresh browser context, 390 by 844 at 3x

Walkthrough video: coming soon.

Cold profile, every shotfresh context, nothing dismissed, 390 by 844Rung 1: nightly sweepheadless Chromium + SwiftShader WebGLscreenshots + report.jsona model judges, files one issue per gameread-only: cannot edit codeRung 2: merge gatePlaywright asserts the invariantsdock reachable, board size, WebGL aliveblocks the merge, replayable tracesno model, no API keyHuman:triages the issues, reviews every fix as its own pull request

Problem

The site ships an install prompt that stays dismissed for thirty days. On my warm, logged-in profile every game looked fine. A first-time visitor on a cold profile got the prompt layered over the in-game control dock, and the taps that should have built a tower hit the prompt instead. Nobody files a bug for that. They leave.

Constraint

CI runners have no GPU, and the flag that disables it gives you a black canvas. No human visits every game in both render modes from a clean profile on a phone every day. And an LLM's opinion can never be the thing that blocks a merge.

Decision

Two rungs, built with different tools because they answer different questions. A nightly smoke sweep boots headless Chromium at an iPhone-class viewport with software WebGL, screenshots every game in 2D and 3D from a fresh browser context per shot, writes a report of console errors and failed assets, then hands the shots to Claude Code, which judges them against a written invariant list and files one labeled issue per failing game. A deterministic Playwright gate encodes the same invariants as hard assertions on every pull request: dock unobstructed, board large enough, WebGL context alive, scripted interaction error-free.

Rejected

  • One LLM loop that both judges and blocks

    A non-deterministic gate trains people to re-run until green. The judge files evidence; the gate blocks by determinism and uploads traces, so a failure is replayable, not arguable.

  • Test from my own logged-in profile

    That profile had dismissed the install prompt. Every shot uses a fresh context, so the cold-profile bug can never be masked by a tester who already dismissed it.

What shipped

The capture harness, the nightly workflow that hands the shots to Claude Code and files issues, the per-PR gate with a reference spec, a stale-game advisory mode, and an operator that drains the playtest backlog into reviewed pull requests.

What I'd do differently

The gate then broke for the runner's reasons twice: one game's Claim button never stabilized and blocked every pull request (#1415), and the runner ran out of disk, not memory (#2381). Each was its own fix, because a gate that is red for the wrong reason is a gate people learn to skip.

Verify it yourself

If you ask me about this

Why software WebGL?
CI runners have no GPU. Routing the renderer to SwiftShader draws the full 3D pipeline on CPU, and the harness inspects the live WebGL context afterward because flags drift across Chromium versions.
What can the nightly LLM run change?
Nothing. It is read-only: its whole output is an issue naming the game, the mode, the invariant, and the screenshot. The fix is always a separate reviewed pull request.

Evidence

  1. Wire up the playtest games QA harness and gate

    #722 · 2026-06-07 · feature

  2. Add stale-game advisory mode to the playtest harness

    #725 · 2026-06-07 · feature

  3. playtest smoke: inspect WebGL after the game starts, not at the cover screen

    #742 · 2026-06-07 · feature

  4. Agents Store: ship the game-builder operator — games.fix drains the playtest backlog

    #1002 · 2026-06-11 · bugfix

  5. the-pull: fix the Claim button never stabilizing (unblocks the playtest gate)

    #1415 · 2026-06-18 · bugfix

  6. Playtest gate: the runner runs out of disk, not memory

    #2381 · 2026-09-18 · feature

Receipts

Checked at build against the published corpus.

  • Every cited PR exists in the published corpus6 / 6
  • Pending PRs are named as pending, never passed off as mergedall merged
  • Cited PRs sit in the case's disciplinesDevOps, CI, Games
  • Numbers with no source say so0 unmetered

www.jakelawrence.xyz/research/case-study-library/playtest-rig

Cite: Lawrence, J. (2026). An autonomous mobile QA rig for a thirty-game library. jakelawrence.xyz case study library.

Need something like this built?

I design and ship AI tools, full-stack apps, and data pipelines — end to end, to production. Tell me the problem in a sentence; I'll give you an honest read on fit within a day.

Work with me →