Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/forge/orchestrator/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ def _is_workflow_errored(state: dict) -> bool:
def _has_new_reportable_error(result: dict, error_before_invoke: str | None) -> bool:
"""Return whether an invocation produced an error that should be reported."""
last_error = result.get("last_error")
return bool(
last_error and not result.get("is_paused", False) and last_error != error_before_invoke
)
return bool(last_error and last_error != error_before_invoke)


async def _report_new_workflow_error(result: dict, error_before_invoke: str | None) -> None:
Expand Down
3 changes: 2 additions & 1 deletion src/forge/workflow/bug/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def route_entry(state: BugState) -> str:
def _route_after_triage_check(state: BugState) -> str:
"""Route after triage_check based on what triage_check set as current_node."""
node = state.get("current_node", "triage_gate")
if node in ("analyze_bug", "triage_gate", "escalate_blocked"):
if node in ("triage_check", "analyze_bug", "triage_gate", "escalate_blocked"):
return node
return "triage_gate"

Expand Down Expand Up @@ -491,6 +491,7 @@ def build_bug_graph() -> StateGraph:
"triage_check",
_route_after_triage_check,
{
"triage_check": "triage_check",
"triage_gate": "triage_gate",
"analyze_bug": "analyze_bug",
"escalate_blocked": "escalate_blocked",
Expand Down
3 changes: 3 additions & 0 deletions src/forge/workflow/task_takeover/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def route_entry(state: TaskTakeoverState) -> str:
def _route_after_triage_check(state: TaskTakeoverState) -> str:
"""Route after triage_check based on what triage_check set as current_node."""
node = state.get("current_node", "triage_gate")
if node == "triage_check":
return "triage_check"
if node in ("analyze_bug", "generate_plan"):
return "generate_plan"
if node in ("triage_gate", "escalate_blocked"):
Expand Down Expand Up @@ -282,6 +284,7 @@ def build_task_takeover_graph() -> StateGraph[TaskTakeoverState, Any, Any]:
"triage_check",
_route_after_triage_check,
{
"triage_check": "triage_check",
"triage_gate": "triage_gate",
"generate_plan": "generate_plan",
"escalate_blocked": "escalate_blocked",
Expand Down
18 changes: 16 additions & 2 deletions tests/unit/orchestrator/test_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
[
({"last_error": "new failure", "is_paused": False}, None, True),
({"last_error": "same failure", "is_paused": False}, "same failure", False),
({"last_error": "needs input", "is_paused": True}, None, False),
({"last_error": "paused failure", "is_paused": True}, None, True),
({"last_error": None, "is_paused": False}, None, False),
],
)
Expand All @@ -42,12 +42,26 @@ async def test_report_new_workflow_error_posts_once():
notify.assert_awaited_once_with(result, "clone failed", "setup_workspace")


@pytest.mark.asyncio
async def test_report_new_workflow_error_posts_when_failure_ends_at_pause_gate():
result = {
"ticket_key": "TEST-123",
"current_node": "triage_gate",
"last_error": "model backend unavailable",
"is_paused": True,
}

with patch("forge.orchestrator.worker.notify_error", new_callable=AsyncMock) as notify:
await _report_new_workflow_error(result, None)

notify.assert_awaited_once_with(result, "model backend unavailable", "triage_gate")


@pytest.mark.asyncio
@pytest.mark.parametrize(
("result", "error_before_invoke"),
[
({"last_error": "same failure", "is_paused": False}, "same failure"),
({"last_error": "needs input", "is_paused": True}, None),
({"last_error": None, "is_paused": False}, None),
],
)
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/workflow/bug/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def test_empty_current_node_routes_to_triage(self):
class TestTriageCheckRouting:
"""_route_after_triage_check proxies triage_check current_node output."""

def test_routes_retryable_failure_back_to_triage_check(self):
state = _bug_state(current_node="triage_check", last_error="temporary failure")
assert _route_after_triage_check(state) == "triage_check"

def test_routes_to_analyze_bug(self):
state = _bug_state(current_node="analyze_bug")
assert _route_after_triage_check(state) == "analyze_bug"
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/workflow/task_takeover/test_graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Unit tests for Task Takeover workflow state and graph structure."""

from typing import Any, cast

import pytest
from langgraph.graph import END, StateGraph

Expand Down Expand Up @@ -93,6 +94,7 @@ class TestTriageCheckRouting:
@pytest.mark.parametrize(
"current_node,expected",
[
("triage_check", "triage_check"),
("analyze_bug", "generate_plan"),
("triage_gate", "triage_gate"),
("escalate_blocked", "escalate_blocked"),
Expand Down
1 change: 1 addition & 0 deletions tests/workflow/test_task_takeover_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def test_route_entry(self, current_node: str, expected_next: str) -> None:
@pytest.mark.parametrize(
"current_node, expected_next",
[
("triage_check", "triage_check"),
("generate_plan", "generate_plan"),
("triage_gate", "triage_gate"),
("escalate_blocked", "escalate_blocked"),
Expand Down
Loading