feat(plane): осмысленная статусная модель Plane (слой B — индикация)

Приводит статусы доски Plane к смыслу стадий конвейера, сохраняя
инвариант «статус — индикация, а не управление». Меняется только слой B
(отображение: src/plane_sync.py + точки выставления статуса в
stage_engine.py/webhooks/plane.py/reconciler.py); слой A — машина стадий
src/stages.py::STAGE_TRANSITIONS — остаётся байт-в-байт неизменным (AC-21).

- 6 новых логических ключей статуса (to_analyse, analysis, code_review,
  awaiting_deploy, deploying, monitoring) + сеттеры и диспетчер
  set_issue_stage_state.
- Project-relative alias-fallback (BR-12): новый ключ деградирует на
  базовый UUID того же проекта → нулевая регрессия для enduro-trails.
- Самодеплой (ORCH-036) индицирует фазы: Awaiting Deploy / Deploying;
  terminal-sync для self-hosting → Monitoring after Deploy, для прочих →
  терминальный Done.
- Post-deploy монитор (ORCH-021): HEALTHY → Done, DEGRADED → Blocked
  (только индикация; self-hosting ALERT_ONLY, прод не трогается, BR-5).
- Reconciler: триггер старта/резюма на To Analyse; Guard 2 учитывает
  новые активные ожидания без расширения skip-set на алиасах.
- never-raise контракт сеттеров и резолвера состояний сохранён.
- Раскатка — созданием статусов в Plane оператором, без kill-switch.

Инварианты не менялись: STAGE_TRANSITIONS, QG_CHECKS (12 чеков),
check_deploy_status, exit-код-контракт хука, merge-gate, схема БД.

ADR: docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md
Тесты: test_plane_status_model, test_plane_to_analyse_resume,
test_plane_status_failclosed + TC в существующих наборах. 774 passed.

Refs: ORCH-066

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 21:54:12 +00:00
committed by deployer
parent 22d3b77426
commit 0dfddf93f0
19 changed files with 999 additions and 40 deletions

View File

@@ -47,6 +47,9 @@ def silence_side_effects(monkeypatch):
"send_telegram", "plane_notify_stage", "plane_notify_qg", "plane_add_comment",
"set_issue_in_review", "set_issue_needs_input", "set_issue_in_progress",
"set_issue_blocked", "set_issue_done",
# ORCH-066 status setters.
"set_issue_analysis", "set_issue_awaiting_deploy", "set_issue_deploying",
"set_issue_monitoring",
):
monkeypatch.setattr(stage_engine, name, MagicMock())
@@ -242,6 +245,81 @@ def test_finished_window_tick_is_noop(monkeypatch):
probe.assert_not_called()
# ---------------------------------------------------------------------------
# ORCH-066 TC-10 (AC-10): HEALTHY + window exhausted -> Plane state Done.
# ---------------------------------------------------------------------------
def test_orch066_tc10_clean_window_close_sets_done(monkeypatch):
monkeypatch.setattr(post_deploy.settings, "post_deploy_monitor_enabled", True)
monkeypatch.setattr(post_deploy.settings, "post_deploy_window_s", 30)
monkeypatch.setattr(post_deploy.settings, "post_deploy_interval_s", 30) # budget=1
monkeypatch.setattr(
post_deploy, "probe_signals",
lambda url: post_deploy.ProbeResult(True, 2, 0, "ok"),
)
task_id = _make_task("done")
post_deploy.write_marker("orchestrator", "ORCH-021", post_deploy.ARMED, "armed")
stage_engine.run_post_deploy_monitor(
{"task_id": task_id, "repo": "orchestrator", "id": 1, "agent": "post-deploy-monitor"}
)
# Clean window close -> terminal Done indicated on Plane; window marked done.
stage_engine.set_issue_done.assert_called_once_with("ORCH-021")
stage_engine.set_issue_blocked.assert_not_called()
assert post_deploy.has_marker("orchestrator", "ORCH-021", post_deploy.DONE)
# No follow-up tick once the window closed.
assert _jobs("post-deploy-monitor") == []
# ---------------------------------------------------------------------------
# ORCH-066 TC-11 (AC-11): DEGRADED -> Plane state Blocked (self-hosting alert).
# ---------------------------------------------------------------------------
def test_orch066_tc11_degraded_sets_blocked(monkeypatch):
monkeypatch.setattr(post_deploy.settings, "post_deploy_monitor_enabled", True)
monkeypatch.setattr(post_deploy.settings, "post_deploy_fail_threshold", 1)
monkeypatch.setattr(post_deploy.settings, "post_deploy_window_s", 30)
monkeypatch.setattr(post_deploy.settings, "post_deploy_interval_s", 30)
monkeypatch.setattr(
post_deploy, "probe_signals",
lambda url: post_deploy.ProbeResult(False, 2, 2, "down"),
)
monkeypatch.setattr(stage_engine, "_notify_post_deploy", MagicMock())
task_id = _make_task("done")
post_deploy.write_marker("orchestrator", "ORCH-021", post_deploy.ARMED, "armed")
stage_engine.run_post_deploy_monitor(
{"task_id": task_id, "repo": "orchestrator", "id": 1, "agent": "post-deploy-monitor"}
)
# DEGRADED -> Blocked indication (NOT Done); window finalised.
stage_engine.set_issue_blocked.assert_called_once_with("ORCH-021")
stage_engine.set_issue_done.assert_not_called()
assert post_deploy.has_marker("orchestrator", "ORCH-021", post_deploy.DONE)
# ---------------------------------------------------------------------------
# ORCH-066 TC-12 (AC-12): a self-hosting tick NEVER restarts/rolls back prod —
# the Blocked indication is the ONLY mutation (ORCH-021 BR-5 preserved).
# ---------------------------------------------------------------------------
def test_orch066_tc12_self_tick_never_restarts_prod(monkeypatch):
monkeypatch.setattr(post_deploy.settings, "post_deploy_monitor_enabled", True)
monkeypatch.setattr(post_deploy.settings, "post_deploy_auto_rollback", True)
monkeypatch.setattr(post_deploy.settings, "post_deploy_fail_threshold", 1)
monkeypatch.setattr(post_deploy.settings, "post_deploy_window_s", 30)
monkeypatch.setattr(post_deploy.settings, "post_deploy_interval_s", 30)
monkeypatch.setattr(
post_deploy, "probe_signals",
lambda url: post_deploy.ProbeResult(False, 2, 2, "down"),
)
monkeypatch.setattr(stage_engine, "_notify_post_deploy", MagicMock())
# The rollback hook (the only restart-capable path) MUST stay untouched for self.
rollback = MagicMock(return_value=(0, "ok"))
monkeypatch.setattr(post_deploy, "run_rollback", rollback)
task_id = _make_task("done")
post_deploy.write_marker("orchestrator", "ORCH-021", post_deploy.ARMED, "armed")
stage_engine.run_post_deploy_monitor(
{"task_id": task_id, "repo": "orchestrator", "id": 1, "agent": "post-deploy-monitor"}
)
rollback.assert_not_called() # never restarts/rolls back the prod self-container
stage_engine.set_issue_blocked.assert_called_once_with("ORCH-021") # indication only
# ---------------------------------------------------------------------------
# TC-20 — /queue observability block
# ---------------------------------------------------------------------------