Local pytest invocations, the bench opt-in marker, and the GitHub Actions / SonarCloud wiring.
← README · Decision records: ADR 0004 (why the suite is offline-only), ADR 0005 (the seam modules tests patch), ADR 0010 (the reset entry point fixtures drain)
pytest # full suite (~4-5 min, ~6.6k collected cases)
pytest --cov=codexa --cov-report=term --cov-fail-under=90
HYPOTHESIS_PROFILE=deep pytest $(ls tests/*fuzz*.py tests/security/test_*.py) # deep fuzz
pytest -m bench # opt-in indexer ramp-up benchmark
ruff check codexa tests # lint
pyright codexa tests # static type checkThe default suite is offline-only by design (mirroring the product's no-default-egress invariant, ADR 0004): CODEXA_DISABLE_HF=1 and TOKENIZERS_PARALLELISM=false are set in tests/conftest.py, NLTK calls in metadata.entities are mocked, and SentenceTransformer / Chroma / Ollama / paddleocr / tesseract are all replaced with deterministic stand-ins.
Hypothesis property tests run inside the normal suite with the ci
profile (deadline=None, cheap example budget). The current suite shape is
roughly 5.5k def test_* functions plus 180+ @given properties; pytest's
expanded collection is about 6.6k executed cases. For adversarial sweeps, run
HYPOTHESIS_PROFILE=deep pytest ...; the scheduled/manual
.github/workflows/deep-fuzz.yml lane applies that profile without slowing
every pull request. That lane discovers its targets at run time — every
tests/**/*fuzz*.py plus tests/security/test_*.py — so a new fuzz file is
picked up automatically (no YAML edit needed). The ls glob above mirrors
that discovery for a local run.
The bench marker is deselected by default. Tests under it spawn the real indexer subprocess against BENCH_CORPUS_DIR (default /path/to/corpus) and measure throughput + system pressure; they are the test-runner counterpart of scripts/bench_indexer.py. See tests/test_bench_indexer.py for the full env-var override list.
⚠️ Do not run twopytestprocesses in parallel against this repo. Each pulls in chromadb + pymupdf + sentence-transformers and a second concurrent run can OOM the host.
- Phase regression tests — every shipped phase lands one regression file under
tests/test_phase{N}_{slug}_{YYYY_MM_DD}.pycovering the closed TODO row(s). The file's docstring lists the closed IDs + the contract pinned. Phase 85+ examples: ROB-* atomic writes, WIRE-18/19/20/21/23/24/25, PROMPT-5..10, FAITH-5/7+COH-8, PERF-30/31/33/34/35, FAITH-8/9, RETQ-7/8/9/10, COH-9..12+BOT-28/29/30, CACHE-2/3/8/9/12+SCAL-15/16. - AppContext.reset between tests — when a test mutates a wired cache, drain the chain via
AppContext.current().reset()(or the per-cacheclear_<name>_cache()helper) in a fixture teardown so subsequent tests start cold (ADR 0010). - Type ignores on duck-typed test stubs — SimpleNamespace / Mock objects swapped in for
Session/Turn/BM25Searcheretc. carry a# type: ignore[arg-type](or similar) marker; pyright's stub-vs-real-type mismatch is intentional.
GitHub Actions runs on every push and pull request to develop / main. The workflow has five jobs:
- lock —
uv lock --check, so apyproject.tomledit that was never re-locked fails before anything is installed and every later job resolves from the committeduv.lock. - lint —
ruff check codexa testson Python 3.12. - typecheck —
uv run --frozen pyrighton Python 3.12. This is a hard gate; a type regression blocks merge. - test —
pytestwith--cov-fail-under=90on Python 3.12 (XML coverage uploaded as an artifact). The same job then runs the bundled repo-doc retrieval quality gate:codexa eval docs-baseline --save … --min-recall 0.5 --min-ndcg-at-5 0.3 --max-regression 0.02against the repo docs (deterministic bundled cases, no operator gold file needed) and uploads theretrieval-quality-baselinesummary artifact. Fixed floors catch absolute quality drops;--max-regressioncompares against the committed.codexa-quality/retrieval_baseline_summary.json. The bundled relevance lists are explicitly open (closed_gold: false), so precision is neither published nor gated. A second, optional retrieval quality eval gate runs only when an operator copiestests/data/eval_qrel.example.ymltotests/data/eval_qrel.ymland provides a matching indexed corpus (skipped otherwise). Setclosed_gold: trueand add--min-precisiononly whenexpected_sourcesexhaustively judges that corpus; omittedclosed_golddefaults totruefor legacy files. - sonar — SonarQube Cloud analysis (needs
test), ingesting the coverage XML. Skipped on fork PRs (noSONAR_TOKEN).
codexa eval tune-hybrid --cases … is not a CI job — it is an operator-run command. It selects the fusion grid winner by mean NDCG@5, using mean MRR@5 as the tie-breaker; the cutoff stays fixed while rrf_k is swept. Open and closed qrels both contribute ranking metrics; only closed qrels contribute the retained precision diagnostic.
A second workflow, deep-fuzz.yml, runs the Hypothesis Deep Fuzz job on a schedule and on manual dispatch — it is not part of the per-push gate.
Concurrency is set so a new push to a branch cancels any in-flight run, and the same OMP_NUM_THREADS/MKL_NUM_THREADS/TOKENIZERS_PARALLELISM discipline used locally is applied as workflow-level env vars to keep CI minutes predictable. See .github/workflows/ci.yml.
sonar-project.properties configures SonarQube / SonarCloud: production code is codexa/; tests/ and examples/ are declared as test sources but excluded from the main quality analysis so test idioms (long parametrize lists, repeated fixture scaffolding) don't surface as code smells. The coverage XML emitted by the CI pytest run feeds Sonar via sonar.python.coverage.reportPaths.