Testing
NeMo Labs Voice Agent uses pytest for tests and the standalone coverage CLI for coverage. pytest-cov is
deliberately not a dependency. Wrapping pytest in coverage run enables continuous integration (CI) to
combine data files from several jobs.
Test Layout
There are exactly two suites, and no test modules live directly under tests/. Every test file
belongs to one of these directories:
pyproject.toml sets testpaths = ["tests"], so a bare pytest collects both suites. It also sets
addopts = "--verbose --strict-markers". An undeclared marker is a collection error, not a warning.
Running Tests
The everyday loop is the unit suite:
Focus the run while iterating:
Functional tests that need no GPU:
pytest, coverage, and pytest-timeout come from the test dependency group, which a plain uv sync
does not install. If pytest is missing, sync as the CI image does in docker/Dockerfile.ci:
Markers
Six markers are declared under [tool.pytest.ini_options] in pyproject.toml:
Marker selection has two important behaviors:
- The unit lane is selected by path, not by marker. Only a few modules under
tests/unit/carrypytest.mark.unit, so-m unitcollects a small subset. Point pytest attests/unitinstead. - The functional lanes are selected by marker. Every module under
tests/functional/sets a module-levelpytestmark, sofunctionaland, where applicable,gpuare reliable there.
There are no conftest.py files and no automatic GPU detection, so a gpu-marked test run on a
machine without a GPU fails rather than skips. Always pass -m "not gpu" on a CPU-only system.
Selecting With -m
-m takes a boolean expression over marker names:
Combine -m with a path to intersect both filters. The launch scripts use this approach.
Coverage
Wrap pytest in coverage run, as tests/unit/launch_scripts/Launch_Unit_Tests.sh does:
[tool.coverage.run] in pyproject.toml defines the coverage scope:
source = ["nemo_voice_agent"]: Includes only the library, notexamples/orevaluation/.concurrency = ["thread", "multiprocessing"]: Supports the threads and processes used by the bridge and bot servers.omit: Excludestests/*,.venv/*, and the generated scenario shards (nemo_voice_agent/evaluation/scenarios/data/*/group_*.py) so machine-written scaffolding does not affect the results.
To merge several runs, give each one its own data file and combine:
What CI Runs
.github/workflows/cicd-main.yml runs the launch scripts inside the CI image built from
docker/Dockerfile.ci:
A separate coverage job combines the uploaded data files. It enforces --fail-under per flag: 75 for the
unit lane, 0 for the end-to-end lane, and 80 for the combined all flag. Adding library code without unit
tests commonly causes the unit coverage threshold of 75 to fail.
Adding a Test
To add a test that matches the repository layout and CI contracts, complete the following steps:
-
Put the module in
tests/unit/by default or under the matchingtests/functional/<area>/directory. Never put it at the top level oftests/. -
Add the SPDX and Apache header.
copyright-check.ymlfails on any*.pywithout one in its first 10 lines. -
Mark functional modules with a module-level
pytestmark, addingpytest.mark.gpuwhen the test needs hardware. Only use markers from the table above.--strict-markersrejects the rest. -
Format and lint before committing:
-
If the test belongs to a new functional lane, add a launch script under
tests/functional/launch_scripts/h100/active/and register it in thefunctional-tests-h100matrix. CI does not auto-discover scripts.
Unit tests also cover evaluation-harness behavior, including scenario registries, gold replay, database hashing, and scoring aggregation. Evaluation explains what those tests validate, and Contributing describes the overall pull request workflow.