work_item: ORCH-062 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-09 model_used: claude-opus-4-8 title: "Авто-prune docker build cache на mva154 — план тестов" framework: pytest scope: > Покрывает code-путь (Вариант A — heartbeat-демон src/build_cache_pruner.py по образцу src/disk_watchdog.py): чистая decision-логика (надо ли убирать на этом тике), построение безопасной docker-команды с политикой удержания, never-raise на ошибках subprocess/таймаут/ недоступность docker.sock, kill-switch (демон не стартует), наблюдаемость status()/GET /queue, интеграция в lifespan. ВНЕ покрытия pytest: реальный вызов docker (subprocess мокается — тесты не должны трогать настоящий docker daemon), реальное освобождение диска. Если архитектор выберет чистый инфра-путь (B daemon.json / C cron) без кода src/**, применимые TC сводятся к ручной host-верификации, описанной в 07-infra-requirements.md / INFRA.md (см. TC-10). notes: > docker-вызовы изолируются моками (monkeypatch subprocess.run / docker-клиента) — НИ ОДИН тест не выполняет настоящий `docker builder prune`. Время/период инъектируются (now_provider), как в тестах disk_watchdog. Полный регресс `pytest tests/ -q` остаётся зелёным; STAGE_TRANSITIONS / QG_CHECKS / схема БД не затрагиваются — отдельных гейт-тестов фича не добавляет. tests: - id: TC-01 type: unit description: "decide-функция: при включённом pruner и истёкшем периоде с прошлой уборки решение = PRUNE" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-02 type: unit description: "decide-функция: период с прошлой уборки не истёк → решение = SKIP (анти-частота, NFR-4)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-03 type: unit description: "Построение docker-команды несёт возрастной фильтр удержания (until=) и НЕ содержит image/system prune (FR-2/FR-3/AC-2/AC-3)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-04 type: unit description: "never-raise: subprocess бросает исключение / возвращает ненулевой rc → тик не падает, ошибка залогирована (FR-6/AC-4)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-05 type: unit description: "never-raise: недоступность docker.sock (FileNotFoundError/PermissionError) → тик пропускается, цикл жив (FR-6/AC-4)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-06 type: unit description: "never-raise: таймаут docker-команды (TimeoutExpired) проглатывается, фоновый цикл продолжает работу (FR-6/AC-4)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-07 type: unit description: "kill-switch: при *_enabled=False start() — no-op, фоновый поток не стартует (FR-5/AC-5/NFR-3)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-08 type: unit description: "config: невалидный *_interval_s / retention → лог-warning + безопасный дефолт, старт не падает (FR-5/AC-6)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-09 type: unit description: "status() never-raise и содержит enabled/interval_s/retention/last_run_ts + best-effort результат последней уборки (FR-4/AC-7)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-10 type: unit description: "Изоляция от Quality Gate: модуль-pruner — leaf, не импортирует stage_engine/stages/qg; STAGE_TRANSITIONS и QG_CHECKS не изменены (NFR-2/AC-8)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-11 type: integration description: "lifespan: при включённом флаге демон стартует в app-lifespan и корректно останавливается на shutdown (рядом с disk_watchdog), docker замокан (FR-1/AC-1)" module: tests/test_build_cache_pruner.py expected: PASS - id: TC-12 type: integration description: "GET /queue содержит read-only блок авто-prune с состоянием (enabled/interval_s/retention/last_run_ts); при выключенном флаге enabled=false (FR-4/AC-5/AC-7)" module: tests/test_build_cache_pruner.py expected: PASS