"""ORCH-059 TC-10/11/12: end-to-end routing from a Plane webhook payload through handle_issue_updated into the stage engine, with the host deploy mocked. Contract (AC-2, AC-3, AC-8): * TC-10 — task on `deploy` + webhook "Confirm Deploy" -> initiate_deploy called, `deploy-finalizer` enqueued, `initiated` marker written. * TC-11 — task on `deploy` + webhook "Approved" -> NO prod deploy initiated, the task stays on `deploy` (no rollback, no advance to done). * TC-12 — non-self repo: verdict statuses on `deploy` do not change deploy behaviour (self_deploy_applies == False; the confirm-deploy branch is inert). """ import os import tempfile import pytest _test_db = os.path.join(tempfile.gettempdir(), "test_orch_confirm_e2e.db") os.environ["ORCH_DB_PATH"] = _test_db os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir() os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") from unittest.mock import MagicMock # noqa: E402 import src.db as _db # noqa: E402 from src.db import init_db, get_db # noqa: E402 from src import stage_engine # noqa: E402 from src import self_deploy # noqa: E402 import src.plane_sync as plane_sync # noqa: E402 import src.webhooks.plane as wh # noqa: E402 IN_PROGRESS = "11111111-1111-1111-1111-111111111111" APPROVED = "22222222-2222-2222-2222-222222222222" REJECTED = "33333333-3333-3333-3333-333333333333" CONFIRM = "44444444-4444-4444-4444-444444444444" # ORCH project: Confirm Deploy resolved. enduro-like project: NO confirm_deploy key. _STATES_SELF = { "in_progress": IN_PROGRESS, "approved": APPROVED, "rejected": REJECTED, "confirm_deploy": CONFIRM, } _STATES_NONSELF = { "in_progress": IN_PROGRESS, "approved": APPROVED, "rejected": REJECTED, } @pytest.fixture(autouse=True) def fresh_db(monkeypatch, tmp_path): monkeypatch.setattr(_db.settings, "db_path", _test_db) if os.path.exists(_test_db): os.unlink(_test_db) init_db() monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path)) monkeypatch.setattr(self_deploy.settings, "host_repos_dir", str(tmp_path)) monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True) yield @pytest.fixture(autouse=True) def silence_engine(monkeypatch): for name in ( "notify_stage_change", "notify_qg_failure", "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", ): monkeypatch.setattr(stage_engine, name, MagicMock(), raising=False) def _make_task(stage, repo, branch, wi, plane_id): conn = get_db() cur = conn.execute( "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) " "VALUES (?, ?, ?, ?, ?)", (plane_id, wi, repo, branch, stage), ) task_id = cur.lastrowid conn.commit() conn.close() return task_id def _stage(task_id): conn = get_db() row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone() conn.close() return row[0] def _jobs(): conn = get_db() rows = conn.execute("SELECT agent FROM jobs ORDER BY id").fetchall() conn.close() return [r[0] for r in rows] def _payload(state_uuid, plane_id): return {"id": plane_id, "state": {"id": state_uuid}} # --------------------------------------------------------------------------- # TC-10: E2E Confirm Deploy -> prod deploy initiated # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc10_confirm_deploy_e2e_initiates(monkeypatch): monkeypatch.setattr(plane_sync, "get_project_states", lambda pid: _STATES_SELF) initiate = MagicMock(return_value=(True, "ok")) monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate) task_id = _make_task("deploy", "orchestrator", "feature/ORCH-059-x", "ORCH-059", "plane-ORCH-059") await wh.handle_issue_updated(_payload(CONFIRM, "plane-ORCH-059"), "orch-proj") initiate.assert_called_once() assert "deploy-finalizer" in _jobs() assert self_deploy.has_marker("orchestrator", "ORCH-059", self_deploy.INITIATED) # Verdict comes later via the finalizer — still on `deploy`. assert _stage(task_id) == "deploy" # --------------------------------------------------------------------------- # TC-11: E2E Approved -> no prod deploy, task stays on deploy # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc11_approved_e2e_noop(monkeypatch): monkeypatch.setattr(plane_sync, "get_project_states", lambda pid: _STATES_SELF) initiate = MagicMock(return_value=(True, "ok")) monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate) task_id = _make_task("deploy", "orchestrator", "feature/ORCH-059-x", "ORCH-059", "plane-ORCH-059") await wh.handle_issue_updated(_payload(APPROVED, "plane-ORCH-059"), "orch-proj") initiate.assert_not_called() assert "deploy-finalizer" not in _jobs() assert _stage(task_id) == "deploy" # no rollback, no advance to done assert not self_deploy.has_marker("orchestrator", "ORCH-059", self_deploy.INITIATED) # --------------------------------------------------------------------------- # TC-12: non-self repo -> confirm-deploy branch inert (fail-closed, no key) # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc12_non_self_repo_unaffected(monkeypatch): # Non-self project has no confirm_deploy key at all -> the branch never fires. monkeypatch.setattr(plane_sync, "get_project_states", lambda pid: _STATES_NONSELF) initiate = MagicMock(return_value=(True, "ok")) monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate) # Stub the deploy gate so the legacy non-self path stays deterministic (no # real git/network); its verdict is irrelevant to this test's assertions. monkeypatch.setattr( stage_engine, "QG_CHECKS", {**stage_engine.QG_CHECKS, "check_deploy_status": lambda *a, **k: (True, "ok")}, ) task_id = _make_task("deploy", "enduro-trails", "feature/ET-009-x", "ET-009", "plane-ET-009") # An Approved on a non-self deploy task does not initiate self-deploy logic. await wh.handle_issue_updated(_payload(APPROVED, "plane-ET-009"), "enduro-proj") initiate.assert_not_called() # The (absent) Confirm Deploy status simply maps to no pipeline action. assert self_deploy.self_deploy_applies("enduro-trails") is False