tests/ directory and no aggregate runner. This page tells you how to run each, what it needs, and what breaks if you skip it.
There is no CI. The repository has no
.github/ directory, no workflows, and no pre-commit hooks. Nothing runs on a push. Every check on this page is one you run yourself, before opening a pull request.The five suites
None of them needs a database, a GPU, or a network. Five model-server modules shell out to
docker compose config.
Run one at a time (see the warning below), a healthy tree looks like this:
Running them
Every command below assumes you are at the repository root and the root is onPYTHONPATH:
apps/providers, apps/telephony, and apps/runtime import each other as apps.*, so this is not optional. See Local setup.
Run the suites in separate
pytest invocations, one per package. Passing them all to a single command fails at collection with ModuleNotFoundError: No module named 'app.auth' and similar: apps/api imports a top-level app.* package via its conftest.py, and model-server/tests puts its own tree on sys.path, so whichever is collected first claims the name. Each suite passes on its own.Providers and telephony
Both suites need the runtime requirements as well as the API ones, pluspytest-asyncio:
apps/telephony/providers/plivo/serializers.py and .../vobiz/serializers.py both import pipecat.serializers.plivo at module level, and the Bhashini provider tests reach the Pipecat TTS base classes, so pytest fails at collection with ModuleNotFoundError: No module named 'pipecat' without it. pytest-asyncio is needed too — several modules use @pytest.mark.asyncio, and without the plugin those tests fail rather than skip.
API
apps/api/tests/conftest.py puts both apps/api (for app.*) and the repository root (for apps.*) on sys.path, so this works from any directory. Routes are exercised through FastAPI’s TestClient against in-memory stores and unittest.mock.patch, not a live FerretDB.
One module is opt-in. test_agent_telephony_integration.py places real calls against real provider credentials and is skipped unless you ask for it:
Runtime
pytest-asyncio is not optional here: test_hold.py and test_call_metrics_writer.py are @pytest.mark.asyncio coroutines, and without the plugin pytest reports PytestUnknownMarkWarning and fails them.
A separate virtualenv is worth it — apps/runtime/requirements.txt pulls in pipecat-ai[deepgram,cartesia,openai,silero,websocket]==1.8.1, which the other suites neither need nor want.
Model server
tests/requirements-dev.txt is deliberately small: pytest, pytest-asyncio, httpx, numpy, fastapi, uvicorn, ruff, websockets, plus the three grpcio packages that exercise stt/_grpc/. No torch, no NeMo, no CUDA. tests/pytest.ini sets asyncio_mode = auto, so async tests need no decorator.
Add
pyyaml as well. Thirteen modules import yaml to read models.yaml and the compose files, but requirements-dev.txt does not list it, so a clean virtualenv built from that file alone fails at collection with ModuleNotFoundError: No module named 'yaml'.What needs Docker
Five model-server modules shell out todocker compose config: test_model_switching.py, test_gpu_placement.py, test_grpc_facade.py, test_model_extras.py, and test_mps.py. That subcommand only interpolates the compose files, so it needs the docker CLI but no running daemon. Four of them guard with skipif(shutil.which("docker") is None); test_grpc_facade.py instead checks that docker compose version exits zero. On a machine with no Docker at all the whole model-server suite still runs; you just lose those modules’ coverage.
test_gpu_placement.py::test_the_model_matches_real_compose fails rather than skips on a Docker Desktop install. The skipif guard resolves docker on the full PATH, but the subprocess is then launched with a hardcoded env={"PATH": "/usr/bin:/bin"}. Docker Desktop puts the binary in /usr/local/bin, so the call raises FileNotFoundError before reaching the returncode != 0 check that was meant to skip. Passing the inherited PATH through fixes it.Why the model-server suite needs no GPU
The GPU stack is stubbed.model-server/tests/stubs/ holds stand-ins for the three heavy imports:
tests/conftest.py puts that directory first on sys.path, ahead of any real package:
Fixtures and stubs
There are only twoconftest.py files under apps/, and both do one narrow job.
apps/api/tests/conftest.py fixes imports:
apps/runtime/tests/conftest.py stubs the Pipecat runners so route tests do not build a pipeline:
sys.modules assignment has to happen before apps.runtime.app is imported, which is why the import sits below it with a noqa. It also provides the shared client fixture wrapping TestClient(app).
apps/providers/tests and apps/telephony/tests have no conftest.py at all. They rely on PYTHONPATH and on load_providers() doing its own discovery.
model-server/tests/conftest.py does more: the stub path insertion above, a free_port() helper, a serve() helper that runs an ASGI app on a background thread and waits for it to accept, and a find_setup() helper that locates setup.sh in either of the two places it has lived. Its comment on that last one is worth reading — hardcoding either path “turns a move into six silent skips: the suite stays green while the checks are simply not running.”
What each suite protects
apps/providers
Five modules.test_provider_schemas.py tests the registry as a whole rather than each vendor: it pins the union member counts (13 STT, 15 TTS, 10 LLM), asserts every registered config has a creator and the reverse, and walks every provider asserting the catalog dump contains no $defs, $ref, or anyOf, that every secret field is marked and carries no input_mode, and that every language id emitted exists in the canonical LANGUAGES map. test_availability.py, test_capabilities.py, test_scoped_settings.py, and test_settings_by_model_language.py cover which providers are reachable, what each declares it can do, and how per-model and per-language settings resolve. See Adding an AI provider.
apps/telephony
test_registry.py asserts the registered set is exactly {"vobiz", "plivo"} and that the lazy serializer load works. test_xml.py pins the answer XML per sample rate — the string most likely to be silently wrong, because malformed XML produces a call that connects and then goes quiet.
Two tests in
test_serializers.py currently fail: test_create_frame_serializer_known_providers[plivo] and [Plivo], with ValueError: auto_hang_up is enabled but missing required parameters: auth_id, auth_token.This is a real defect, not a stale test. Pipecat’s PlivoFrameSerializer defaults auto_hang_up to True and then requires auth_id and auth_token, but apps/telephony/providers/plivo/serializer_service.py passes neither — and neither does the runtime callsite in apps/runtime/services/pipecat/runners.py, so the serializer raises before any Plivo call can start. Vobiz is unaffected because VobizFrameSerializer.InputParams sets auto_hang_up=False explicitly. The fix is to do the same for Plivo, since the runtime ends calls itself and holds no Plivo API credentials at that point.apps/api
The broadest suite.test_secret_crypto.py covers Fernet encryption of ProviderAuth. test_agent_telephony_service.py and test_agent_telephony.py cover application provisioning and teardown on agent create, update, and delete. The campaign modules cover the dispatcher, the repository, CSV sync, and the status processor.
apps/runtime
test_routing.py covers /answer and the /agent WebSocket handshake. test_hold.py, test_call_ending.py, and test_prompt_substitution.py cover pipeline behaviour that only shows up mid-call.
model-server
model-server/tests/README.md has a full table. The shape of it: the early entries guard the audio itself (test_stt_audio_parity, test_tts_request_parity, test_pcm_chunk_boundaries), the middle ones guard the wiring between files (test_catalogue, test_model_switching, test_setup_selection), and the last ones guard the seam between the model server and the voice pipeline.
Four model-server modules silently skip
test_llm_wiring.py, test_client_selection.py, test_tts_format_negotiation.py, and test_partial_transcripts.py all locate the voice pipeline at ROOT.parent / "voice_2_voice_server" — a directory that no longer exists. Every test in those four modules therefore skips, and a skip does not fail a run.voice_2_voice_server was renamed to apps/runtime in the revamp. The directory those tests point at was never recreated, so V2V.is_dir() is False and 46 tests across the four modules never run. The exact number moves with the catalogue — test_llm_wiring.py parametrises over deployable_llms().
The suite is honest about it. conftest.py installs a pytest_terminal_summary hook that prints a red NOT VERIFIED block after the summary line, naming what is unverified while that is true:
- every model marked
readycan actually be named by an agent config - the client decodes the audio format each TTS model declares
- partial transcripts still reach the caller mid-utterance
apps/runtime. The specific paths they look for are voice_2_voice_server/api/services.py, voice_2_voice_server/services/ai4bharat/stt.py, and voice_2_voice_server/services/ai4bharat/tts.py, none of which map one-to-one onto the current runtime layout — the client selection logic now lives in apps/providers and the pipeline in apps/runtime/services/pipecat/. Repointing them is a real piece of work, not a path substitution.
Until that is done, treat the model-server suite’s pass count as covering the server side only. The seam between the model server and the voice pipeline is unverified, and that seam is where a mistake stays invisible until a live call drops.