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)
Run the Python pre-finish gate in this order from the repository root. Pyright
reads the project configuration, so do not pass positional source paths. Select only
tests that cover the changed behavior. Full, broad-coverage, benchmark, and long
suites run only when explicitly requested. Never start separate pytest commands
concurrently; pytest-xdist owns safe parallelism inside one invocation.
uv run --frozen ruff check codexa tests
uv run --frozen pyright
uv run --frozen python scripts/check_repo_privacy.py
uv run --frozen pytest tests/test_changed_contract.pyPytest uses four xdist workers and module-scoped scheduling by default. Pass -n 0
for a focused test that owns process-global state. Mutation testing does this
automatically. Explicit opt-in suites are:
HYPOTHESIS_PROFILE=deep uv run --frozen pytest $(ls tests/*fuzz*.py tests/security/test_*.py)
uv run --frozen pytest -m bench
uv run --frozen pytest -m long -n 0
uv run --frozen pytest --cov=codexa --cov-report=term --cov-fail-under=90The 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). Suite-size figures are not
hard-coded because parametrization and generated cases make them drift. Recompute
the current inventory from the repository root when a release or audit needs it:
rg -n '^def test_|^[[:space:]]+def test_' tests | wc -l # definitions
rg -n '@given\(' tests | wc -l # property sites
uv run --frozen pytest --collect-only -q # collected casesWhen publishing a count, record the command and commit hash that produced it.
For adversarial sweeps, run
HYPOTHESIS_PROFILE=deep uv run --frozen 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, and persisted store bytes per chunk; they are the test-runner counterpart of scripts/bench_indexer.py. See tests/test_bench_indexer.py for the full env-var override list.
indexer-benchmark.yml runs weekly and on manual dispatch, never on pull requests. It builds a deterministic 120-file Markdown/text/HTML corpus, exercises the real extraction → MiniLM embedding → Chroma store path, and fails when throughput drops below 0.03 files/s or persisted growth exceeds 1,000,000 bytes/chunk. The indexer-benchmark-evidence artifact retains the JSON summary and structured indexer logs for 30 days. Adjust a threshold only from measured runner evidence, and keep the prior artifact with the change rationale.
⚠️ Do not run two separatepytestcommands concurrently against this repo. Each pulls in chromadb + pymupdf + sentence-transformers. Use the configured xdist workers inside one command; force-n 0for stateful tests.
Line coverage only proves that code ran. For each production-logic change, mutate only the executable lines changed in that work item and use the smallest focused tests that exercise them:
uv run --frozen python scripts/run_mutation.py --jobs 1 --lines 58-69 codexa/utils/lru.py tests/test_mutation_lru_2026_08_03.py--lines is a changed-line slice and deliberately skips the whole-module ledger.
The slice must report zero survivors; document any genuinely equivalent mutant in
the focused test instead of widening the test set to unrelated suites. A timeout is
an inconclusive failure, never evidence that a test killed the mutation: every mode
reports it separately and exits nonzero. --check does not compare an inconclusive
run with the ledger, --update-baseline does not write it, and --check-all continues
the remaining modules before failing the overall gate.
For a module already recorded in scripts/mutation_baseline.json, the explicit
whole-module non-regression check is:
uv run --frozen python scripts/run_mutation.py --jobs 1 --check codexa/utils/lru.py tests/test_mutation_lru_2026_08_03.py--check is a survivor-count ratchet: it fails when a recorded module exceeds its
allowed survivors. An unrecorded module passes with an invitation to establish a
reviewed baseline, so this mode is not a universal zero-survivor claim. A bare
whole-module run remains the authoring mode and fails on any survivor.
Use --update-baseline only in a separately scoped whole-module mutation audit to
establish a reviewed first baseline after every survivor is killed or documented as
equivalent, or to lock in a genuine decrease for an existing entry:
uv run --frozen python scripts/run_mutation.py --jobs 1 --update-baseline codexa/utils/lru.py tests/test_mutation_lru_2026_08_03.pyYou must never raise a baseline to make a run pass. Do not create a baseline merely
because a module is new. The scheduled/manual mutation workflow runs
--check-all --jobs 4 in its controlled CI lane; it is not part of the per-push or
local pre-finish gate.
- 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 seven 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. - artifacts — builds twice from clean
git archivesource trees, compares byte-identical wheels and metadata-normalized sdists, rebuilds each wheel from its sdist, and uploads the first pair. It installs the wheel into a fresh venv from locked runtime dependencies, checks every packaged catalog/data/UI resource, and smokescodexa --helppluscodexa demo --no-writeoutside the checkout. The wheel itself drives dependency resolution; the exported lock constrains every resolved runtime dependency and supplies its hashes. - optional-install —
uv sync --frozen --all-extrason Python 3.12, after installing declared native prerequisites, so an unsupported optional package cannot silently break the supported install profile. - 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 — one ordered Python 3.12 job. It first runs
pytestover the default non-longsuite with xdist,--cov-fail-under=90, and XML coverage. It then runs the deterministic repo-doc retrieval harness smoke:codexa eval docs-baseline --save … --min-recall 0.5 --min-ndcg-at-5 0.3 --max-regression 0.02. That model-free check covers bundled-case loading, metric aggregation, fixed floors, and saved-summary diffing; it does not claim to measure production ranking. Last, CI stages seven bundled documents (three repository-root files and four files underdocs/) into the corpus configured by.codexa-quality/ci-retrieval.yml, builds a real MiniLM/Chroma index, and runscodexa eval baselinethrough dense retrieval, persisted corpus BM25, RRF, and MMR. Its fixed floors and--max-regression 0.02ratchet compare against.codexa-quality/real_pipeline_eval_baseline_summary.json, uploaded asproduction-retrieval-baseline. This seven-document gate measures production-path retrieval quality, not 200k-file scale. The main pytest run separately includes synthetic 100,000- and 200,000-file RSS gates that remain fast enough for the default lane:test_scal22_memory_budget_2026_07_20.pycovers the scan/change handoff, andtest_mem21_memory_budget_2026_07_20.pycovers fresh and unchanged current-run ledgers. Those tests generate paths and metadata without indexing 200,000 real documents. The bundled relevance lists are explicitly open (closed_gold: false), so precision is neither published nor gated. Setclosed_gold: trueand add--min-precisionfor an operator corpus only whenexpected_sourcesexhaustively judges that corpus; omittedclosed_golddefaults totruefor legacy files. Answer-generating evals should also label each rowanswerable: trueoranswerable: false; omitted labels stay unknown and do not enter refusal gates. Use--max-false-refusal-ratefor answerable cases and--min-refusal-recallfor unanswerable cases. Raw refusal rate remains diagnostic only because refusing every question is not success. - 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.
Two scheduled/manual workflows stay outside the per-push gate: deep-fuzz.yml runs the Hypothesis Deep Fuzz job, and indexer-benchmark.yml runs the representative production-path index benchmark.
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.