From 665dd451535e5f16594394d1f2a17404a8aa5e8e Mon Sep 17 00:00:00 2001 From: Forge Date: Thu, 16 Jul 2026 08:16:37 +0000 Subject: [PATCH 1/3] [AISOS-2199] Implement task takeover status update post PR merge Detailed description: - Updated complete_task_takeover in src/forge/workflow/task_takeover/graph.py to transition Jira tickets to CLOSED status and apply ForgeLabel.TASK_REVIEW_APPROVED label. - Handled errors resiliently to log warnings if Jira client updates fail. - Ensured proper cleanup by closing JiraClient in a finally block. - Added comprehensive unit tests validating success and failure flows. Closes: AISOS-2199 --- src/forge/workflow/task_takeover/graph.py | 18 ++++++++- tests/workflow/test_task_takeover_graph.py | 45 ++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) 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.""" From 45cb78b62c141d49efb666cd661acdb68c573ef1 Mon Sep 17 00:00:00 2001 From: Forge Date: Thu, 16 Jul 2026 08:19:40 +0000 Subject: [PATCH 2/3] [AISOS-2199-review] Review task takeover changes for AISOS-2199 Auto-committed by Forge container fallback. --- Pipfile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Pipfile diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..a9fe832a --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.14" From 0b5438b9e54570681524e931e94c07ea9391be57 Mon Sep 17 00:00:00 2001 From: Forge Date: Thu, 16 Jul 2026 11:02:32 +0000 Subject: [PATCH 3/3] [AISOS-2199] review: address PR feedback --- Pipfile | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index a9fe832a..00000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.14"