Testing
Sonality follows a testing philosophy that prioritizes meaningful behavior verification over coverage metrics. Each test earns its place by catching real bugs or verifying invariants that the type system cannot guarantee.
Test Architecture
tests/
├── conftest.py # Environment defaults for isolation
├── shared/llm/test_parse.py # LLM output parsing (49 tests)
├── sonality/test_api.py # API contract verification
├── sonality/memory/test_derivatives.py # Chunk normalization + fallback
└── fathom/test_models.py # Pydantic coercion edge cases
Test Categories
LLM Output Parsing (49 tests)
The largest test suite covers the output normalization pipeline in shared/llm/parse.py. These tests verify handling of:
- Thinking removal —
<think>blocks, reasoning fences, XML tags from chain-of-thought models - JSON extraction — Markdown fences, buried JSON in prose, escaped quote handling
- Tool call parsing — Standard format, malformed calls, missing parameters
- Response sanitization — Fake XML injection, leaked system prompts, bare tool call artifacts
- Pydantic normalization — List wrapping for single items, field type coercion
These tests exist because quantized local models produce non-standard output. Each test case represents a real artifact encountered in production with models ranging from 2-bit to 8-bit quantization.
API Contract (test_api.py)
Verifies the FastAPI server behavior:
/v1/chat/completionsendpoint contract (request/response shapes)/healthendpoint availability/ingestendpoint for external content feeding- Error handling (invalid requests, missing fields)
Derivative Normalization (test_derivatives.py)
Tests the LLM semantic chunking pipeline:
- Chunk count within bounds (1–15 per episode)
- Handling of empty/minimal input
- Fallback behavior when LLM produces invalid chunk structures
Model Coercion (test_models.py)
Tests Fathom's Pydantic model resilience:
Checklistitem parsing from various LLM output formatsFactextraction with partial/missing fields- Type coercion for numeric fields returned as strings
Running Tests
make test # All tests (excludes live API tests)
make test-report # Tests with JSON/XML report output
uv run pytest tests -v # Direct pytest invocation
Live Tests
Tests requiring external API access are marked live and excluded from default runs:
Testing Philosophy
Integration over mocks. Tests use real Pydantic models, real parsing functions, and real normalization pipelines. Mocking is used only for external I/O boundaries (HTTP calls, database connections).
Edge cases over happy paths. The type system already guarantees happy-path behavior for most operations. Tests focus on boundary conditions: malformed input, partial failures, quantization artifacts.
Fewer tests, more value. Each test must justify its existence by catching a category of bugs. Tests that verify obvious behavior or implementation details are removed.
Reproducibility. All tests run without external dependencies (no API keys, no databases, no network). Environment defaults in conftest.py ensure isolation.
Test Configuration
From pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v --tb=short -m 'not live'"
timeout = 1800
The 1800-second timeout accommodates slow CI environments. Local runs typically complete in under 30 seconds.
Adding Tests
When adding tests:
- Place tests in the appropriate subdirectory matching the source module structure
- Use the
livemarker for anything requiring external services - Prefer testing at module boundaries rather than internal implementation
- Each test should fail if the behavior it tests is broken, and pass otherwise
- Name test functions to describe the specific behavior being verified