---
name: env-source-defeats-test-setdefault
description: "паттерн 'set -a && source .env && pytest' экспортирует прод .env ДО python — conftest.py os.environ.setdefault(...) становится no-op, тесты тихо бьют в прод-БД/конфиг"
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 4eea915e-3b47-4cd5-8a0e-bfa29b78a87c
  modified: 2026-08-03T16:27:49.607Z
---

**Правило:** если тесты запускаются через `set -a && source .env && set +a && pytest ...`, любой `os.environ.setdefault("X", "test-value")` в conftest.py/тест-файлах — **no-op**, если `X` уже задан в реальном `.env`. `.env` побеждает всегда, потому что shell экспортирует его в окружение процесса ДО того, как Python вообще стартовал — setdefault просто не успевает первым.

**Почему это опасно:** конкретный кейс — avtero (`~/Services/avtero`, 2026-07-13). `bot/tests/conftest.py` пытался `setdefault("BOT_DB_PATH", ":memory:")`, но реальный `.env` содержит `BOT_DB_PATH=data/bot.db` — все e2e-тесты минимум неделю писали/читали РЕАЛЬНУЮ прод-БД фейковым тест-юзером, вместо изолированной `:memory:`. Второй слой того же паттерна в том же файле: `BOT_ADMIN_USER_IDS` из `.env` не содержал тестового второго админа — юнит-тесты admin_guard проверяли несуществующую конфигурацию месяцами. Баг всплыл не при первом запуске, а когда счётчик (дневной лимит запросов для фейкового юзера) органически перешёл порог — то есть выглядел как «внезапная» новая поломка, хотя корень был всегда. См. [[avtero_oem_provenance]] Changelog 2026-07-13.

**Как чинить:** в conftest.py/тест-файлах использовать **force-assign**, не setdefault, для любой переменной, критичной для изоляции тестов: `os.environ["BOT_DB_PATH"] = ":memory:"` вместо `os.environ.setdefault(...)`. setdefault годится ТОЛЬКО для переменных, которых точно нет в реальном `.env` (иначе сам смысл setdefault — «не трогай, если уже задано» — работает ПРОТИВ теста).

**Как искать похожее в других проектах:** grep `os.environ.setdefault` в любом `conftest.py`/тест-bootstrap рядом с переменными, которые ТАКЖЕ существуют в реальном `.env` проекта (`grep -f <(grep -oP 'setdefault\("\K[A-Z_]+' conftest.py) .env`) — пересечение = кандидат на этот баг.

**Отдельная грабля (независимая, но нашлась в том же расследовании):** `Path(":memory:").resolve()` превращает магическое ключевое слово SQLite в ЛИТЕРАЛЬНОЕ имя файла на диске — даже если ":memory:" реально дошёл до кода, нужен спецкейс (`if db_path == ":memory:": create_engine("sqlite:///:memory:", poolclass=StaticPool, ...)`), иначе получится настоящий файл с именем `:memory:` в рабочей директории (не будет удаляться, будет расти).

**Новый симптом того же бага (2026-08-03, avtero, [[avtero_stale_mock_and_provenance_retry_gap]]-follow-up #195):** на этот раз не тихая порча данных, а **зависание на минуты, почти 0% CPU**. `tests/conftest.py` строка ~24-27 делает `os.environ.setdefault("NVIDIA_BASE_URL", "http://127.0.0.1:65534/v1")` (заведомо недоступный порт — тест обязан упасть быстро, если мок не сработал). После `set -a && source .env && set +a` эти setdefault — no-op, реальные `NVIDIA_BASE_URL`/`AI_GATEWAY_URL` из прод `.env` побеждают, и любой путь в тесте, который мок не перехватил, реально стучится в боевой провайдер — зависает на его таймауте (минуты), а не падает мгновенно на connection refused от localhost:65534. Ложно читается как «мой новый код повесил тест» — на самом деле код был в порядке (изолированный прогон одного теста через `-k` прошёл за 0.25с), вис файл целиком именно из-за `source .env`.

**Как правильно гонять тесты в avtero, если один файл требует конкретную переменную** (например `test_learning_corrections.py` требует `TELEGRAM_BOT_TOKEN` для импорта `bot.config`) — НЕ `source .env` целиком, а задать точечно:
```bash
TELEGRAM_BOT_TOKEN=test-token-123:ABC ~/Services/avtero-venv/bin/python3 -m pytest tests/ -q --ignore=tests/e2e
```
Даёт полный зелёный suite (1151/1151) за ~5 секунд вместо 636+ секунд (реальные сетевые попытки/таймауты) при `source .env`, и без риска зависания. Правило: **никогда не `source .env` перед pytest в этом проекте** — только `VAR=fake pytest ...` для той единственной переменной, которой не хватает.
