Perspicacité's test suite is organized into three tiers, each with different dependencies and gate conditions.
No external services, no API keys, no running server. Run fast; always run in CI.
uv run pytest tests/unit/ -vUnit tests cover:
- Config parsing and validation (
config/schema.py) - Paper model validation and PaperSource enum
- BibTeX parser edge cases
- Chunking strategy logic
- BM25 scoring
- Snowball deduplication and filter logic
- Provider client mocking
Require at least one live API key. Marked with @pytest.mark.live.
# Run all integration tests (requires keys set in .env)
uv run pytest tests/integration/ -v
# Skip tests requiring live keys (safe for offline dev)
uv run pytest tests/ -m "not live" -vAPI key gates:
| Test group | Required key / condition |
|---|---|
| Embedding tests | OPENAI_API_KEY or a local embedding model |
| LLM synthesis tests | ANTHROPIC_API_KEY, DEEPSEEK_API_KEY, or OPENAI_API_KEY |
| Semantic Scholar fallback | SS_API_KEY (optional; tests run without it but at lower rate limit) |
| arXiv fetch | No key needed (arXiv is open) |
| PMC JATS fetch | No key needed (Europe PMC is open) |
| Zotero tests | ZOTERO_API_KEY + PERSPICACITE_ZOTERO_LIBRARY_ID |
Tests that require keys check for them at the start of the test function and skip with a clear message if the key is not set.
End-to-end harnesses that run against a live server and real databases. These are used to validate that a code change has not broken the live pipeline for a set of representative papers. Not run in CI by default.
# Run the full pipeline audit (requires a running server + Anthropic key)
uv run python tests/audit/run_audit.pyResults are written to tests/audit/results/ as JSON and Markdown.
# Full suite with coverage report
uv run pytest --cov=src/perspicacite --cov-report=term-missing
# Coverage XML for CI upload
uv run pytest --cov=src/perspicacite --cov-report=xml:coverage.xmltests/
unit/
test_config.py # Config parsing + validation
test_models.py # Paper, PaperSource, Author models
test_bibtex.py # BibTeX parser edge cases
test_chunking.py # Chunking strategies
test_screening.py # BM25 + LLM screen logic
test_snowball.py # Citation-graph filter + dedup
test_provenance.py # ProvenanceRecord construction
test_zotero.py # Zotero client mocking
integration/
test_content_pipeline.py # PMC, arXiv, Crossref, Unpaywall live fetchers
test_rag_modes.py # All 6 RAG modes with a live LLM
test_mcp_live.py # MCP tool calls against a running server
test_kb_lifecycle.py # Create → ingest → query → delete
audit/
run_audit.py # Full pipeline audit harness
results/ # Audit output (JSON + Markdown, git-ignored)
CI runs are defined in .github/workflows/ci.yml. The CI matrix:
- Runs
uv run pytest tests/unit/ -m "not live"on every push and PR - Uses Python 3.12
- Does not run integration or audit tests (no API keys in CI)
For unit tests:
- Use
pytestfixtures intests/conftest.pyfor shared mocks (config, AppState, Chroma collection, etc.) - Mock external HTTP calls with
respxorhttpxtest clients - Do not use
asyncio.runin tests — usepytest-asynciowith@pytest.mark.asyncio
For integration tests:
- Use
@pytest.mark.liveto gate on API key availability - Check for the key at the top of the test:
if not os.getenv("OPENAI_API_KEY"): pytest.skip(...) - Keep the test focused: one external service call per test function
- development/contributing.md — setting up the dev environment
- MANUAL_QA.md — manual QA checklist for UI features