Build1 publisher3 min readPublished
A hardcoded array in three test gates missed a new arcade entry it never knew existed
The build reads one ENTRIES table, and three gates in the same suite read page lists written when the arcade was smaller. A ninth entry got built, linked and emitted while every gate printed pass.
The Engineer · Build desk
What happened
- Prismwar, a ninth entry for the author's browser arcade, was built, listed in the index and present in dist/, and it was not live yet.
- Three gates in the deploy suite never loaded it: the boot/paint/save gate, the stress battery and the SEO-heading gate, each carrying its own hardcoded page list from when the arcade was smaller.
- The stopgap was adding the missing rows to those gates by hand, after which the full run included Prismwar and passed.
- On Ashline, an idle auto-battler from the same developer, four deliberate sabotages were all reported as caught, and every one failed with the identical symptom of the army stalling at wave 1.
- A DOM smoke test on that game scored 13 for 13 on a build where the author opened the page and found a line across the screen with nothing to click.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A gate that names its own subjects can only cover the set it was written with, so each entry added after that day ships uncovered while the suite still prints pass.
- decision Importing ENTRIES into the gates removes the drift and gives one table veto power over what gets tested, so the table now needs a check that does not read it.
- cost Trusting a pass count means paying for a deliberate-breakage run per assertion, which is what turned 13 for 13 into a number the author would rely on.
One table decides what the arcade contains. build.mjs reads ENTRIES, and so do the index, the sitemap and llms.txt [5]. Three of the test scripts read a literal instead, an array of ids ending in the comment `/* ...frozen in time */` [9]. The comment was accurate. The repair the author names is two lines, importing ENTRIES from build.mjs and mapping it to ids, so that a new entry cannot be built without also being tested [10][12].
"Nothing in the output was false. It just described a smaller arcade than the one I was shipping," the author wrote [8]. Both halves hold, because a gate reports on the set it was handed and passes on the rows it can see [7].
Deriving each gate's list from ENTRIES has a cost worth naming before you adopt it. The suite then trusts the same table the build trusts. A row missing from ENTRIES is missing from the build and from the tests together, and no gate derived from it will say so. Catching that needs a check that does not read ENTRIES at all.
The harder question in the same week was whether a passing check is able to fail. Ashline, an idle auto-battler, got an instrument proof: break the engine on purpose, then require the balance suite to go red for each break [13]. The first version could not tell a planted bug from a resident one, because the unbroken build was already stalled at wave 1 by a cold start where the starting army did zero damage [15]. The repair was a control. The unbroken build runs first and has to pass, and a sabotage counts as caught only when the baseline was green and the broken build fails a check that fits the breakage [16].
The DOM smoke test had the opposite gap. It checked that five buy buttons existed and carried accessible names, and never that one could be pressed [17][20]. The cause of the dead screen was load() calling buyLoop(), the automated player, which spent the opening currency before first paint and left every button rendered disabled [19]. Four assertions closed it: an action available at load, a purchase affordable at load, the starting grant unspent, and wave 1 cleared by clicking alone within 12 seconds [21]. That takes the suite to 17 checks [1]. Putting buyLoop() back inside load() on purpose dropped it to 15 of 17, failing precisely the two grant checks [22].
Three more checks were passing without evidence. A gravity check returned true without reading velocity before and after a step, and now measures +90 vy per 0.1s at G=900, which is 900 per second and matches the constant [23][2]. `check('champion drawn larger', true)` was a literal that would pass on a blank canvas forever [24]. A reduced-motion check passed because the attack's 250ms cooldown refused the second call, so motion on and motion off looked identical; fixing that made the test fail on a good build, because the harness pins Date.now() to 0 and the cooldown never clears [25]. "That's two harness bugs and zero game bugs," the author wrote [26].
The literal became a spy that wraps Champion.draw, records the height passed on each call, and asserts the arena height exceeds the battle-line height [27]. The measured values are 52px on the line and 86px in the arena, a ratio of about 1.65 [28][3].
Ashline's balance bands already run against the engine extracted from the built HTML, not the source module [29]. That is the same discipline as deriving a page list from ENTRIES.
For the arcade lesson to transfer, two things have to be true of your repo: the build generates its subject list from data, and at least one gate names its subjects separately. Both were true here, in a project that deploys on push to main and ships nothing unless verify-all.sh prints ALL GATES PASS [1][2].
What to watch
- Whether the three gates actually import ENTRIES, or keep hand-added rows that drift again at entry ten.
- Whether the harness gets a clock the 250ms cooldown can advance, since pinning Date.now() to 0 failed a good build.
- Whether the delivery-path pattern spreads to the arcade gates, so they exercise the emitted dist/ output instead of source modules.