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:
@@ -47,13 +47,18 @@ UNKNOWN_PLANE_ID = "deadbeef-0000-0000-0000-000000000000"
|
||||
_PROJECT_STATES = {
|
||||
ENDURO_PLANE_ID: {
|
||||
"in_progress": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||
# ORCH-066: To Analyse is the start trigger; absent -> aliases in_progress.
|
||||
"to_analyse": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||
"approved": "a519a341-dada-4a91-8910-7604f82b79c5",
|
||||
"rejected": "ba958f3c-5db5-461d-8f82-89425e413b97",
|
||||
"cancelled": "b1cae7f9-961d-4889-a179-f3acea697d17",
|
||||
},
|
||||
ORCH_PLANE_ID: {
|
||||
"in_progress": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
|
||||
"to_analyse": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
|
||||
"approved": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
|
||||
"rejected": "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
|
||||
"cancelled": "59d1d210-8e3a-4a83-930a-cbc5dbf6ad85",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -219,3 +224,38 @@ def test_prefixes_independent_per_project(mock_branch, mock_docs, mock_launcher)
|
||||
assert rows["o1"] == "ORCH-001"
|
||||
assert rows["o2"] == "ORCH-002"
|
||||
assert rows["e1"] == "ET-001"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ORCH-066 TC-15 (AC-15): Cancelled is a valid human exit — the orchestrator
|
||||
# performs NO advance/rollback (indication, not control).
|
||||
# ---------------------------------------------------------------------------
|
||||
@patch("src.webhooks.plane.handle_verdict", new_callable=AsyncMock)
|
||||
@patch("src.webhooks.plane.handle_status_start", new_callable=AsyncMock)
|
||||
@patch("src.webhooks.plane.launcher")
|
||||
def test_cancelled_state_does_no_pipeline_action(mock_launcher, mock_start, mock_verdict):
|
||||
cancelled = _PROJECT_STATES[ORCH_PLANE_ID]["cancelled"]
|
||||
resp = client.post(
|
||||
"/webhook/plane",
|
||||
json={
|
||||
"event": "issue",
|
||||
"action": "updated",
|
||||
"data": {
|
||||
"id": "cancel-1",
|
||||
"name": "A cancelled work item",
|
||||
"description_stripped": "This is a sufficiently long description.",
|
||||
"project": ORCH_PLANE_ID,
|
||||
"state": {"id": cancelled, "name": "Cancelled", "group": "cancelled"},
|
||||
},
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
# Neither the start nor the verdict (advance/rollback) handler ran.
|
||||
mock_start.assert_not_called()
|
||||
mock_verdict.assert_not_called()
|
||||
mock_launcher.launch.assert_not_called()
|
||||
# No task created off a Cancelled transition.
|
||||
conn = get_db()
|
||||
task = conn.execute("SELECT * FROM tasks WHERE plane_id='cancel-1'").fetchone()
|
||||
conn.close()
|
||||
assert task is None
|
||||
|
||||
Reference in New Issue
Block a user