Daeseon Yoo
Back to project
·Tech retro·3 min

It compiled, the tests passed, and it put the reply in the wrong inbox

AI coding reliably gets you code that compiles and passes its unit tests. It does not automatically guarantee the basic interactive details — async races, stale state on switch — that a human assumes are 'just there.' The fix is a checklist, not faith.

I clicked one inquiry, told it to draft a reply, then clicked a different inquiry — and the first draft dropped into the second one's box.

A basic bug. The kind you assume any half-decent UI just handles. My honest reaction was the founder's: "I assumed this would just be there."

It wasn't. And the interesting part is why, because it says something about what AI-assisted coding gives you for free and what it doesn't.

What the AI guaranteed

The reply feature was real and worked: read the inbox, draft from the company's own memory, send over SMTP, remember the send. The Rust compiled. 84 unit tests were green. The production build passed. By every automatic signal, it was "done."

And it was — for the happy path. Open an inquiry, draft, send. That path is exactly what the model writes well, and exactly what a compiler and unit tests verify.

What it didn't

The bug lived in the gap between those signals and reality. Drafting is async — a few seconds of the user's own claude -p. If you switch inquiries while it's drafting, the result arrives after you've moved on and lands in the wrong place. No compiler catches that. No unit test I'd written caught it. It shows up only when a human clicks fast, out of order — which is to say, when a human actually uses it.

This is the honest shape of AI coding right now. It is very good at the path you described and the cases that are testable. It does not automatically internalize the boring, obvious-to-a-human interactive details: stale async results, resetting state on a context switch, blocking a double-submit, making the highlighted thing be the shown thing. Those aren't hard. They're a class. And a model that didn't trip over them in this component will trip over a sibling next week, because nothing forces the check.

The fix isn't faith, it's a list

So I stopped treating "it should just handle this" as a plan. The fix was two things:

  1. The bug. Track the open item in a ref; apply an async result only if you're still on the same item when it resolves — if (current === startedFor) apply(). A draft that finishes after you've moved on is dropped, not misplaced.
  2. A checklist — a small UI_STATE_CHECKLIST.md of the basic interactive behaviours to run through before calling any interactive component done: async result guards, reset-on-switch, double-submit, empty states, recipient-confirmed-before-send.

The point of the list isn't that the items are clever. It's that the model won't reliably remember them and the compiler won't enforce them — so a human, or a rule, has to. Externalize what the tool doesn't internalize.

The uncomfortable, useful takeaway: "it compiles and the tests pass" is a real bar, and it is not the bar. For anything a person touches, the bar is a person touching it wrong on purpose — and a list that remembers the ways they will.