Work · Agent-built app · 2026
Six bugs the static checks could not see
A two-sided ride-hailing app written with coding agents passed its analyzer and its schema self-check. Driving the release build against the live backend found six bugs that would each have stopped the first twenty drivers.
1. The setup
A rider app and a driver app in Flutter, a hosted Postgres with PostGIS and row security behind them, offers and ride state driven by database triggers, and a live map. Most of the code was written with coding agents in a few days. The static analyzer was clean, and a self-check script exercised the ride state machine in SQL and passed.
Before the first drivers touched it, both apps were built as release web builds, pointed at the live backend, and driven with a browser at phone size with the location faked to a real street in the city. The rule for the session was that a screenshot decides, and an assertion in code is a hint.
2. What the checks could not see
Six defects would have shipped. None of them is exotic, and each one lived at a boundary the analyzer and the SQL self-check do not cross.
| Symptom | Cause | Why the checks missed it |
|---|---|---|
| A driver goes online and never receives a ride | The position stream only emits when the car moves, so the last-seen timestamp never updated and the matcher treated the driver as stale | Correct code; the failure is a parked car |
| No live updates anywhere | Two tables were never added to the realtime publication, so every screen looked alive on one-shot fetches only | Configuration, and the self-check reads the tables directly |
| No driver could accept a ride | Row security allowed a driver to read a ride only after accepting it, and the accept button sat inside the fetch that failed | The self-check ran as the service role, which bypasses row security |
| Every read of the rides table failed with infinite recursion | The fix for the previous row referenced a table whose policy referenced the rides table back | Policies are evaluated by the database, and only under the anonymous key |
| Every map screen crashed to a grey box | The API serialises geography columns as hex binary, and the client expected GeoJSON | Type-correct on both sides, wrong at the wire |
| No button that changed ride state did anything | The client library returns lazy futures; a handler that created one without awaiting it sent no request and logged no error | Compiles, runs, and fails silently |
3. The failure that came later
A week after release the driver app showed a driver as online while the socket behind it was dead. The realtime stream had gone quiet, and quiet is not an error, so nothing fired. On office wifi the state never occurs. On a city mobile network it is routine.
The fix was a heartbeat with a visible offline banner, and the test for it needed a window longer than the 25-second heartbeat interval, because any shorter test reports a false pass. The banner text was not exposed to the accessibility tree by the web build either, so the assertion said absent while the screenshot showed it. Screenshots stayed the source of truth.
4. What to take away
- 1
An agent-written codebase passing its analyzer says the code is well formed. It says nothing about the boundaries: policies, publications, serialisation, and the network.
- 2
Read the database as the anonymous user. A self-check that runs as the service role cannot see a single row-security defect.
- 3
Drive the release build against the live backend at phone size before the first real user does. Six blocking bugs in one afternoon is the normal yield.