Skip to content
Open
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
39 changes: 20 additions & 19 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,14 @@ def _maybe_continue_pipeline(
)
return

# Check if we've reached the stopping point
stopping_step = STOPPING_POINT_TO_STEP.get(stopping_point)
if stopping_step and current_step == stopping_step:
# We've reached the stopping point
return

# Check if we should trigger coding agent handoff instead of continuing
handoff_config = cls._get_handoff_config_if_applicable(stopping_point, current_step, group)
# Check if we should trigger coding agent handoff before evaluating
# the stopping point. Handoff replaces the rest of the pipeline, so it
# must take precedence over the stopping_point early-return — matching
# the legacy seer-side order in
# seer/automation/autofix/steps/root_cause_step.py, where
# _check_and_trigger_coding_agent_handoff runs before
# _should_auto_run_solution_step.
handoff_config = cls._get_handoff_config_if_applicable(current_step, group)
if handoff_config:
cls._trigger_coding_agent_handoff(
organization,
Expand All @@ -346,6 +346,12 @@ def _maybe_continue_pipeline(
)
return

# Check if we've reached the stopping point
stopping_step = STOPPING_POINT_TO_STEP.get(stopping_point)
if stopping_step and current_step == stopping_step:
# We've reached the stopping point
return

# Special case: if stopping_point is open_pr and we just finished code_changes, push changes
if (
stopping_point == AutofixStoppingPoint.OPEN_PR
Expand Down Expand Up @@ -449,7 +455,6 @@ def _push_changes(cls, group: Group, run_id: int, state: SeerRunState) -> None:
@classmethod
def _get_handoff_config_if_applicable(
cls,
stopping_point: AutofixStoppingPoint,
current_step: AutofixStep | None,
group: Group,
) -> SeerAutomationHandoffConfiguration | None:
Expand All @@ -458,21 +463,17 @@ def _get_handoff_config_if_applicable(

Handoff is triggered when:
- current_step is ROOT_CAUSE
- stopping_point is SOLUTION, CODE_CHANGES, or OPEN_PR
Comment thread
cursor[bot] marked this conversation as resolved.
- automation_handoff is configured with handoff_point = ROOT_CAUSE

The legacy seer-side gate
(seer/automation/autofix/steps/root_cause_step.py
_check_and_trigger_coding_agent_handoff) does not look at the run's
stopping_point. "Stop seer at root cause, then hand off to an external
coding agent" is a coherent configuration that must trigger handoff.
"""
# Only trigger handoff after root cause is completed
if current_step != AutofixStep.ROOT_CAUSE:
return None

# Only trigger handoff when continuing beyond root cause
if stopping_point not in [
AutofixStoppingPoint.SOLUTION,
AutofixStoppingPoint.CODE_CHANGES,
AutofixStoppingPoint.OPEN_PR,
]:
return None

return read_preference_from_sentry_db(group.project).automation_handoff

@classmethod
Expand Down
58 changes: 47 additions & 11 deletions tests/sentry/seer/autofix/test_autofix_on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,32 +456,37 @@ def _make_handoff_config(
def test_get_handoff_config_returns_none_when_not_root_cause_step(self, mock_read_pref) -> None:
"""Returns None without reading preferences when current step is not ROOT_CAUSE."""
result = AutofixOnCompletionHook._get_handoff_config_if_applicable(
stopping_point=AutofixStoppingPoint.CODE_CHANGES,
current_step=AutofixStep.SOLUTION, # Not ROOT_CAUSE
group=self.group,
)

assert result is None
mock_read_pref.assert_not_called()

@patch("sentry.seer.autofix.on_completion_hook.read_preference_from_sentry_db")
def test_get_handoff_config_returns_none_when_stopping_at_root_cause(
self, mock_read_pref
) -> None:
"""Returns None without reading preferences when stopping point is ROOT_CAUSE."""
def test_get_handoff_config_returns_config_when_stopping_at_root_cause(self) -> None:
"""Returns handoff config when the project's stopping_point is ROOT_CAUSE.

Mirrors the legacy seer-side gate, which does not look at
stopping_point when deciding to fire a handoff. The combination
"stop seer at root cause, then hand off to an external coding agent"
must trigger a handoff.
"""
self.project.update_option(
"sentry:seer_automated_run_stopping_point",
AutofixStoppingPoint.ROOT_CAUSE.value,
)
expected_handoff_config = self._make_handoff_config()

result = AutofixOnCompletionHook._get_handoff_config_if_applicable(
stopping_point=AutofixStoppingPoint.ROOT_CAUSE,
current_step=AutofixStep.ROOT_CAUSE,
group=self.group,
)

assert result is None
mock_read_pref.assert_not_called()
assert result == expected_handoff_config

def test_get_handoff_config_returns_none_when_no_handoff_configured(self) -> None:
"""Returns None when project has no automation handoff configured."""
result = AutofixOnCompletionHook._get_handoff_config_if_applicable(
stopping_point=AutofixStoppingPoint.CODE_CHANGES,
current_step=AutofixStep.ROOT_CAUSE,
group=self.group,
)
Expand All @@ -493,7 +498,6 @@ def test_get_handoff_config_returns_config_when_applicable(self) -> None:
expected_handoff_config = self._make_handoff_config()

result = AutofixOnCompletionHook._get_handoff_config_if_applicable(
stopping_point=AutofixStoppingPoint.CODE_CHANGES,
current_step=AutofixStep.ROOT_CAUSE,
group=self.group,
)
Expand Down Expand Up @@ -526,6 +530,38 @@ def test_maybe_continue_pipeline_triggers_handoff_when_configured(self, mock_tri
== AutofixReferrer.ISSUE_SUMMARY_POST_PROCESS_FIXABILITY
)

@patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff")
def test_maybe_continue_pipeline_triggers_handoff_when_stopping_at_root_cause(
self, mock_trigger_handoff
):
"""Handoff fires even when stopping_point is ROOT_CAUSE.

Regression: the previous order of if-blocks let the stopping_point
early-return short-circuit before the handoff check, dropping handoffs
for the coherent "stop seer at root cause, then external coding agent
does the fix" configuration. The handoff check must take precedence,
matching the legacy seer-side ordering in
seer/automation/autofix/steps/root_cause_step.py.
"""
self._make_handoff_config()
mock_trigger_handoff.return_value = {"successes": [], "failures": []}

state = run_state(
blocks=[
root_cause_memory_block(
referrer=AutofixReferrer.ISSUE_SUMMARY_POST_PROCESS_FIXABILITY.value
)
],
metadata={
"group_id": self.group.id,
"stopping_point": AutofixStoppingPoint.ROOT_CAUSE.value,
},
)

AutofixOnCompletionHook._maybe_continue_pipeline(self.organization, 123, state, self.group)

mock_trigger_handoff.assert_called_once()

@patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff")
def test_trigger_coding_agent_handoff_clears_preference_on_not_found(self, mock_trigger):
"""When IntegrationNotFound is raised, automation_handoff is cleared from preferences."""
Expand Down
Loading