Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде, отсутствие ретраев у Plane/Gitea, неразрезолвленный sha→branch) оставляет задачу молча застрявшей (класс инцидента ORCH-044). Новый фоновый daemon-поток src/reconciler.py (паттерн queue_worker) доигрывает пропущенный переход через те же штатные гейты/обработчики, что и webhook: - F-1 gate-side: для задач stage≠done, без активного job и age(updated_at) ≥ grace_for_stage(stage) — read-only пред-оценка канонического QG; зелёный → stage_engine.advance_stage(..., finished_agent=None); красный → тишина (спам нотификаций структурно невозможен). analysis F-1 не трогает (человеческий гейт). - F-2 plane-side: опрос Plane API per-project (plane_sync.list_issues_by_state, курсорная пагинация, never-raise) → реплей In Progress/Approved/Rejected через существующие handle_status_start/handle_verdict (async из sync-потока, asyncio.run). - F-3: усиление sha→branch в handle_ci_status — БД-fallback по единственной development-задаче repo (неоднозначность → не резолвим), debug→info. - Анти-дубль на создании (db.create_task_atomic под process-wide Lock): гонка reconcile↔webhook не плодит второй task/branch/worktree/analyst-job (AC-4). - F-4 observability: лог-строка разблокировки + Telegram + блок reconcile в /queue. Старт/стоп в main.lifespan (после worker.start() / перед worker.stop()), restart-safe, never-raise на единицу работы. Kill-switches ORCH_RECONCILE_ENABLED / ORCH_RECONCILE_PLANE_ENABLED + grace-настройки. Схема БД и реестры STAGE_TRANSITIONS/QG_CHECKS не менялись. Тесты: test_reconciler.py, test_reconciler_plane.py, test_gitea_sha_resolve.py, test_config.py (33 новых, 563 всего зелёные). Документация обновлена (golden source): architecture/README.md, INFRA.md, README.md, CHANGELOG.md, adr-0007 → accepted. Refs: ORCH-053 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
78 lines
3.9 KiB
Python
78 lines
3.9 KiB
Python
"""Global pytest fixtures.
|
|
|
|
test(conftest): mute Telegram in ALL tests to stop prod leakage.
|
|
|
|
Background: a pytest run on prod was sending REAL Telegram messages to Slava,
|
|
because some tests (e.g. test_webhook_dedup advancing a stage) reach
|
|
notify_stage_change -> send_telegram, which reads the live .env
|
|
telegram_bot_token/chat_id and actually POSTs to Telegram.
|
|
|
|
This autouse fixture stubs send_telegram to a no-op for every test:
|
|
|
|
- "src.notifications.send_telegram" is the SOURCE. All the notify_* helpers in
|
|
notifications.py call the module-global send_telegram, and every other module
|
|
that does a *local* `from .notifications import send_telegram` inside a
|
|
function resolves it live at call time -> covered by patching the source.
|
|
|
|
- "src.stage_engine.send_telegram" is patched too, because stage_engine binds
|
|
send_telegram as a MODULE-LEVEL name (from .notifications import send_telegram
|
|
at import), so a patch of the source alone would not intercept its 3 direct
|
|
calls. webhooks/plane and launcher import it locally inside functions, so the
|
|
source patch already covers them; they are patched defensively with
|
|
raising=False anyway in case that ever changes.
|
|
|
|
raising=False so a module that doesn't (yet) expose the name never breaks setup.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_telegram(monkeypatch):
|
|
_noop = lambda *a, **k: None # noqa: E731
|
|
# Source of truth (covers notifications.notify_* and all local re-imports).
|
|
monkeypatch.setattr("src.notifications.send_telegram", _noop, raising=False)
|
|
# Module-level binding in stage_engine (and defensive coverage elsewhere).
|
|
monkeypatch.setattr("src.stage_engine.send_telegram", _noop, raising=False)
|
|
monkeypatch.setattr("src.webhooks.plane.send_telegram", _noop, raising=False)
|
|
monkeypatch.setattr("src.agents.launcher.send_telegram", _noop, raising=False)
|
|
monkeypatch.setattr("src.queue_worker.send_telegram", _noop, raising=False)
|
|
# ORCH-053: the reconciler binds send_telegram as a MODULE-LEVEL name
|
|
# (from .notifications import send_telegram), so the source patch alone would
|
|
# not intercept its unblock notification — patch it here too.
|
|
monkeypatch.setattr("src.reconciler.send_telegram", _noop, raising=False)
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_webhook_secrets(monkeypatch):
|
|
"""Isolate settings singleton between test files (CI cross-file isolation).
|
|
|
|
settings is a process-wide Pydantic singleton read once at import. Different
|
|
test modules set env variables differently at import-time, so those values leak
|
|
across files when pytest collects them together (as CI does).
|
|
|
|
1. webhook secrets: reset to "" so HMAC is disabled by default. Tests that
|
|
intentionally test the 401 path (test_webhook_dedup.py:268,278) re-apply
|
|
their own monkeypatch AFTER this autouse fixture runs, which overrides the
|
|
reset for the duration of that one test only.
|
|
|
|
2. db_path: reset to the value from ORCH_DB_PATH env var (last written by the
|
|
last imported test module). Without this, test_webhook_dedup.py (imported
|
|
first, alphabetically) seeds settings.db_path = dedup.db, while
|
|
test_webhooks.py's setup_db fixture tries to remove test_orchestrator.db,
|
|
leaving the DB dirty across tests that share a branch name and causing
|
|
get_task_by_repo_branch() to return a stale row with the wrong stage.
|
|
Per-test monkeypatches in test_webhook_dedup.setup_db override this reset.
|
|
"""
|
|
import os
|
|
from src.webhooks import gitea as gitea_mod
|
|
from src.webhooks import plane as plane_mod
|
|
from src import db as db_mod
|
|
monkeypatch.setattr(gitea_mod.settings, "gitea_webhook_secret", "", raising=False)
|
|
monkeypatch.setattr(plane_mod.settings, "plane_webhook_secret", "", raising=False)
|
|
db_path_env = os.environ.get("ORCH_DB_PATH", "")
|
|
if db_path_env:
|
|
monkeypatch.setattr(db_mod.settings, "db_path", db_path_env, raising=False)
|
|
yield
|