Root cause: LLM provider selection reads three process-global surfaces —
env vars (LLM_DEFAULT_PROVIDER, per-provider *_API_KEY/*_BASE_URL,
TRANSLATE_*), the SQLite settings store (llm.active_provider & co.), and
prefs.json (llm_backend). Importing `main` (TestClient fixtures do)
dotenv-loads the developer's .env and ~/.config/omnivoice/env straight
into os.environ, and several tests/endpoints mutate these surfaces
without teardown — so whichever test imported the app first flipped what
later tests' active_backend_id()/active_provider_id() resolved to
(order-dependent failures in test_engines.py,
test_llm_endpoint_settings.py, test_llm_providers.py).
Fix the class, not the instances:
- tests/conftest.py: redirect OMNIVOICE_DATA_DIR to a per-session tmp dir
and OMNIVOICE_ENV_FILE into it (before collection freezes
core.config.DATA_DIR), so tests never read or write the developer's
real app state and local runs behave like clean CI.
- tests/conftest.py: autouse `_isolate_llm_provider_state` fixture
snapshots env (derived from llm_providers._PROVIDERS, so new providers
are guarded automatically), llm.* / secret.llm_key.* settings rows, and
the prefs llm_backend/env.TRANSLATE* keys before every test and
restores them exactly afterwards.
- shared `clean_llm_env` fixture clears the FULL provider env surface;
the four LLM test modules' hand-picked partial delenv lists (which left
e.g. LLM_DEFAULT_PROVIDER / OPENROUTER_API_KEY standing) now use it.
- tests/test_llm_state_isolation.py: deterministic fail-before/pass-after
regression pair — pollutes all three surfaces without cleanup, then
asserts the guard restored them.
Verified: the issue's two-test repro passes; the five LLM-related test
files pass in order; full suite green (2046 passed, 20 skipped,
10 xfailed, 4 xpassed).
Fixes#878
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>