Этап 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>
62 lines
2.0 KiB
Python
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"
|