When Unit Tests Lie: The Race Condition in Your Telegram Bot

When Unit Tests Lie: The Telegram Bot That Passed Everything—Except Reality
The bot-social-publisher project looked bulletproof on paper. The developer had just shipped a ChatManager class to implement private chat functionality—a permission system where bot owners could lock down conversations and restrict access to trusted users only. Perfect for personal AI assistants and moderated group chats. The architecture was clean: SQLite migrations for the managed_chats table, four new command handlers (/manage add, /manage remove, /manage status, /manage list), and middleware wired into aiogram to check permissions before processing any message. The test suite ran green. Every assertion passed. Then they fired up the actual bot.
The first integration test seemed to work flawlessly. Launch with python telegram_main.py, send /manage add from a personal account to privatize the chat, post a message—the bot responds. Switch to a secondary Telegram account, send the same message—silence. Perfect. The permission layer held. But when the developer executed /manage add and /manage remove in rapid succession, something broke. Messages weren’t getting through when they should have.
The first problem was a race condition hiding in plain sight. In aiogram’s asynchronous architecture combined with aiosqlite, the middleware’s permission check could execute before the database transaction from /manage add actually committed to disk. The handler would receive the command, start writing to the database, but the access control system would check permissions in parallel—reading stale data from before the write landed. No unit test catches that because unit tests run the functions in isolation, sequentially, without the noise of real asynchronous execution.
The second issue was more subtle: SQLite’s handling of concurrent async operations. When multiple handlers ran simultaneously, one would write a change while another was mid-permission-check, causing the reader to see outdated state because the commit() hadn’t fired yet. The fix required explicit transaction management and careful await ordering to guarantee that database writes propagated before the next permission check ran.
This is where integration testing becomes non-negotiable. Unit tests verify that a function’s logic is correct in isolation. But real Telegram traffic, actual webhook delivery, the full middleware stack, and genuine database concurrency reveal failures that never show up in a controlled test environment. The developer had to physically send messages through Telegram’s servers, watch them traverse the entire handler pipeline, and observe whether the database state actually updated in time.
After resolving the concurrency bugs, the checklist grew: verify imports, validate migrations, test all commands through Telegram’s interface, run the full pytest suite, and document everything in docs/CHAT_MANAGEMENT.md with architecture notes. Eight checkpoints. Eight potential failure modes that nearly slipped through.
The lesson? When you’re building with async code and databases, green unit tests are table stakes—they’re not optional. But they’re also not sufficient. Real-world conditions, real concurrency, and real timing expose gaps that no mock can simulate.
😄 I guess you could say SQLite’s race conditions prove that even databases play tag sometimes.
Metadata
- Session ID:
- grouped_C--projects-bot-social-publisher_20260209_1220
- Branch:
- main
- Dev Joke
- Что сказал Celery при деплое? «Не трогайте меня, я нестабилен»