diff --git a/src/forge/workflow/task_takeover/graph.py b/src/forge/workflow/task_takeover/graph.py index 04862246..e0bdeb84 100644 --- a/src/forge/workflow/task_takeover/graph.py +++ b/src/forge/workflow/task_takeover/graph.py @@ -8,6 +8,8 @@ from langgraph.graph import END, StateGraph +from forge.integrations.jira.client import JiraClient +from forge.models.workflow import ForgeLabel, JiraStatus from forge.workflow.gates.task_plan_approval import ( route_task_plan_approval, task_plan_approval_gate, @@ -209,7 +211,21 @@ def _route_human_review_task_takeover(state: TaskTakeoverState) -> str: async def complete_task_takeover(state: TaskTakeoverState) -> TaskTakeoverState: - """Mark Task Takeover workflow complete after PR merge.""" + """Mark Task Takeover workflow complete after PR merge and transition Jira ticket.""" + ticket_key = state["ticket_key"] + logger.info(f"Completing task takeover workflow for ticket: {ticket_key}") + + jira = JiraClient() + try: + try: + await jira.transition_issue(ticket_key, JiraStatus.CLOSED.value) + await jira.set_workflow_label(ticket_key, ForgeLabel.TASK_REVIEW_APPROVED) + logger.info(f"Task {ticket_key} successfully transitioned to Closed/Done") + except Exception as e: + logger.warning(f"Failed to transition Jira status/label for {ticket_key}: {e}") + finally: + await jira.close() + return update_state_timestamp( { **state, diff --git a/tests/workflow/test_task_takeover_graph.py b/tests/workflow/test_task_takeover_graph.py index a66711ac..b9f8371d 100644 --- a/tests/workflow/test_task_takeover_graph.py +++ b/tests/workflow/test_task_takeover_graph.py @@ -6,7 +6,7 @@ import pytest from langgraph.graph import END, StateGraph -from forge.models.workflow import ForgeLabel, TicketType +from forge.models.workflow import ForgeLabel, JiraStatus, TicketType from forge.workflow.gates.task_plan_approval import route_task_plan_approval from forge.workflow.task_takeover.graph import ( _route_after_answer, @@ -241,13 +241,52 @@ def test_human_review_approved_routes_to_task_takeover_complete(self) -> None: assert _route_human_review_task_takeover(state) == "complete_task_takeover" @pytest.mark.asyncio - async def test_complete_task_takeover_marks_workflow_complete(self) -> None: - state = make_task_state(current_node="human_review_gate", is_paused=True) + @patch("forge.workflow.task_takeover.graph.JiraClient") + async def test_complete_task_takeover_marks_workflow_complete( + self, mock_jira_class: MagicMock + ) -> None: + mock_jira = MagicMock() + mock_jira.transition_issue = AsyncMock() + mock_jira.set_workflow_label = AsyncMock() + mock_jira.close = AsyncMock() + mock_jira_class.return_value = mock_jira + + state = make_task_state( + ticket_key="TASK-123", current_node="human_review_gate", is_paused=True + ) + result = await complete_task_takeover(state) + assert result["current_node"] == "complete" + assert result["is_paused"] is False + assert result["ci_fix_attempt"] == 0 + + mock_jira.transition_issue.assert_called_once_with("TASK-123", JiraStatus.CLOSED.value) + mock_jira.set_workflow_label.assert_called_once_with( + "TASK-123", ForgeLabel.TASK_REVIEW_APPROVED + ) + mock_jira.close.assert_called_once() + + @pytest.mark.asyncio + @patch("forge.workflow.task_takeover.graph.JiraClient") + async def test_complete_task_takeover_resilience_on_exception( + self, mock_jira_class: MagicMock + ) -> None: + mock_jira = MagicMock() + mock_jira.transition_issue = AsyncMock(side_effect=Exception("Jira is down")) + mock_jira.set_workflow_label = AsyncMock() + mock_jira.close = AsyncMock() + mock_jira_class.return_value = mock_jira + + state = make_task_state( + ticket_key="TASK-123", current_node="human_review_gate", is_paused=True + ) result = await complete_task_takeover(state) assert result["current_node"] == "complete" assert result["is_paused"] is False assert result["ci_fix_attempt"] == 0 + mock_jira.transition_issue.assert_called_once_with("TASK-123", JiraStatus.CLOSED.value) + mock_jira.close.assert_called_once() + class TestInteractiveGateBehavior: """Test interactive gate behavior for plan approvals, questions, and revision requests."""