From d27780779cdfaae13f896ffb83afd5ec9a81380b Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Sat, 24 Jan 2026 09:36:27 -0500 Subject: [PATCH] fix(cursor): Handle empty branch_name when launching Cursor agent When `branch_name` is an empty string, the Cursor API returns an error: "Git ref (branch/tag) cannot be an empty string" This affects ~50% of Seer project preferences where users intentionally leave branch_name blank, expecting Seer to use the repo's default branch. The fix converts empty/falsy `branch_name` to `None`, which gets excluded from the JSON payload via `exclude_none=True`, allowing Cursor to automatically resolve to the repository's default branch. Fixes: Seer fails when invoking Cursor agent with empty source.ref --- src/sentry/integrations/cursor/client.py | 3 +- .../sentry/integrations/cursor/test_client.py | 99 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/sentry/integrations/cursor/client.py b/src/sentry/integrations/cursor/client.py index f280b38a0a702d..ce0e13e172f898 100644 --- a/src/sentry/integrations/cursor/client.py +++ b/src/sentry/integrations/cursor/client.py @@ -57,7 +57,8 @@ def launch(self, webhook_url: str, request: CodingAgentLaunchRequest) -> CodingA ), source=CursorAgentSource( repository=f"https://github.com/{request.repository.owner}/{request.repository.name}", - ref=request.repository.branch_name, + # Use None for empty branch_name so Cursor uses repo's default branch + ref=request.repository.branch_name or None, ), webhook=CursorAgentLaunchRequestWebhook(url=webhook_url, secret=self.webhook_secret), target=CursorAgentLaunchRequestTarget( diff --git a/tests/sentry/integrations/cursor/test_client.py b/tests/sentry/integrations/cursor/test_client.py index a4ba9cb0b9734c..ae8b30781e0ab6 100644 --- a/tests/sentry/integrations/cursor/test_client.py +++ b/tests/sentry/integrations/cursor/test_client.py @@ -146,3 +146,102 @@ def test_launch_default_auto_create_pr(self, mock_post: Mock) -> None: # Verify the payload contains autoCreatePr=False (the default) payload = call_kwargs["data"] assert payload["target"]["autoCreatePr"] is False + + @patch.object(CursorAgentClient, "post") + def test_launch_with_empty_branch_name_uses_default(self, mock_post: Mock) -> None: + """Test that launch() excludes ref when branch_name is empty, allowing Cursor to use repo default""" + # Setup mock response + mock_response = Mock() + mock_response.json = { + "id": "agent_123", + "status": "running", + "name": "Test Agent", + "createdAt": "2023-01-01T00:00:00Z", + "source": { + "repository": "https://github.com/getsentry/sentry", + "ref": "main", # Cursor returns the resolved default branch + }, + "target": { + "url": "https://cursor.com/agent/123", + "autoCreatePr": False, + "branchName": "fix-bug-123", + }, + } + mock_post.return_value = mock_response + + # Create repo definition with empty branch_name + repo_definition_empty_branch = SeerRepoDefinition( + integration_id="111", + provider="github", + owner="getsentry", + name="sentry", + external_id="123456", + branch_name="", # Empty string + ) + + request = CodingAgentLaunchRequest( + prompt="Fix this bug", + repository=repo_definition_empty_branch, + branch_name="fix-bug-123", + ) + + # Launch the agent + self.cursor_client.launch(webhook_url=self.webhook_url, request=request) + + # Assert that post was called with correct parameters + mock_post.assert_called_once() + call_kwargs = mock_post.call_args[1] + + # Verify the payload does NOT contain ref (it's excluded when None) + # This allows Cursor to use the repo's default branch + payload = call_kwargs["data"] + assert "ref" not in payload["source"] + + @patch.object(CursorAgentClient, "post") + def test_launch_with_none_branch_name_uses_default(self, mock_post: Mock) -> None: + """Test that launch() excludes ref when branch_name is None, allowing Cursor to use repo default""" + # Setup mock response + mock_response = Mock() + mock_response.json = { + "id": "agent_123", + "status": "running", + "name": "Test Agent", + "createdAt": "2023-01-01T00:00:00Z", + "source": { + "repository": "https://github.com/getsentry/sentry", + "ref": "main", + }, + "target": { + "url": "https://cursor.com/agent/123", + "autoCreatePr": False, + "branchName": "fix-bug-123", + }, + } + mock_post.return_value = mock_response + + # Create repo definition with None branch_name + repo_definition_none_branch = SeerRepoDefinition( + integration_id="111", + provider="github", + owner="getsentry", + name="sentry", + external_id="123456", + branch_name=None, + ) + + request = CodingAgentLaunchRequest( + prompt="Fix this bug", + repository=repo_definition_none_branch, + branch_name="fix-bug-123", + ) + + # Launch the agent + self.cursor_client.launch(webhook_url=self.webhook_url, request=request) + + # Assert that post was called with correct parameters + mock_post.assert_called_once() + call_kwargs = mock_post.call_args[1] + + # Verify the payload does NOT contain ref + payload = call_kwargs["data"] + assert "ref" not in payload["source"]