Run and write tests
In short
piighosthas more than a thousand automated tests. They run in a few seconds without downloading any AI model.- The tests that load real models are kept apart. They run every night on GitHub.
- Before any merge, a blocking check verifies formatting, types, code security and documentation.
- Some of the tests are run by no automated job, because the libraries they need are missing. See What runs nowhere in CI.
The terms are defined in the glossary.
Run the tests
| You want to… | Command |
|---|---|
| Install the development environment | uv sync |
| Run the whole fast suite | uv run pytest |
| Run one specific test | uv run pytest tests/pipeline/test_pipeline.py -k "test_name" |
| Run the tests that load models | uv run pytest -m integration |
| Fix the formatting | make format |
| Pass the blocking check | make lint |
addopts excludes the integration marker by default and disables the anyio and langsmith_plugin plugins (pyproject.toml:166). With asyncio_mode = "auto", an async def test needs no decorator.
Check
uv run pytest -q must end with no failed and no error. The deselected tests (deselected) are the integration tests.
What make lint checks
make lint modifies nothing and fails at the first problem (Makefile:11-16):
ruff format --check .: formatting.ruff check .: style rules, including mandatory type annotations.pyrefly check src tests examples docs/tools: type checking, on explicit paths.bandit -c pyproject.toml -r src examples: code security.python skills/piighost-docs/scripts/audit.py: audit of the documentation pages.
How the suite is organized
| Folder | Content |
|---|---|
tests/components/ | One subfolder per stage, that is detectors, deny list and allow list, overlaps, expansion, links, resolvers, anonymizer, placeholders, guard rails |
tests/pipeline/ | Simple pipeline, conversation pipeline, human correction, lists built into the pipeline |
tests/conversation_memory/, tests/crypto/ | Storage and encryption |
tests/config/, tests/cli/, tests/test_catalog.py | Configuration, command line, catalog |
tests/integrations/ | LangChain, Pydantic AI, LlamaIndex, Claude Code, HTTP client |
tests/observation/ | OpenTelemetry spans and trace masking |
tests/models/, tests/text/ | Data models, word boundaries, spaces, splitting |
tests/regression/ | Public API and imports without optional dependencies |
tests/skills/test_docs_audit.py | The documentation audit script |
Write a test
- Put the file in the folder of the stage under test, on the model of the neighboring test.
- Use
ExactMatchDetector({"Claire Dubois": "PERSON"})as the detector, so that no model loads. - For a storage backend, use
InMemoryConversationMemory(), orfakeredisfor Redis as intests/conversation_memory/test_redis.py:21-27. - If the test requires an optional dependency, call
pytest.importorskip("module_name"). - If the test loads a real model (torch, gliner2, spacy, transformers), mark it
@pytest.mark.integration. - For a new detector, also add its constructor to
DETECTORSintests/components/detector/test_contract.py.
Check
uv run pytest path/to/test.py -v
make lintWhat GitHub CI runs
| Workflow | Trigger | What it runs |
|---|---|---|
ci.yml, lint job | push and pull request on master and develop, except doc-only changes | uv sync --locked --dev, then make lint (Python 3.13) |
ci.yml, audit job | same | pip-audit on all locked dependencies, with one acknowledged nltk vulnerability |
ci.yml, tests job | after lint and audit | uv run pytest --cov=piighost on Python 3.11, 3.12, 3.13 and 3.14 |
integration.yml | every night at 03:00 UTC, or by hand | uv sync --all-extras --all-groups, spaCy model en_core_web_sm, then pytest -m integration |
A change that touches only .md files, docs/ or LICENSE does not trigger ci.yml (paths-ignore).
What runs nowhere in CI
The dev group installs the extras config, argon2, crypto, redis, mistral, langchain, pydantic-ai, observation, fuzzy, sqlalchemy (pyproject.toml:150-151). It installs neither llama-index, nor gliner2, nor spacy, nor transformers, nor presidio.
- In the
testsjob, the tests that request these libraries throughimportorskipare skipped. - In
integration.yml, these libraries are installed, but-m integrationselects only the tests markedintegration.
The skipped and unmarked tests therefore run in no job. They include:
tests/integrations/llama_index/(both files)- the
gliner2,transformers,presidioandspacyentries oftests/components/detector/test_contract.py tests/components/guard/test_gliner2_guard.pytests/components/detector/ner/test_presidio.py, and the unmarked tests oftest_spacy.pyandtest_transformers.pytests/config/test_presidio_detector.py
To run them locally:
uv sync --all-extras --all-groups
uv run pytest -rs
uv syncThen go back to the default environment with uv sync, and run make lint again. An environment loaded with every extra can hide an import or type error that CI would see.
Pitfalls
- A skipped test looks like a passed one. Run
pytest -rsto see the list and the reason for each skip. - The observation conftest installs a global tracer for the whole session. The clear-text tracing warning is therefore filtered in
pyproject.toml:172-174, and only its own tests check it. pyreflyreceives explicit paths. Without them, it checks nothing in a git worktree and fails anyway (Makefile:8-10).- The Redis tests use
fakeredis. The behavior of a real server or of a cluster is not covered.
See also Add or replace a component for the contract and import tests.