Skip to content
12 changes: 12 additions & 0 deletions gittensor/utils/github_api_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,18 @@ def check_github_issue_closed(repo: str, issue_number: int, token: str) -> Optio
if data.get('state') != 'closed':
return {'is_closed': False}

state_reason = data.get('state_reason')
if not isinstance(state_reason, str) or state_reason.strip().lower() != 'completed':
bt.logging.info(
f'Issue closed on GitHub but not completed: {repo}#{issue_number} state_reason={state_reason}'
)
return {
'is_closed': True,
'solver_github_id': None,
'pr_number': None,
'solver_lookup_failed': False,
}

bt.logging.debug(f'Finding solver for {repo}#{issue_number}')
solver_lookup = find_solver_from_cross_references(repo, issue_number, token)
if solver_lookup is None:
Expand Down
33 changes: 31 additions & 2 deletions tests/utils/test_github_api_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ class TestCheckGithubIssueClosed:
def test_graphql_failure_sets_solver_lookup_failed(self, mock_logging, mock_get, mock_graphql):
issue_response = Mock()
issue_response.status_code = 200
issue_response.json.return_value = {'state': 'closed'}
issue_response.json.return_value = {'state': 'closed', 'state_reason': 'completed'}
mock_get.return_value = issue_response
mock_graphql.return_value = None

Expand All @@ -1094,7 +1094,7 @@ def test_graphql_failure_sets_solver_lookup_failed(self, mock_logging, mock_get,
def test_closed_issue_with_no_solver_keeps_lookup_failed_false(self, mock_logging, mock_get, mock_graphql):
issue_response = Mock()
issue_response.status_code = 200
issue_response.json.return_value = {'state': 'closed'}
issue_response.json.return_value = {'state': 'closed', 'state_reason': 'completed'}
mock_get.return_value = issue_response
mock_graphql.return_value = _graphql_response([])

Expand All @@ -1107,6 +1107,35 @@ def test_closed_issue_with_no_solver_keeps_lookup_failed_false(self, mock_loggin
'solver_lookup_failed': False,
}

@pytest.mark.parametrize(
'issue_payload',
[
{'state': 'closed', 'state_reason': 'not_planned'},
{'state': 'closed', 'state_reason': 'duplicate'},
{'state': 'closed', 'state_reason': 'transferred'},
{'state': 'closed', 'state_reason': None},
{'state': 'closed'},
],
)
@patch('gittensor.utils.github_api_tools.execute_graphql_query')
@patch('gittensor.utils.github_api_tools.requests.get')
@patch('gittensor.utils.github_api_tools.bt.logging')
def test_non_completed_closed_issue_skips_solver_lookup(self, mock_logging, mock_get, mock_graphql, issue_payload):
issue_response = Mock()
issue_response.status_code = 200
issue_response.json.return_value = issue_payload
mock_get.return_value = issue_response

result = check_github_issue_closed('owner/repo', 12, 'fake_token')

assert result == {
'is_closed': True,
'solver_github_id': None,
'pr_number': None,
'solver_lookup_failed': False,
}
mock_graphql.assert_not_called()


# ============================================================================
# load_miners_prs Per-PR Error Resilience Tests
Expand Down
Loading