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
14 changes: 13 additions & 1 deletion src/forge/orchestrator/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,19 @@ async def _handle_resume_event(
}
prev_error = current_state.get("last_error")
is_paused_at_gate = current_state.get("is_paused") and current_node in approval_gates
if is_paused_at_gate and not prev_error:
if current_node == "triage_gate":
logger.info("Retry at triage_gate — re-running triage_check")
updated_state["is_paused"] = False
updated_state["is_blocked"] = False
updated_state["last_error"] = None
updated_state["auto_retry_cap_notified"] = False
updated_state["retry_count"] = 0
updated_state["current_node"] = "triage_check"
updated_state["context"] = {
**updated_state.get("context", {}),
"force_fresh_invoke": True,
}
elif is_paused_at_gate:
logger.info(
f"Retry at approval gate {current_node} — triggering regeneration "
f"via revision request"
Expand Down
78 changes: 78 additions & 0 deletions tests/unit/orchestrator/test_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,84 @@ async def test_retry_at_task_approval_gate_clears_stale_epic_and_task_keys(
assert result["current_task_key"] is None
assert result["revision_requested"] is True

@pytest.mark.asyncio
async def test_retry_at_triage_gate_reenters_triage_check(
self, worker: OrchestratorWorker, base_message: QueueMessage, base_state: dict
):
state = {
**base_state,
"current_node": "triage_gate",
"is_paused": True,
"last_error": "model backend unavailable",
"retry_count": 1,
}
payload = {
**base_message.payload,
"changelog": {
"items": [
{
"field": "labels",
"fromString": "forge:managed",
"toString": "forge:managed forge:retry",
}
]
},
}
message = QueueMessage(
message_id=base_message.message_id,
event_id=base_message.event_id,
source=base_message.source,
event_type="jira:issue_updated",
ticket_key=base_message.ticket_key,
payload=payload,
)

result = await worker._handle_resume_event(message, state)

assert result["current_node"] == "triage_check"
assert result["is_paused"] is False
assert result["last_error"] is None
assert result["context"]["force_fresh_invoke"] is True

@pytest.mark.asyncio
async def test_retry_at_approval_gate_with_error_triggers_regeneration(
self, worker: OrchestratorWorker, base_message: QueueMessage, base_state: dict
):
state = {
**base_state,
"current_node": "prd_approval_gate",
"is_paused": True,
"last_error": "PRD publish pending",
}
payload = {
**base_message.payload,
"changelog": {
"items": [
{
"field": "labels",
"fromString": "forge:managed",
"toString": "forge:managed forge:retry",
}
]
},
}
message = QueueMessage(
message_id=base_message.message_id,
event_id=base_message.event_id,
source=base_message.source,
event_type="jira:issue_updated",
ticket_key=base_message.ticket_key,
payload=payload,
)

result = await worker._handle_resume_event(message, state)

assert result["current_node"] == "prd_approval_gate"
assert result["is_paused"] is False
assert result["last_error"] is None
assert result["revision_requested"] is True
assert result["feedback_comment"] == "Regeneration requested via retry."

@pytest.mark.asyncio
async def test_prd_label_change_to_approved_sets_approved_flag(
self, worker: OrchestratorWorker, base_message: QueueMessage, base_state: dict
Expand Down
Loading