diff --git a/gittensor/cli/issue_commands/helpers.py b/gittensor/cli/issue_commands/helpers.py index 84eb1eda..fdf4830d 100644 --- a/gittensor/cli/issue_commands/helpers.py +++ b/gittensor/cli/issue_commands/helpers.py @@ -254,8 +254,12 @@ def fetch_open_issue_pull_requests( repository_full_name: str, issue_number: int, as_json: bool, -) -> list: - """Fetch open PR submissions for a GitHub issue.""" +) -> list | None: + """Fetch open PR submissions for a GitHub issue. + + Returns: + PR list on success (possibly empty), or ``None`` when GraphQL lookup failed. + """ token = os.environ.get('GITTENSOR_MINER_PAT') or '' if not token and not as_json: print_warning('No GitHub token found; set GITTENSOR_MINER_PAT to fetch GitHub issue submissions') @@ -270,7 +274,8 @@ def fetch_open_issue_pull_requests( token=token or None, open_only=True, ) - # Intentionally return GitHub tool output as-is (no CLI schema mapping yet). + if prs is None: + return None return prs except Exception as e: raise click.ClickException(f'Failed to fetch PR submissions from GitHub: {e}') diff --git a/gittensor/cli/issue_commands/submissions.py b/gittensor/cli/issue_commands/submissions.py index 560c9291..e31d1504 100644 --- a/gittensor/cli/issue_commands/submissions.py +++ b/gittensor/cli/issue_commands/submissions.py @@ -81,6 +81,13 @@ def issues_submissions( except click.ClickException as e: handle_exception(as_json, str(e), click_error_type(e)) + if pull_requests is None: + handle_exception( + as_json, + f'Failed to fetch PR submissions from GitHub for {repo_name}#{issue_number} (GraphQL lookup failed).', + 'read_failed', + ) + if as_json: submissions = [ { diff --git a/gittensor/utils/github_api_tools.py b/gittensor/utils/github_api_tools.py index 803969d1..551f0870 100644 --- a/gittensor/utils/github_api_tools.py +++ b/gittensor/utils/github_api_tools.py @@ -329,14 +329,21 @@ def find_prs_for_issue( issue_number: int, open_only: bool = True, token: Optional[str] = None, -) -> List[PRInfo]: - """Find PRs that reference an issue via GraphQL cross-reference data.""" +) -> Optional[List[PRInfo]]: + """Find PRs that reference an issue via GraphQL cross-reference data. + + Returns: + List of PR info dicts when lookup succeeds (possibly empty). + ``None`` when GraphQL lookup fails and callers should not treat the + result as "zero submissions". + Empty list when no token is available (lookup was not attempted). + """ if token: try: - prs = _search_issue_referencing_prs_graphql(repo, issue_number, token, open_only=open_only) - return prs or [] + return _search_issue_referencing_prs_graphql(repo, issue_number, token, open_only=open_only) except Exception as exc: bt.logging.debug(f'GraphQL PR fetch failed for {repo}#{issue_number}: {exc}') + return None return [] diff --git a/tests/cli/test_issue_submission.py b/tests/cli/test_issue_submission.py index 7f6b099a..f9dcf952 100644 --- a/tests/cli/test_issue_submission.py +++ b/tests/cli/test_issue_submission.py @@ -138,6 +138,27 @@ def test_submissions_json_contract_read_failure_returns_structured_error(cli_roo assert 'not found on-chain' not in payload['error']['message'] +def test_submissions_json_github_lookup_failure_returns_read_failed(cli_root, runner, sample_issue): + with ( + patch('gittensor.cli.issue_commands.submissions.get_contract_address', return_value='0xabc'), + patch('gittensor.cli.issue_commands.submissions.resolve_network', return_value=('ws://x', 'test')), + patch('gittensor.cli.issue_commands.submissions.fetch_issue_from_contract', return_value=sample_issue), + patch('gittensor.cli.issue_commands.submissions.fetch_open_issue_pull_requests', return_value=None), + ): + result = runner.invoke( + cli_root, + ['issues', 'submissions', '--id', '42', '--json'], + catch_exceptions=False, + ) + + assert result.exit_code != 0 + payload = json.loads(result.stdout) + assert payload['success'] is False + assert payload['error']['type'] == 'read_failed' + assert 'GraphQL lookup failed' in payload['error']['message'] + assert 'submission_count' not in payload + + def test_submissions_help_via_issue_alias_routes_to_command_help(cli_root, runner): result = runner.invoke( cli_root, diff --git a/tests/utils/test_github_api_tools.py b/tests/utils/test_github_api_tools.py index 5ed4c3ad..a0e290ad 100644 --- a/tests/utils/test_github_api_tools.py +++ b/tests/utils/test_github_api_tools.py @@ -172,12 +172,22 @@ def test_find_prs_returns_empty_when_graphql_empty(mock_graphql): @patch('gittensor.utils.github_api_tools._search_issue_referencing_prs_graphql') -def test_find_prs_returns_empty_when_graphql_errors(mock_graphql): +def test_find_prs_returns_none_when_graphql_errors(mock_graphql): mock_graphql.side_effect = RuntimeError('boom') result = find_prs_for_issue('owner/repo', 12, open_only=True, token='fake_token') - assert result == [] + assert result is None + mock_graphql.assert_called_once_with('owner/repo', 12, 'fake_token', open_only=True) + + +@patch('gittensor.utils.github_api_tools._search_issue_referencing_prs_graphql') +def test_find_prs_returns_none_when_graphql_lookup_fails(mock_graphql): + mock_graphql.return_value = None + + result = find_prs_for_issue('owner/repo', 12, open_only=True, token='fake_token') + + assert result is None mock_graphql.assert_called_once_with('owner/repo', 12, 'fake_token', open_only=True)