work_item: ORCH-088 title: "Serial gate (Этап 1: пакетный автономный режим, serial e2e)" scope: "FR-1..FR-5 only. Merge-queue / pre-merge rebase / phases A/B/C / ORCH-83 — out of scope." framework: pytest # Принципы тестирования: # - чистую логику gate/freeze покрываем unit-тестами на leaf-функциях (без сети/БД где можно); # - claim-gate и e2e-последовательность — integration на временной SQLite-БД; # - все тесты детерминированы (без реальных Plane/Gitea/прод вызовов — мокируются); # - проверяем оба направления kill-switch (вкл/выкл) и never-raise. tests: # ---------- FR-1 / AC-1: gate закрыт при активной задаче ---------- - id: TC-01 type: unit description: "claim_next_job НЕ выбирает analyst-job новой задачи B, если в репо есть задача A со stage!='done' (gate закрыт)" module: tests/test_serial_gate.py expected: PASS - id: TC-02 type: unit description: "serial_gate_applies(repo): enabled + пустой CSV → True для зарегистрированного репо; CSV с членством → True; репо вне CSV → False" module: tests/test_serial_gate.py expected: PASS - id: TC-03 type: unit description: "Job'ы УЖЕ активной задачи (architect/developer/.../deployer) gate'ом НЕ блокируются — единственная активная задача свободно идёт по конвейеру" module: tests/test_serial_gate.py expected: PASS # ---------- FR-1/2 / AC-2: автостарт следующей по достижении done ---------- - id: TC-04 type: integration description: "После перевода A.stage='done' claim_next_job выбирает analyst-job ожидающей B того же репо (gate открылся автоматически)" module: tests/test_serial_gate_e2e.py expected: PASS - id: TC-05 type: integration description: "Очередь из 3 задач одного репо обрабатывается строго по одной: пока A не done, ни B, ни C не стартуют; порядок FIFO по jobs.id" module: tests/test_serial_gate_e2e.py expected: PASS # ---------- FR-4 / AC-3: restart-safe ---------- - id: TC-06 type: integration description: "Активная задача определяется из БД (tasks.repo + stage!='done'), не из in-memory — после пересоздания воркера/состояния gate остаётся закрытым при A.stage истинно (на временном git-репо)" module: tests/test_serial_gate_branch.py expected: PASS # ---------- AC-7: kill-switch / нулевая регрессия ---------- - id: TC-15 type: unit description: "serial_gate_enabled=False → claim_next_job SQL/поведение идентичны исходным (gate инертен); B стартует независимо от A" module: tests/test_serial_gate.py expected: PASS - id: TC-16 type: unit description: "Репо вне serial_gate_repos (CSV непуст) → gate не применяется к этому репо" module: tests/test_serial_gate.py expected: PASS # ---------- AC-8 / AC-9: never-raise ---------- - id: TC-17 type: unit description: "Ошибка БД при вычислении gate в claim → перехвачена, залогирована, claim не падает (fail-OPEN: claim продолжается)" module: tests/test_serial_gate.py expected: PASS - id: TC-18 type: unit description: "Ошибка при определении freeze → fail-CLOSED: следующая не стартует при невозможности подтвердить отсутствие freeze" module: tests/test_serial_gate_freeze.py expected: PASS # ---------- AC-10: наблюдаемость ---------- - id: TC-19 type: unit description: "serial_gate snapshot() возвращает {enabled, repos, per-repo active_task, waiting, frozen}; never-raise при ошибке → минимальный словарь" module: tests/test_serial_gate.py expected: PASS - id: TC-20 type: integration description: "GET /queue содержит аддитивный блок serial_gate и НЕ меняет существующие ключи (counts/max_concurrency/reconcile/reaper/post_deploy/task_deps/recent)" module: tests/test_queue_endpoint.py expected: PASS # ---------- AC-11: инварианты ---------- - id: TC-21 type: unit description: "STAGE_TRANSITIONS и реестр QG_CHECKS не изменены (снимок ключей совпадает с эталоном); новых QG-проверок нет" module: tests/test_serial_gate.py expected: PASS - id: TC-22 type: unit description: "Миграция freeze-хранилища идемпотентна: повторный вызов init_db/_ensure не падает и не дублирует структуру" module: tests/test_serial_gate_freeze.py expected: PASS