"""ORCH-066: To Analyse resume semantics (F-1 status-only model). `handle_status_start` forks on (existing task?) + (active job?): * TC-02 (AC-2, BR-11) — an EXISTING task with NO active job + To Analyse -> RELAUNCH the current stage's agent (the analyst resumes from Needs Input); NO second task is created; the issue is re-indicated `Analysis`. * TC-04 (AC-4) — an EXISTING task WITH an active job + To Analyse -> busy-guard: NO relaunch (no double launch). handle_status_start is exercised directly; enqueue_job + Plane side-effects are mocked. A real isolated sqlite DB backs get_task_by_plane_id / the job guard. """ import os import tempfile import pytest _test_db = os.path.join(tempfile.gettempdir(), "test_orch066_to_analyse_resume.db") os.environ["ORCH_DB_PATH"] = _test_db os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir() os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") from unittest.mock import patch, AsyncMock, MagicMock # noqa: E402 import src.db as _db # noqa: E402 from src.db import init_db, get_db # noqa: E402 from src.webhooks.plane import handle_status_start # noqa: E402 @pytest.fixture(autouse=True) def fresh_db(monkeypatch): monkeypatch.setattr(_db.settings, "db_path", _test_db) if os.path.exists(_test_db): os.unlink(_test_db) init_db() yield if os.path.exists(_test_db): os.unlink(_test_db) def _make_task(plane_id="resume-1", stage="analysis", repo="enduro-trails", branch="feature/ET-001-x", wi="ET-001"): conn = get_db() cur = conn.execute( "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) " "VALUES (?, ?, ?, ?, ?)", (plane_id, wi, repo, branch, stage), ) tid = cur.lastrowid conn.commit() conn.close() return tid def _count(plane_id): conn = get_db() n = conn.execute("SELECT COUNT(*) FROM tasks WHERE plane_id=?", (plane_id,)).fetchone()[0] conn.close() return n # --------------------------------------------------------------------------- # TC-02 (AC-2 / BR-11): existing task, no active job -> RELAUNCH (resume), no dup. # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc02_to_analyse_resume_relaunches_analyst_no_duplicate(): _make_task("resume-1", stage="analysis") data = {"id": "resume-1", "state": {"id": "ip-uuid", "name": "To Analyse"}} with patch("src.webhooks.plane.enqueue_job", return_value=7) as mock_enqueue, \ patch("src.webhooks.plane.start_pipeline", new_callable=AsyncMock) as mock_start, \ patch("src.plane_sync.add_comment", MagicMock()), \ patch("src.plane_sync.set_issue_analysis") as mock_analysis: await handle_status_start(data, "proj-1") # No new pipeline start (it is a resume, not a fresh task). mock_start.assert_not_called() assert _count("resume-1") == 1 # NO duplicate task # The current stage's agent (analyst) was relaunched exactly once. assert mock_enqueue.call_count == 1 assert mock_enqueue.call_args.args[0] == "analyst" # AC-3: the resumed analysis stage is re-indicated as Analysis. mock_analysis.assert_called_once_with("ET-001") # --------------------------------------------------------------------------- # TC-04 (AC-4): existing task WITH active job -> busy-guard, NO relaunch. # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_tc04_to_analyse_with_active_job_does_not_relaunch(): tid = _make_task("resume-2", stage="analysis") # Seed an active (queued) job so has_active_job_for_task reports busy. conn = get_db() conn.execute( "INSERT INTO jobs (agent, repo, task_id, status) VALUES (?, ?, ?, 'queued')", ("analyst", "enduro-trails", tid), ) conn.commit() conn.close() data = {"id": "resume-2", "state": {"id": "ip-uuid", "name": "To Analyse"}} with patch("src.webhooks.plane.enqueue_job", return_value=9) as mock_enqueue, \ patch("src.webhooks.plane.start_pipeline", new_callable=AsyncMock) as mock_start, \ patch("src.plane_sync.add_comment", MagicMock()), \ patch("src.plane_sync.set_issue_analysis") as mock_analysis: await handle_status_start(data, "proj-1") mock_start.assert_not_called() mock_enqueue.assert_not_called() # busy-guard held: NO double launch mock_analysis.assert_not_called() assert _count("resume-2") == 1