diff --git a/gittensor/cli/issue_commands/view.py b/gittensor/cli/issue_commands/view.py index 576bb1e7..8367ca59 100644 --- a/gittensor/cli/issue_commands/view.py +++ b/gittensor/cli/issue_commands/view.py @@ -24,7 +24,6 @@ _resolve_contract_and_network, colorize_status, console, - emit_error_json, emit_json, format_alpha, handle_exception, @@ -80,8 +79,7 @@ def issues_list(issue_id: int, network: str, rpc_url: str, contract: str, verbos if issue_id is not None: issue = next((i for i in issues if i['id'] == issue_id), None) if issue is None: - emit_error_json(f'Issue {issue_id} not found on-chain.', error_type='not_found') - raise SystemExit(1) + handle_exception(as_json, f'Issue {issue_id} not found on-chain.', 'not_found') emit_json({'success': True, 'issue': issue}) else: emit_json({'success': True, 'issue_count': len(issues), 'issues': issues}) @@ -109,7 +107,7 @@ def issues_list(issue_id: int, network: str, rpc_url: str, contract: str, verbos ) ) else: - console.print(f'[yellow]Issue {issue_id} not found.[/yellow]') + handle_exception(as_json, f'Issue {issue_id} not found on-chain.', 'not_found') return # Table view of all issues diff --git a/gittensor/cli/miner_commands/check.py b/gittensor/cli/miner_commands/check.py index b0b0db70..774a0970 100644 --- a/gittensor/cli/miner_commands/check.py +++ b/gittensor/cli/miner_commands/check.py @@ -15,6 +15,8 @@ _connect_bittensor, _error, _load_config_value, + _pat_check_aggregate_counts, + _pat_check_row_category, _print, _require_registered, _require_validator_axons, @@ -24,6 +26,13 @@ console = Console() +_PAT_CHECK_STATUS_MARKUP = { + 'valid': '[green]✓ valid[/green]', + 'no_pat': '[red]✗ no PAT[/red]', + 'invalid_pat': '[red]✗ invalid[/red]', + 'no_response': '[yellow]— no response[/yellow]', +} + @click.command() @click.option('--wallet', 'wallet_name', default=None, help='Bittensor wallet name.') @@ -95,8 +104,8 @@ async def _check(): } ) - valid_count = sum(1 for r in results if r['pat_valid'] is True) - no_response_count = sum(1 for r in results if r['has_pat'] is None) + counts = _pat_check_aggregate_counts(results) + valid_count = counts['valid'] # 6. Display results if json_mode: @@ -105,9 +114,7 @@ async def _check(): { 'success': valid_count > 0, 'total_validators': len(results), - 'valid': valid_count, - 'invalid': len(results) - valid_count - no_response_count, - 'no_response': no_response_count, + **counts, 'results': results, }, indent=2, @@ -121,14 +128,8 @@ async def _check(): table.add_column('Reason', style='dim') for r in results: - if r['pat_valid'] is True: - status = '[green]✓ valid[/green]' - elif r['has_pat'] is False: - status = '[red]✗ no PAT[/red]' - elif r['pat_valid'] is False: - status = '[red]✗ invalid[/red]' - else: - status = '[yellow]— no response[/yellow]' + category = _pat_check_row_category(r) + status = _PAT_CHECK_STATUS_MARKUP[category] table.add_row(str(r['uid']), r['hotkey'], status, r.get('rejection_reason') or '') console.print(table) diff --git a/gittensor/cli/miner_commands/helpers.py b/gittensor/cli/miner_commands/helpers.py index 506d9bbd..3108901a 100644 --- a/gittensor/cli/miner_commands/helpers.py +++ b/gittensor/cli/miner_commands/helpers.py @@ -6,8 +6,10 @@ import json import sys +from collections import Counter from contextlib import nullcontext from pathlib import Path +from typing import Any import click from rich.console import Console @@ -101,3 +103,25 @@ def _require_validator_axons(metagraph, json_mode: bool) -> tuple[list, list]: _error('No reachable validator axons found on the network.', json_mode) sys.exit(1) return validator_axons, validator_uids + + +def _pat_check_row_category(row: dict[str, Any]) -> str: + """Classify a PAT probe row; must match `miner check` table rendering order.""" + if row.get('pat_valid') is True: + return 'valid' + if row.get('has_pat') is False: + return 'no_pat' + if row.get('has_pat') is True and row.get('pat_valid') is False: + return 'invalid_pat' + return 'no_response' + + +def _pat_check_aggregate_counts(results: list[dict[str, Any]]) -> dict[str, int]: + """Count PAT check rows by status for JSON summaries.""" + counts = Counter(_pat_check_row_category(r) for r in results) + return { + 'valid': counts['valid'], + 'no_pat': counts['no_pat'], + 'invalid_pat': counts['invalid_pat'], + 'no_response': counts['no_response'], + } diff --git a/tests/cli/test_issues_list_json.py b/tests/cli/test_issues_list_json.py index 41fe3131..9cf61b72 100644 --- a/tests/cli/test_issues_list_json.py +++ b/tests/cli/test_issues_list_json.py @@ -35,3 +35,19 @@ def test_issues_list_json_missing_issue_returns_structured_error(cli_root, runne assert payload['success'] is False assert payload['error']['type'] == 'not_found' assert '999' in payload['error']['message'] + + +def test_issues_list_human_missing_issue_exits_non_zero(cli_root, runner): + """Human mode must exit non-zero for missing --id, matching JSON semantics.""" + with ( + patch( + 'gittensor.cli.issue_commands.view._resolve_contract_and_network', + return_value=('5Fakeaddr', 'ws://x', 'test'), + ), + patch('gittensor.cli.issue_commands.view.read_issues_from_contract', return_value=FAKE_ISSUES), + ): + result = runner.invoke(cli_root, ['issues', 'list', '--id', '999'], catch_exceptions=False) + + assert result.exit_code != 0 + assert '999' in result.output + assert 'not found' in result.output.lower() diff --git a/tests/cli/test_miner_commands.py b/tests/cli/test_miner_commands.py index 41e4ee27..28cb3a7d 100644 --- a/tests/cli/test_miner_commands.py +++ b/tests/cli/test_miner_commands.py @@ -10,6 +10,7 @@ from gittensor import __version__ from gittensor.cli.main import cli +from gittensor.cli.miner_commands.helpers import _pat_check_aggregate_counts @pytest.fixture @@ -75,3 +76,19 @@ def test_version_matches_package_version(self, runner): result = runner.invoke(cli, ['--version']) assert result.exit_code == 0 assert result.output == f'gittensor, version {__version__}\n' + + +class TestPatCheckAggregateCounts: + def test_splits_valid_no_pat_invalid_and_no_response(self): + results = [ + {'pat_valid': True, 'has_pat': True}, + {'pat_valid': False, 'has_pat': False}, + {'pat_valid': False, 'has_pat': True}, + {'pat_valid': None, 'has_pat': None}, + ] + assert _pat_check_aggregate_counts(results) == { + 'valid': 1, + 'no_pat': 1, + 'invalid_pat': 1, + 'no_response': 1, + }