2026-07-24
Sprint 8 Closes: Eight Small Fixes, One Bug the Test Suite Was Structurally Unable to Catch
Sprint 8 of the beer app closed today. Where most sprints ship as one pull request per sprint, this one shipped as eight — a deliberate choice, since these were eight small, independent fixes rather than one connected feature, and bundling them would have made each one harder to review or revert on its own.
Reusing a flow instead of building a new screen
The admin-invites-a-bartender story could have meant a whole new "set up a new staff account" screen with its own password field and its own validation. Instead it reuses the password-reset flow that already existed: the invite endpoint creates the account and generates the same kind of reset token forgot-password already generates, and the email links to the same reset-password page that already exists. From the new hire's point of view it looks like a normal "set your password" link. From the codebase's point of view, zero new frontend screens were needed — the existing one just gets used for a second purpose it was never explicitly designed for but happens to fit exactly.
A shared PIN-authorization helper, used twice
Two stories this sprint both needed to resolve "which bartender does this PIN belong to, and are they locked out" — the existing beer-confirmation flow, and a new one that lets a bartender flip a beer's stock status using the same PIN they'd type to confirm a drink. Rather than copy that resolution-and-lockout logic a second time, it got pulled out into one shared method both endpoints call, so the two features share a single per-customer brute-force counter instead of each getting its own. That matters for more than tidiness: if guessing PINs against the confirmation endpoint and guessing PINs against the availability endpoint had separate lockout counters, a customer trying to brute-force a PIN could double their guessing budget just by splitting attempts across the two. One shared counter closes that off as a side effect of not duplicating code, not as a separately-designed security feature.
A crowd-sourced signal that reuses an existing screen
Customers can now flag a beer as unavailable — a keg that's actually kicked but hasn't been marked out of stock yet. The interesting decision wasn't the reporting side, it was where the report goes: rather than a new admin screen, it surfaces as a new entry type in the anomaly panel that Sprint 5 already built for off-hours activity and confirmation velocity spikes. A crowd-sourced customer report isn't really a statistical anomaly, but reusing the panel an admin already checks every Monday morning beat building a second place they'd have to remember to look.
The bug that only a real database could show
The most consequential thing to come out of this sprint wasn't a feature, it was a bug found while writing one. Adding a "this delete will fail if the beer's been confirmed" warning to the beer-management screen meant actually checking what happens today when an admin tries to delete a beer that customers have already confirmed — and the honest answer was a raw, unhandled 500 error with a full database stack trace in the response body. The foreign key between a confirmation and the beer it confirms is set to block deletion rather than cascade it, which is the correct behavior — the confirmation is real history, it shouldn't disappear because someone deleted the beer — but nothing in the code was catching that block and turning it into a clean error message. It just failed at the database layer and let the exception fall straight through to the client.
What made this one worth calling out specifically: every automated test in this project runs against an in-memory database stand-in, and that stand-in doesn't enforce foreign key constraints the same way a real database does. Every single unit and integration test for that delete endpoint had been passing the whole time, and none of them could have caught this — the bug only existed at the boundary between the code and a real Postgres database, which is exactly the boundary the test suite doesn't cross. It only became visible by actually running the app against its real database and trying the thing a real admin would eventually try. The fix — check for existing confirmations before attempting the delete, return a proper 409 with an explanation instead of letting the database throw — took a few minutes once found. Finding it took building a UI feature that happened to make someone look at that code path.
The PM angle
Two things stood out about doing eight small stories instead of one big one. First, "one PR per issue" isn't just process theater — a couple of small, unrelated frontend fixes that would have needed careful test updates to coexist in one branch instead just merged one at a time, cleanly, before the next one started. Second, and more importantly: a green test suite is a claim about the code paths the suite actually exercises, not a claim about the code. The delete-beer bug lived in exactly the gap between "the in-memory test double behaves reasonably" and "the real database enforces a real constraint," and no amount of additional unit tests written against the same test double would have found it. The only thing that found it was actually running the real thing.
Where the project stands
Eight sprints built and closed now, including this one — 328 backend tests and 209 frontend tests, both green, plus the delete-confirmation bug fixed and verified live against the real Docker/Postgres stack in both directions: a beer with confirmations correctly refuses to delete with a clear message, and a beer with none deletes cleanly as before. The next epic — badges, push notifications, a full "My Beers" screen, a social layer — is bigger than anything scoped so far, big enough that it got split rather than groomed all at once: a "My Beers" completion sprint and a push-notification-infrastructure sprint are scoped and ready, with the rest of that epic deliberately left unscoped until its turn comes.
Comments
Loading comments...