Six defects ordinary play would never have found

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

Singularity Tower went into Chrome Web Store review this morning. Rather than refresh the dashboard all day, I spent the wait trying to break it.

The build already had eight automated suites and they were all green. That is exactly the problem with green suites: they only prove the things you thought to ask. So instead of writing more of the same, I attacked the game from four directions at once, each one deliberately hostile in a different way.

Four ways to attack your own game

  • Static analysis. Read every line looking for the classics: dynamic code execution, unescaped markup, promises with no catch, timers nobody clears.
  • Hostile saves. Feed the loader garbage. Null, an empty object, an array where a map should be, a resource set to NaN, a resource set to negative five hundred, a resource set to infinity.
  • A clumsy player. Not a malicious one, just a normal person having a normal day: rage-clicking a button twelve times, typing a note and closing the window immediately, starting a minigame and wandering off, pasting nonsense into the import box.
  • A hostile browser. Storage that suddenly refuses to write. A clipboard API that does not exist. An extension reloaded underneath an open page.

What broke

Six things, and the worst of them was the quietest.

A tab that never comes back

The tower has hired workers who buy a generator every ten seconds. When time passes, the engine catches them up by looping until the backlog is spent. That loop had no ceiling. Hand it a large enough gap and it grinds for minutes; hand it an infinite one and the tab freezes for good.

In normal play this could not happen, because the interface clamps elapsed time to five seconds before the engine ever sees it. But the engine is a separate module with its own front door, and a clock jump, a resumed laptop, or simply a second caller could walk straight through it. A defect that is currently unreachable is still a defect; it is just waiting for the shape of the code to change. Time is now validated at the boundary, capped at an hour, and the catch-up loop has a hard iteration ceiling.

A save that loads cleanly and then dies

The save migrator checked the version number and passed everything else straight through. Give it a save missing its floors — hand-edited, truncated, or imported from a bad clipboard — and it would load without complaint, render a normal-looking tower, and then throw on the very first frame. Every frame after that too. The game was unrecoverable without a full reset, and nothing on screen explained why.

A save file is user input. It now goes through a sanitiser that coerces every field into a sane shape, clamps generator counts and upgrade levels, and truncates anything oversized. A save that is wrong gets repaired into one that is merely disappointing.

The note that was never there

The notes card autosaves seven hundred milliseconds after you stop typing. Type a note, close the panel within that window, and it was gone — no error, no warning, just an empty card next time. This is my favourite kind of bug: technically correct, completely indefensible. It now flushes when the field loses focus, when the tab is hidden, and when the page unloads.

Silence when saving stops

This one only happens to people who install updates, which is to say everyone. Reloading or updating an extension invalidates the context of every page it already has open. Every storage call from that page starts failing. The tower kept ticking, kept accepting purchases, kept looking completely healthy — and saved none of it.

Storage now degrades instead of throwing, tracks its own health, and the page tells you plainly that saving has stopped and a reload will reconnect it. Failing is acceptable. Failing quietly, while the player keeps working, is not.

The remaining two were smaller: a multiplier that ran to infinity given an absurd generator count, and a save-as-PNG button that produced one file per click if you were impatient with it.

What held

Worth recording, because a stress test that only produces bad news is not telling you much. Twelve rapid clicks on a buy button bought exactly what twelve clicks should buy — the re-entrancy guards held. Pasting nonsense into the save importer was rejected without touching the existing save. The read-only surface genuinely cannot write. There is no path by which a note reaches the page as markup, so there is no injection surface. The game boots on an engine with no modern media-query API. With three windows open, exactly one holds the clock.

Two things I got wrong about testing itself

The first: my fake storage layer handed back the same object it was given, while real browser storage takes a structural copy. Two simulated windows were therefore sharing one save in memory, and the harness reported that the read-only window was corrupting the file. It was not. I nearly fixed a bug that did not exist, which would have been a worse outcome than the bug.

The second is more embarrassing. Partway through the fixes I inserted the new sanitiser into the file and asserted that the insertion landed — but never asserted that anything called it. It sat there, complete and unreachable, while the tests kept failing for a reason I had supposedly just fixed. The only thing that caught it was re-reading the written file rather than trusting the report that the edit succeeded.

Both are the same lesson wearing different clothes. Verify the artefact, not the account of the artefact. A green test is evidence about the test as much as about the code, and the moment you stop checking which, you are just reading your own handwriting back to yourself.

Where it stands

All six defects are fixed, and all six are now permanent tests in a hardening suite that runs on every build. That brings the tower to nine automated suites: idle simulations across thirty simulated days, eighty-five feature cases, two concurrency suites for the save lease, a DOM-contract extractor, a manifest tripwire, a constants tripwire, a full render suite that boots the real pages, and now hardening.

The project page is updated with everything the game actually does now — workers, contracts, the arrangeable board, the full conversion chain from pond scum to essence. The install link goes up the moment review clears.

Until then, it is a tower nobody can play yet, tested harder than it strictly needed to be. That seems like the right way round.