Files
orchestrator/tests/test_queue_endpoint.py
claude-bot ee4773f5b0 feat(serial-gate): per-repo serial gate + deferred branch cut + rollback-freeze (ORCH-088)
Этап 1 (serial e2e) пакетного автономного режима. Новая задача репо не входит
в analysis (analyst-job не выбирается, ветка не режется), пока в репо есть более
ранняя незавершённая задача (FIFO, t2.id < jobs.task_id) ИЛИ репо заморожен.

- src/serial_gate.py — новый leaf (never-raise): build_claim_clause (fail-OPEN),
  is_repo_frozen (fail-CLOSED), set/clear_repo_freeze, serial_gate_applies, snapshot.
- src/db.py — идемпотентная миграция repo_freeze + serial_gate-фрагмент в claim_next_job.
- src/webhooks/plane.py + src/agents/launcher.py — отложенный срез ветки: start_pipeline
  не создаёт Gitea-ветку/docs для применимого репо; релокация в _materialize_deferred_branch
  на момент claim analyst-job (база = свежий origin/main с кодом предшественника, AC-6).
- src/stage_engine.py — post-deploy DEGRADED → durable per-repo freeze + Telegram-алерт.
- src/main.py — блок serial_gate в GET /queue + POST /serial-gate/unfreeze.
- src/config.py — serial_gate_enabled / serial_gate_repos / serial_gate_freeze_enabled.

FIFO-уточнение реализации (FR-2): ADR-001 D1 фиксировал t2.id != jobs.task_id; при !=
пакет одновременно созданных свежих задач взаимно блокировался бы (дедлок). t2.id <
jobs.task_id допускает самую раннюю задачу и сериализует остальные, сохраняя AC-1/R-7.

STAGE_TRANSITIONS / QG_CHECKS / check_* — без изменений. Аддитивно, под kill-switch,
never-raise, restart-safe; при выключенном флаге — нулевая регрессия (enduro не затронут).

Тесты: TC-01..TC-22 (test_serial_gate*.py + test_queue_endpoint.py); полный прогон 1114 зелёных.
Docs: README (serial gate / /queue / API / БД), CLAUDE.md, CHANGELOG.md, .env.example.

Refs: ORCH-088
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 11:24:48 +03:00

62 lines
2.0 KiB
Python

"""ORCH-088 — GET /queue additive serial_gate block (AC-10 / TC-20).
The /queue payload must gain an additive ``serial_gate`` block WITHOUT changing
any pre-existing key (counts/max_concurrency/reconcile/reaper/post_deploy/
task_deps/recent ...).
"""
import os
import tempfile
import pytest
os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_queue_endpoint.db")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
import src.db as db # noqa: E402
from src.db import init_db # noqa: E402
from src import config as cfg # noqa: E402
@pytest.fixture(autouse=True)
def fresh_db(tmp_path, monkeypatch):
dbfile = tmp_path / "q.db"
monkeypatch.setattr(db.settings, "db_path", str(dbfile))
monkeypatch.setattr(cfg.settings, "serial_gate_enabled", True, raising=False)
init_db()
yield
def test_queue_has_serial_gate_block_and_keeps_existing_keys():
import asyncio
from src import main
payload = asyncio.run(main.queue())
# Pre-existing keys are all still present (no contract break).
for key in (
"counts", "max_concurrency", "poll_interval", "resilience", "reconcile",
"reaper", "post_deploy", "merge_verify", "task_deps", "recent",
):
assert key in payload, f"existing /queue key '{key}' must be preserved"
# New additive block.
assert "serial_gate" in payload
sg = payload["serial_gate"]
assert sg["enabled"] is True
assert "repos" in sg and "freeze_enabled" in sg
assert isinstance(sg["per_repo"], dict)
def test_queue_serial_gate_reflects_freeze():
import asyncio
from src import main
from src import serial_gate
serial_gate.set_repo_freeze("orchestrator", "DEGRADED", "ORCH-900")
payload = asyncio.run(main.queue())
per = payload["serial_gate"]["per_repo"]
assert "orchestrator" in per
assert per["orchestrator"]["frozen"] is True
assert per["orchestrator"]["frozen_reason"] == "DEGRADED"