- Notifications
You must be signed in to change notification settings - Fork 18
Testing
Kai has roughly 5,900 tests across 167 test modules. This page explains how to run them, how the suite and CI are organized, and what patterns to follow when writing new tests.
make test# full backend test suite (pytest, verbose)
make lint # ruff check
make check # ruff check + ruff format --check
make format # ruff format (auto-fix)
make typecheck # pyright, strict
make client-check # Workshop client: typecheck + vitest + verify-generatedOr directly:
.venv/bin/pytest tests/
.venv/bin/pytest tests/test_bot.py -k "test_help"# run specific tests
.venv/bin/pytest tests/ -x # stop on first failureAuxiliary checks, all run by CI:
make check-install-constraints # pip dry-runs against the constraints file
make module-sizes # module size budget (scripts/module-sizes.py)
make audit-deps # pip-audit over installed dependenciesThe suite has three broad tiers rather than a one-file-per-module mapping:
- Core: the classic surface:
test_bot.py(plustest_bot_totp.py),test_config.py,test_claude.py,test_pool.py,test_webhook.py/test_webhook_api.py,test_install.py,test_review.py,test_triage.py,test_sessions.py,test_history.py, voice, services, locks, and friends. - Memory: the semantic memory subsystem and its eval harnesses.
- Workshop: about 81
test_workshop_*.pymodules covering the canonical core (store, projections, channels, agents, runs, delivery, memory queries, settings, scheduling, client API). Shared helpers live intests/workshop_delivery.pyandtests/workshop_profiles.py.
tests/conftest.py provides global fixtures that apply to every test automatically, giving safety guarantees individual tests don't need to remember to set up.
The Workshop browser client has its own suite: make client-check runs the TypeScript typecheck, vitest, and verify-generated, which rebuilds the client bundle and fails if the committed output under the static directory drifts from source. The built bundle is committed, so a client change without a rebuild fails CI.
CI runs five jobs on every push and PR:
- changes: classifies the changed paths with
scripts/ci_change_scope.pyintoclient/full/dependencyoutputs. Docs-only changes take a fast path; client-only PRs run the lighter client lane. - quality: install constraints, client check, lint and format check, type check.
- tests: a three-way matrix (
core,memory,workshop) sharded byscripts/ci_test_shard.py, so the suite runs in parallel lanes. - check: the required-lane gate that branch protection points at.
- core-workshop: installs without the
telegramextra and runs the Workshop lane, proving the Telegram-free boundary holds.
The CI scripts have their own regression tests (tests/test_ci_change_scope.py, tests/test_ci_test_shard.py), and .github/CI.md documents the workflow in the repo. A separate scheduled workflow (dependency-audit.yml) runs the dependency audit.
Tests that touch the filesystem use pytest's tmp_path fixture. Sessions, config, and history tests create real SQLite databases and config files in temp directories rather than mocking the filesystem. This catches real path-handling bugs that mocks would hide.
Most of Kai's code is async. The suite sets asyncio_mode = "auto" in pyproject.toml, so plain async def test functions just work; no pytest.mark.asyncio marker is needed. Use AsyncMock from unittest.mock for async collaborators:
asyncdeftest_something(self):
mock_fn=AsyncMock(return_value="result")
# ...Bot tests use factory functions to create mock Telegram Update and Context objects:
_make_update(text, user_id)- creates a mock Update with a message_make_context(bot)- creates a mock CallbackContext
These set up the minimum attributes handlers need (message text, user ID, chat ID, bot instance) without pulling in the full python-telegram-bot object graph.
Backend, transcribe, and TTS tests mock asyncio.create_subprocess_exec (or subprocess.run for sync code) to simulate subprocess behavior without actually running external binaries. This makes tests fast and deterministic.
Service proxy tests use aioresponses to mock outbound HTTP requests:
withaioresponses() asmocked:
mocked.post("https://api.example.com/v1/chat", payload={"result": "ok"})
# call the service proxy...monkeypatch.setattr is the preferred way to patch module-level attributes and globals. It's cleaner than unittest.mock.patch for module globals and auto-restores after each test.
- Put tests in the corresponding file: a change to
webhook.pygets tests intest_webhook.pyortest_webhook_api.py; a Workshop module gets atest_workshop_<area>.pyhome. - Group with classes: use
class TestFeatureNameto organize related tests. - Write plain
async deftests; the auto asyncio mode handles the rest. - Use real databases when testing database interactions (via
tmp_path); mock databases only when the test isn't about database behavior. - Never use real timeouts: mock time or use tiny values, so the suite stays fast and deterministic.
- Add docstrings to tests that verify non-obvious behavior, and follow the commenting style used in the existing tests.
- Client changes rebuild the bundle: run
make client-checkbefore pushing anything that touchesworkshop-client/; CI fails on a stale committed bundle.