Skip to content

Commit 7f129bb

Browse files
fix(seer): Resolve UUID run_id in OrganizationSeerAgentUpdateEndpoint (#118616)
## What After #117265 and #117780, the Seer Explorer frontend started sending `sentry_run_id` (a UUID) instead of the numeric `seer_run_state_id` for run identification. The chat endpoint's GET (poll) and POST (continue) paths were updated to resolve either form via `resolve_seer_run()`. The update endpoint (`/seer/explorer-update/{run_id}/`) was missed, so Seer's Pydantic model rejects UUID run IDs with: ``` {"detail":[{"type":"int_parsing","loc":["body","run_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"cac2b2c1-4fd7-4003-acc2-e0f1ca0a5640"}]} ``` ## Fix Apply `resolve_seer_run()` to `OrganizationSeerAgentUpdateEndpoint.post()`, translating UUIDs (or numeric IDs) to `seer_run_state_id` before forwarding to Seer's `/v1/automation/explorer/update`. Mirrors the fix already applied to the chat endpoint. - `run_id` type hint widened to `str` (the URL pattern already captured it as `[^/]+`) - `resolved.seer_run_state_id` (int) used in the Seer request body - Added `test_explorer_update_with_uuid_run_id` covering the UUID path end-to-end - Updated existing `run_id` assertion from `"123"` → `123` (now correctly an int after resolution) Reported by Sofia Rest. Refs #117265, #117780. --- [View Session in Sentry](https://sentry.sentry.io/traces/?project=4510944073809921&query=gen_ai.conversation.id%3A%22slack%3AC0B0PFS5069%3A1782524325.765739%22) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
1 parent cb7d59b commit 7f129bb

2 files changed

Lines changed: 93 additions & 3 deletions

File tree

src/sentry/seer/endpoints/organization_seer_agent_update.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
has_seer_agent_access_with_detail,
2222
)
2323
from sentry.seer.autofix.constants import CODING_PAYLOAD_TYPES
24+
from sentry.seer.endpoints.utils import resolve_seer_run
2425
from sentry.seer.models import SeerApiError
2526
from sentry.seer.signed_seer_api import make_signed_seer_api_request
2627

@@ -61,7 +62,7 @@ class OrganizationSeerAgentUpdateEndpoint(OrganizationEndpoint):
6162
owner = ApiOwner.ML_AI
6263
permission_classes = (OrganizationSeerAgentUpdatePermission,)
6364

64-
def post(self, request: Request, organization: Organization, run_id: int) -> Response:
65+
def post(self, request: Request, organization: Organization, run_id: str) -> Response:
6566
"""
6667
Send an update event to the agent for a given run.
6768
"""
@@ -83,12 +84,16 @@ def post(self, request: Request, organization: Organization, run_id: int) -> Res
8384
data={"detail": "Code generation is disabled for this organization"},
8485
)
8586

87+
resolved = resolve_seer_run(run_id, organization, for_continue=True)
88+
if isinstance(resolved, Response):
89+
return resolved
90+
8691
path = "/v1/automation/explorer/update"
8792

8893
body = orjson.dumps(
8994
{
9095
**request.data,
91-
"run_id": run_id,
96+
"run_id": resolved.seer_run_state_id,
9297
"organization_id": organization.id,
9398
}
9499
)

tests/sentry/seer/endpoints/test_organization_seer_agent_update.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import uuid
2+
from datetime import datetime, timezone
13
from unittest.mock import MagicMock, patch
24

35
import orjson
46
from rest_framework import status
57

8+
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType
69
from sentry.testutils.cases import APITestCase
710
from sentry.testutils.helpers.features import with_feature
811

@@ -48,10 +51,92 @@ def test_explorer_update_successful(
4851

4952
# Verify the payload
5053
sent_data = orjson.loads(call_args[0][2])
51-
assert sent_data["run_id"] == "123"
54+
assert sent_data["run_id"] == 123
5255
assert sent_data["organization_id"] == self.organization.id
5356
assert sent_data["payload"]["type"] == "interrupt"
5457

58+
@patch("sentry.seer.endpoints.organization_seer_agent_update.has_seer_agent_access_with_detail")
59+
@patch("sentry.seer.endpoints.organization_seer_agent_update.make_signed_seer_api_request")
60+
def test_explorer_update_with_uuid_run_id(
61+
self, mock_request: MagicMock, mock_has_access: MagicMock
62+
) -> None:
63+
"""UUID run_id should be resolved to the numeric seer_run_state_id before forwarding to Seer."""
64+
mock_has_access.return_value = (True, None)
65+
mock_request.return_value.status = 200
66+
mock_request.return_value.json.return_value = {"run_id": 456}
67+
68+
run_uuid = uuid.uuid4()
69+
SeerRun.objects.create(
70+
organization=self.organization,
71+
uuid=run_uuid,
72+
seer_run_state_id=456,
73+
type=SeerRunType.EXPLORER,
74+
mirror_status=SeerRunMirrorStatus.LIVE,
75+
last_triggered_at=datetime.now(tz=timezone.utc),
76+
)
77+
78+
url = f"/api/0/organizations/{self.organization.slug}/seer/explorer-update/{run_uuid}/"
79+
response = self.client.post(
80+
url,
81+
data={"payload": {"type": "interrupt"}},
82+
format="json",
83+
)
84+
85+
assert response.status_code == status.HTTP_202_ACCEPTED
86+
mock_request.assert_called_once()
87+
sent_data = orjson.loads(mock_request.call_args[0][2])
88+
# UUID must be translated to the numeric seer_run_state_id before Seer sees it
89+
assert sent_data["run_id"] == 456
90+
assert sent_data["organization_id"] == self.organization.id
91+
92+
@patch("sentry.seer.endpoints.organization_seer_agent_update.has_seer_agent_access_with_detail")
93+
@patch("sentry.seer.endpoints.organization_seer_agent_update.make_signed_seer_api_request")
94+
def test_explorer_update_uuid_run_still_mirroring_returns_409(
95+
self, mock_request: MagicMock, mock_has_access: MagicMock
96+
) -> None:
97+
"""A UUID run whose seer_run_state_id is not yet populated should return 409."""
98+
mock_has_access.return_value = (True, None)
99+
100+
run_uuid = uuid.uuid4()
101+
SeerRun.objects.create(
102+
organization=self.organization,
103+
uuid=run_uuid,
104+
seer_run_state_id=None, # not yet mirrored
105+
type=SeerRunType.EXPLORER,
106+
mirror_status=SeerRunMirrorStatus.PENDING,
107+
last_triggered_at=datetime.now(tz=timezone.utc),
108+
)
109+
110+
url = f"/api/0/organizations/{self.organization.slug}/seer/explorer-update/{run_uuid}/"
111+
response = self.client.post(url, data={"payload": {"type": "interrupt"}}, format="json")
112+
113+
assert response.status_code == status.HTTP_409_CONFLICT
114+
mock_request.assert_not_called()
115+
116+
@patch("sentry.seer.endpoints.organization_seer_agent_update.has_seer_agent_access_with_detail")
117+
@patch("sentry.seer.endpoints.organization_seer_agent_update.make_signed_seer_api_request")
118+
def test_explorer_update_uuid_run_mirror_failed_returns_422(
119+
self, mock_request: MagicMock, mock_has_access: MagicMock
120+
) -> None:
121+
"""A UUID run whose mirror failed should return 422."""
122+
mock_has_access.return_value = (True, None)
123+
124+
run_uuid = uuid.uuid4()
125+
SeerRun.objects.create(
126+
organization=self.organization,
127+
uuid=run_uuid,
128+
seer_run_state_id=None,
129+
type=SeerRunType.EXPLORER,
130+
mirror_status=SeerRunMirrorStatus.FAILED,
131+
last_triggered_at=datetime.now(tz=timezone.utc),
132+
)
133+
134+
url = f"/api/0/organizations/{self.organization.slug}/seer/explorer-update/{run_uuid}/"
135+
response = self.client.post(url, data={"payload": {"type": "interrupt"}}, format="json")
136+
137+
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
138+
mock_request.assert_not_called()
139+
55140
@patch("sentry.seer.endpoints.organization_seer_agent_update.has_seer_agent_access_with_detail")
56141
@patch("sentry.seer.endpoints.organization_seer_agent_update.make_signed_seer_api_request")
57142
def test_explorer_update_missing_payload(

0 commit comments

Comments
 (0)