fix(plane): resolve issue states per-project instead of hardcoded enduro UUIDs (ORCH-10)
ORCH-10 root cause: PLANE_STATES was a global dict hardcoding enduro-trails
UUIDs. The webhook comparison only
matched ET UUID (b873d9eb) and silently ignored the ORCH in_progress UUID
(e331bfb3), blocking pipeline start for all orchestrator-project tasks.
Changes:
- src/plane_sync.py:
* Rename PLANE_STATES -> _DEFAULT_STATES (enduro UUIDs kept as safe fallback).
* PLANE_STATES preserved as alias to _DEFAULT_STATES (backward compat).
* Add get_project_states(project_id) -> {logical_key: state_uuid}:
fetches Plane API GET /projects/<id>/states/, maps by state name,
caches per project_id, falls back to _DEFAULT_STATES on API failure.
* Add _STATES_CACHE: dict, reload_project_states(project_id=None).
* Add _PLANE_NAME_TO_KEY mapping and _STAGE_TO_STATE_KEY for clean lookup.
* Add stage_to_state(stage, project_id) using get_project_states().
* update_issue_state() uses stage_to_state() instead of STAGE_TO_STATE dict.
* set_issue_{needs_input,in_review,blocked,done,in_progress,stage_state}()
all resolve state UUID via get_project_states(project_id) instead of
the global PLANE_STATES dict.
- src/webhooks/plane.py:
* handle_issue_updated: import get_project_states, resolve proj_states per
incoming project_id, compare new_state against proj_states["in_progress"],
proj_states["approved"], proj_states["rejected"].
* start_pipeline QG-0 blocked path: use get_project_states(plane_project_id)
instead of PLANE_STATES["blocked"].
- tests/test_orch10_states.py: 23 new tests covering:
* get_project_states returns correct UUIDs for both ET and ORCH projects.
* API failure / empty response / None project_id -> _DEFAULT_STATES fallback.
* Caching and reload_project_states (per-project and full flush).
* stage_to_state() per-project resolution.
* Webhook in_progress triggers pipeline for BOTH b873d9eb (ET) and e331bfb3 (ORCH).
* Webhook approved/rejected routes correctly per project.
* PLANE_STATES alias and _DEFAULT_STATES backward compat.
This commit is contained in:
462
tests/test_orch10_states.py
Normal file
462
tests/test_orch10_states.py
Normal file
@@ -0,0 +1,462 @@
|
||||
"""ORCH-10: per-project Plane state resolution tests.
|
||||
|
||||
Verifies:
|
||||
1. get_project_states(ET_PROJECT_ID) -> enduro-trails UUIDs (backward compat).
|
||||
2. get_project_states(ORCH_PROJECT_ID) -> orchestrator UUIDs.
|
||||
3. get_project_states falls back to _DEFAULT_STATES when the Plane API fails.
|
||||
4. _STATES_CACHE is populated after a successful call and reload_project_states
|
||||
evicts it (per-project and full flush).
|
||||
5. stage_to_state() resolves per-project UUIDs for both projects.
|
||||
6. Webhook handle_issue_updated recognises In Progress for BOTH projects
|
||||
(ORCH-10 critical path: e331bfb3 for ORCH, b873d9eb for ET -> pipeline start).
|
||||
7. Webhook handle_issue_updated recognises Approved/Rejected per project.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimal env so src/config.py can import without a real .env file.
|
||||
# ---------------------------------------------------------------------------
|
||||
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")
|
||||
os.environ.setdefault("ORCH_PLANE_WEBHOOK_SECRET", "")
|
||||
os.environ.setdefault("ORCH_GITEA_WEBHOOK_SECRET", "")
|
||||
|
||||
_test_db = os.path.join(tempfile.gettempdir(), "test_orch10_states.db")
|
||||
os.environ["ORCH_DB_PATH"] = _test_db
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Known UUIDs from the ТЗ (source of truth).
|
||||
# ---------------------------------------------------------------------------
|
||||
ET_PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c"
|
||||
ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
|
||||
|
||||
ET_STATES = {
|
||||
"backlog": "113b24f6-cce8-4be9-9a22-a359b9cf0122",
|
||||
"todo": "2c7d3df3-9eb9-419b-92b7-d7d560bcdd10",
|
||||
"in_progress": "b873d9eb-993c-48cd-97ac-99a9b1623967",
|
||||
"architecture": "3020bbb7-6122-4663-930c-0315ba8dfa3d",
|
||||
"development": "9920609b-f140-4e46-ab95-89acda8412c8",
|
||||
"review": "ba0d802c-5218-41d4-ab43-978b0ea123ed",
|
||||
"testing": "7855d807-b1bf-42ef-8dae-6cde0df92d02",
|
||||
"approved": "a519a341-dada-4a91-8910-7604f82b79c5",
|
||||
"rejected": "ba958f3c-5db5-461d-8f82-89425e413b97",
|
||||
"done": "381a2833-3c4e-4be5-bd0f-be84cb946ad8",
|
||||
"cancelled": "b1cae7f9-961d-4889-a179-f3acea697d17",
|
||||
"needs_input": "babf08a3-ff4d-41f3-a821-5491aa29a8ac",
|
||||
"in_review": "38fb1f64-aa1e-48a3-92e0-0b109679046b",
|
||||
"blocked": "6c4543f9-ac47-4ef7-ae0f-070020dc9920",
|
||||
}
|
||||
|
||||
ORCH_STATES = {
|
||||
"backlog": "2d5d42ff-e94d-4209-a664-8020c28c2a95",
|
||||
"todo": "b5d3f512-4870-460f-bf6b-4ea560f00a6f",
|
||||
"in_progress": "e331bfb3-e17e-4699-ba48-4abb89c21b7b",
|
||||
"architecture": "795cc32f-5f5a-4244-be7b-9acffc92c7c0",
|
||||
"development": "f5ed4705-5029-470d-89a9-54c3f0d211ee",
|
||||
"review": "2026f3d9-0f43-4054-ab5f-3f9bae3308b8",
|
||||
"testing": "81c5cd78-2993-4f2c-9e8c-2f52db3e5623",
|
||||
"approved": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff",
|
||||
"rejected": "4c769e90-bf80-4a52-b97a-e1c84904bfc3",
|
||||
"done": "3738cd3c-7610-4907-ba5e-26b9a248d9c0",
|
||||
"cancelled": "59d1d210-8e3a-4a83-930a-cbc5dbf6ad85",
|
||||
"needs_input": "99978b3f-72fe-46e3-8b9b-25ba02899fa0",
|
||||
"in_review": "c52e99b9-31ae-4b31-be3f-9773eea7a747",
|
||||
"blocked": "505f01a6-a12f-4121-aaa7-9c5dd009acc4",
|
||||
}
|
||||
|
||||
|
||||
def _make_states_response(states_dict: dict) -> dict:
|
||||
"""Build a fake Plane GET /states/ response."""
|
||||
name_map = {v: k for k, v in {
|
||||
"backlog": "Backlog",
|
||||
"todo": "Todo",
|
||||
"in_progress": "In Progress",
|
||||
"architecture": "Architecture",
|
||||
"development": "Development",
|
||||
"review": "Review",
|
||||
"testing": "Testing",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"done": "Done",
|
||||
"cancelled": "Cancelled",
|
||||
"needs_input": "Needs Input",
|
||||
"in_review": "In Review",
|
||||
"blocked": "Blocked",
|
||||
}.items()}
|
||||
logical_to_plane = {
|
||||
"backlog": "Backlog",
|
||||
"todo": "Todo",
|
||||
"in_progress": "In Progress",
|
||||
"architecture": "Architecture",
|
||||
"development": "Development",
|
||||
"review": "Review",
|
||||
"testing": "Testing",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"done": "Done",
|
||||
"cancelled": "Cancelled",
|
||||
"needs_input": "Needs Input",
|
||||
"in_review": "In Review",
|
||||
"blocked": "Blocked",
|
||||
}
|
||||
results = [
|
||||
{"id": uid, "name": logical_to_plane[key]}
|
||||
for key, uid in states_dict.items()
|
||||
if key in logical_to_plane
|
||||
]
|
||||
return {"results": results}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers to build fake httpx responses.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _fake_response(data: dict, status: int = 200):
|
||||
m = MagicMock()
|
||||
m.status_code = status
|
||||
m.json.return_value = data
|
||||
if status >= 400:
|
||||
from httpx import HTTPStatusError, Request, Response
|
||||
m.raise_for_status.side_effect = HTTPStatusError(
|
||||
"error", request=MagicMock(), response=MagicMock()
|
||||
)
|
||||
else:
|
||||
m.raise_for_status.return_value = None
|
||||
return m
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_states_cache():
|
||||
"""Ensure the states cache is empty before each test."""
|
||||
import src.plane_sync as ps
|
||||
ps.reload_project_states()
|
||||
yield
|
||||
ps.reload_project_states()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1 & 2. get_project_states returns correct UUIDs per project
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_project_states_enduro():
|
||||
"""ET project -> enduro-trails UUIDs."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
states = ps.get_project_states(ET_PROJECT_ID)
|
||||
|
||||
for key, expected_uuid in ET_STATES.items():
|
||||
assert states[key] == expected_uuid, (
|
||||
f"ET state '{key}': expected {expected_uuid}, got {states.get(key)}"
|
||||
)
|
||||
|
||||
|
||||
def test_get_project_states_orchestrator():
|
||||
"""ORCH project -> orchestrator UUIDs."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
|
||||
states = ps.get_project_states(ORCH_PROJECT_ID)
|
||||
|
||||
for key, expected_uuid in ORCH_STATES.items():
|
||||
assert states[key] == expected_uuid, (
|
||||
f"ORCH state '{key}': expected {expected_uuid}, got {states.get(key)}"
|
||||
)
|
||||
|
||||
|
||||
def test_get_project_states_et_in_progress_uuid():
|
||||
"""ET in_progress == b873d9eb (exact UUID from ТЗ)."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
states = ps.get_project_states(ET_PROJECT_ID)
|
||||
assert states["in_progress"] == "b873d9eb-993c-48cd-97ac-99a9b1623967"
|
||||
|
||||
|
||||
def test_get_project_states_orch_in_progress_uuid():
|
||||
"""ORCH in_progress == e331bfb3 (exact UUID from ТЗ) — the ORCH-10 blocker."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
|
||||
states = ps.get_project_states(ORCH_PROJECT_ID)
|
||||
assert states["in_progress"] == "e331bfb3-e17e-4699-ba48-4abb89c21b7b"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Fallback to _DEFAULT_STATES when API fails
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_project_states_api_error_fallback():
|
||||
"""Network failure -> returns _DEFAULT_STATES (ET values)."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get", side_effect=Exception("network error")):
|
||||
states = ps.get_project_states(ORCH_PROJECT_ID)
|
||||
# Should return _DEFAULT_STATES (ET values) as fallback.
|
||||
assert states is ps._DEFAULT_STATES
|
||||
|
||||
|
||||
def test_get_project_states_non_200_fallback():
|
||||
"""Non-2xx response -> returns _DEFAULT_STATES."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response({}, status=500)
|
||||
states = ps.get_project_states(ORCH_PROJECT_ID)
|
||||
assert states is ps._DEFAULT_STATES
|
||||
|
||||
|
||||
def test_get_project_states_empty_response_fallback():
|
||||
"""Empty results list -> returns _DEFAULT_STATES."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response({"results": []})
|
||||
states = ps.get_project_states(ORCH_PROJECT_ID)
|
||||
assert states is ps._DEFAULT_STATES
|
||||
|
||||
|
||||
def test_get_project_states_none_project_id_fallback():
|
||||
"""None project_id -> _DEFAULT_STATES immediately (no API call)."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
states = ps.get_project_states(None)
|
||||
mock_get.assert_not_called()
|
||||
assert states is ps._DEFAULT_STATES
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Caching & reload_project_states
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_project_states_caches_result():
|
||||
"""Second call returns cached result without hitting the API again."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
_ = ps.get_project_states(ET_PROJECT_ID)
|
||||
_ = ps.get_project_states(ET_PROJECT_ID)
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
|
||||
def test_reload_project_states_per_project():
|
||||
"""reload_project_states(project_id) evicts only that project."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
ps.get_project_states(ET_PROJECT_ID)
|
||||
assert ET_PROJECT_ID in ps._STATES_CACHE
|
||||
|
||||
ps.reload_project_states(ET_PROJECT_ID)
|
||||
assert ET_PROJECT_ID not in ps._STATES_CACHE
|
||||
|
||||
|
||||
def test_reload_project_states_full_flush():
|
||||
"""reload_project_states() with no args clears entire cache."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
ps.get_project_states(ET_PROJECT_ID)
|
||||
ps.reload_project_states()
|
||||
assert ps._STATES_CACHE == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. stage_to_state() resolves per-project
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_stage_to_state_et_analysis():
|
||||
"""ET analysis -> in_progress UUID b873d9eb."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
uid = ps.stage_to_state("analysis", ET_PROJECT_ID)
|
||||
assert uid == "b873d9eb-993c-48cd-97ac-99a9b1623967"
|
||||
|
||||
|
||||
def test_stage_to_state_orch_analysis():
|
||||
"""ORCH analysis -> in_progress UUID e331bfb3."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
|
||||
uid = ps.stage_to_state("analysis", ORCH_PROJECT_ID)
|
||||
assert uid == "e331bfb3-e17e-4699-ba48-4abb89c21b7b"
|
||||
|
||||
|
||||
def test_stage_to_state_unknown_stage():
|
||||
"""Unknown stage -> None."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ET_STATES))
|
||||
uid = ps.stage_to_state("nonexistent_stage", ET_PROJECT_ID)
|
||||
assert uid is None
|
||||
|
||||
|
||||
def test_stage_to_state_orch_done():
|
||||
"""ORCH done -> 3738cd3c."""
|
||||
import src.plane_sync as ps
|
||||
with patch("src.plane_sync.httpx.get") as mock_get:
|
||||
mock_get.return_value = _fake_response(_make_states_response(ORCH_STATES))
|
||||
uid = ps.stage_to_state("done", ORCH_PROJECT_ID)
|
||||
assert uid == "3738cd3c-7610-4907-ba5e-26b9a248d9c0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6 & 7. Webhook handle_issue_updated — ORCH-10 critical path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_in_progress_et_starts_pipeline():
|
||||
"""ET In Progress (b873d9eb) -> handle_status_start called."""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
import src.plane_sync as ps
|
||||
|
||||
et_states_resp = _make_states_response(ET_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(et_states_resp)
|
||||
data = {
|
||||
"id": "et-issue-uuid",
|
||||
"state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress"},
|
||||
}
|
||||
await handle_issue_updated(data, ET_PROJECT_ID)
|
||||
|
||||
mock_start.assert_called_once()
|
||||
mock_verdict.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_in_progress_orch_starts_pipeline():
|
||||
"""ORCH In Progress (e331bfb3) -> handle_status_start called.
|
||||
|
||||
This is the ORCH-10 blocker: previously the webhook compared against the
|
||||
hardcoded ET UUID (b873d9eb) and the ORCH UUID (e331bfb3) was silently
|
||||
ignored — the pipeline never started for ORCH tasks.
|
||||
"""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
import src.plane_sync as ps
|
||||
|
||||
orch_states_resp = _make_states_response(ORCH_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(orch_states_resp)
|
||||
data = {
|
||||
"id": "orch-issue-uuid",
|
||||
"state": {"id": "e331bfb3-e17e-4699-ba48-4abb89c21b7b", "name": "In Progress"},
|
||||
}
|
||||
await handle_issue_updated(data, ORCH_PROJECT_ID)
|
||||
|
||||
mock_start.assert_called_once()
|
||||
mock_verdict.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_approved_orch():
|
||||
"""ORCH Approved (63f2c8fe) -> handle_verdict(approved=True)."""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
orch_states_resp = _make_states_response(ORCH_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(orch_states_resp)
|
||||
data = {
|
||||
"id": "orch-issue-uuid",
|
||||
"state": {"id": "63f2c8fe-dcda-4ace-952f-dd88bd0118ff", "name": "Approved"},
|
||||
}
|
||||
await handle_issue_updated(data, ORCH_PROJECT_ID)
|
||||
|
||||
mock_verdict.assert_called_once_with(data, ORCH_PROJECT_ID, approved=True)
|
||||
mock_start.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_rejected_orch():
|
||||
"""ORCH Rejected (4c769e90) -> handle_verdict(approved=False)."""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
orch_states_resp = _make_states_response(ORCH_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(orch_states_resp)
|
||||
data = {
|
||||
"id": "orch-issue-uuid",
|
||||
"state": {"id": "4c769e90-bf80-4a52-b97a-e1c84904bfc3", "name": "Rejected"},
|
||||
}
|
||||
await handle_issue_updated(data, ORCH_PROJECT_ID)
|
||||
|
||||
mock_verdict.assert_called_once_with(data, ORCH_PROJECT_ID, approved=False)
|
||||
mock_start.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_other_state_no_action():
|
||||
"""A non-trigger state (e.g. 'Needs Input') -> no pipeline action."""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
orch_states_resp = _make_states_response(ORCH_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(orch_states_resp)
|
||||
data = {
|
||||
"id": "orch-issue-uuid",
|
||||
"state": {"id": "99978b3f-72fe-46e3-8b9b-25ba02899fa0", "name": "Needs Input"},
|
||||
}
|
||||
await handle_issue_updated(data, ORCH_PROJECT_ID)
|
||||
|
||||
mock_start.assert_not_called()
|
||||
mock_verdict.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_et_in_progress_not_confused_with_orch():
|
||||
"""ET In Progress UUID does NOT trigger pipeline for ORCH project.
|
||||
|
||||
This guards against the reverse confusion: if somehow an ET UUID was sent
|
||||
for an ORCH project event, it should NOT start the pipeline (wrong UUID).
|
||||
"""
|
||||
from src.webhooks.plane import handle_issue_updated
|
||||
orch_states_resp = _make_states_response(ORCH_STATES)
|
||||
with patch("src.plane_sync.httpx.get") as mock_httpx, \
|
||||
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_httpx.return_value = _fake_response(orch_states_resp)
|
||||
# Send ET's in_progress UUID for an ORCH project event.
|
||||
data = {
|
||||
"id": "orch-issue-uuid",
|
||||
"state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress"},
|
||||
}
|
||||
await handle_issue_updated(data, ORCH_PROJECT_ID)
|
||||
|
||||
# Since ORCH in_progress is e331bfb3, ET's b873d9eb should NOT trigger start.
|
||||
mock_start.assert_not_called()
|
||||
mock_verdict.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. _DEFAULT_STATES / PLANE_STATES alias preserved
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_plane_states_alias_is_default_states():
|
||||
"""PLANE_STATES is still exported and equals _DEFAULT_STATES (backward compat)."""
|
||||
import src.plane_sync as ps
|
||||
assert ps.PLANE_STATES is ps._DEFAULT_STATES
|
||||
|
||||
|
||||
def test_default_states_et_values():
|
||||
"""_DEFAULT_STATES contains the original enduro-trails UUIDs."""
|
||||
import src.plane_sync as ps
|
||||
for key, expected in ET_STATES.items():
|
||||
assert ps._DEFAULT_STATES[key] == expected, (
|
||||
f"_DEFAULT_STATES['{key}']: expected {expected}, got {ps._DEFAULT_STATES.get(key)}"
|
||||
)
|
||||
Reference in New Issue
Block a user