"""ORCH-066 fail-closed (CRITICAL) — the new status model must never wedge the pipeline when the 6 Plane statuses are absent or Plane is unreachable. * TC-16 (AC-16, BR-12) — a project WITHOUT the new statuses resolves each new logical key to its OWN base UUID (to_analyse=in_progress, code_review=review, awaiting_deploy=in_review, monitoring=done); no exception. * TC-17 (AC-16) — Plane API down -> get_project_states falls back to _DEFAULT_STATES; every set_issue_* helper is never-raise. * TC-18 (AC-17) — enduro In Progress STILL starts the pipeline through the to_analyse alias (= in_progress UUID). httpx is mocked; no network. """ import os os.environ.setdefault("ORCH_PLANE_API_URL", "http://plane.local") os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_PLANE_WORKSPACE_SLUG", "test-ws") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") from unittest.mock import patch, MagicMock, AsyncMock # noqa: E402 import pytest # noqa: E402 from src import plane_sync as PS # noqa: E402 ENDURO_PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c" # An enduro-style states response: the 6 ORCH-066 statuses are NOT created. _ENDURO_BASE = { "Backlog": "backlog-u", "Todo": "todo-u", "In Progress": "ip-u", "Review": "review-u", "In Review": "inrev-u", "Approved": "appr-u", "Rejected": "rej-u", "Done": "done-u", "Needs Input": "ni-u", "Blocked": "blk-u", } def _states_response(name_to_uuid): return {"results": [{"id": uid, "name": name} for name, uid in name_to_uuid.items()]} def _fake_resp(data, status=200): m = MagicMock() m.status_code = status m.json.return_value = data m.raise_for_status.return_value = None return m @pytest.fixture(autouse=True) def _reset_cache(): PS.reload_project_states() yield PS.reload_project_states() # --------------------------------------------------------------------------- # TC-16 (AC-16 / BR-12): partial project -> alias to its own base UUIDs, no raise. # --------------------------------------------------------------------------- def test_tc16_partial_project_aliases_to_base_uuids(): with patch("src.plane_sync.httpx.get") as mock_get: mock_get.return_value = _fake_resp(_states_response(_ENDURO_BASE)) states = PS.get_project_states(ENDURO_PROJECT_ID) # The new keys degrade to THIS project's base UUIDs (not foreign defaults). assert states["to_analyse"] == states["in_progress"] == "ip-u" assert states["analysis"] == "ip-u" assert states["code_review"] == states["review"] == "review-u" assert states["awaiting_deploy"] == states["in_review"] == "inrev-u" assert states["deploying"] == "ip-u" assert states["monitoring"] == states["done"] == "done-u" # --------------------------------------------------------------------------- # TC-17 (AC-16): Plane API down -> _DEFAULT_STATES; set_issue_* never-raise. # --------------------------------------------------------------------------- def test_tc17_api_down_falls_back_to_defaults(): with patch("src.plane_sync.httpx.get", side_effect=Exception("plane down")): states = PS.get_project_states(ENDURO_PROJECT_ID) assert states is PS._DEFAULT_STATES # All new keys exist in the defaults (so callers never KeyError). for k in ("to_analyse", "analysis", "code_review", "awaiting_deploy", "deploying", "monitoring"): assert k in states def test_tc17_set_issue_helpers_never_raise_when_issue_missing(): # find_issue_id returns None (issue not in Plane) -> helpers log + return, # they must NOT raise. Covers every ORCH-066 setter. setters = [ PS.set_issue_analysis, PS.set_issue_code_review, PS.set_issue_awaiting_deploy, PS.set_issue_deploying, PS.set_issue_monitoring, ] with patch("src.plane_sync._resolve_project_id", return_value="proj-1"), \ patch("src.plane_sync.get_project_states", return_value=PS._DEFAULT_STATES), \ patch("src.plane_sync.find_issue_id", return_value=None), \ patch("src.plane_sync.httpx.patch") as mock_patch: for setter in setters: setter("ET-1") # must not raise # No PATCH issued because the issue could not be resolved. mock_patch.assert_not_called() def test_tc17_set_issue_helpers_never_raise_when_patch_errors(): # The PATCH itself blows up -> _set_issue_state_direct swallows it. with patch("src.plane_sync._resolve_project_id", return_value="proj-1"), \ patch("src.plane_sync.get_project_states", return_value=PS._DEFAULT_STATES), \ patch("src.plane_sync.find_issue_id", return_value="issue-uuid"), \ patch("src.plane_sync.httpx.patch", side_effect=Exception("boom")): PS.set_issue_monitoring("ET-1") # must not raise # --------------------------------------------------------------------------- # TC-18 (AC-17): enduro In Progress still starts the pipeline via to_analyse alias. # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc18_enduro_in_progress_still_starts_via_alias(): from src.webhooks.plane import handle_issue_updated with patch("src.plane_sync.httpx.get") as mock_get, \ patch("src.webhooks.plane.handle_status_start", new_callable=AsyncMock) as mock_start, \ patch("src.webhooks.plane.handle_verdict", new_callable=AsyncMock) as mock_verdict: mock_get.return_value = _fake_resp(_states_response(_ENDURO_BASE)) # enduro never created 'To Analyse' -> to_analyse aliases In Progress (ip-u). data = {"id": "et-issue", "state": {"id": "ip-u", "name": "In Progress"}} await handle_issue_updated(data, ENDURO_PROJECT_ID) mock_start.assert_called_once() mock_verdict.assert_not_called()