From f73d5d4ff40c2e1ee30fab8f5c6bc3933eb7a063 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Tue, 5 May 2026 21:55:07 +0200 Subject: [PATCH 1/5] feat: show GitHub username after PAT validation in gitt miner post --- gittensor/cli/miner_commands/post.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/gittensor/cli/miner_commands/post.py b/gittensor/cli/miner_commands/post.py index 3cef71e29..2044b820c 100644 --- a/gittensor/cli/miner_commands/post.py +++ b/gittensor/cli/miner_commands/post.py @@ -98,13 +98,13 @@ def miner_post(wallet_name, wallet_hotkey, netuid, network, rpc_url, pat, min_vt # 1b. Validate PAT locally with _status('[bold]Validating PAT...', json_mode): - pat_valid = _validate_pat_locally(pat) + github_login = _validate_pat_locally(pat) - if not pat_valid: + if github_login is None: _error('GitHub PAT is invalid or expired. Check your GITTENSOR_MINER_PAT.', json_mode) sys.exit(1) - _print('[green]PAT is valid.[/green]', json_mode) + _print(f'[green]PAT is valid.[/green] GitHub account: [bold]@{github_login}[/bold]', json_mode) # 2. Resolve wallet and network wallet_name = wallet_name or _load_config_value('wallet') or 'default' @@ -168,6 +168,7 @@ async def _broadcast(): json.dumps( { 'success': accepted_count > 0, + 'github_login': github_login, 'total_validators': len(results), **counts, 'skipped': excluded, @@ -193,15 +194,19 @@ async def _broadcast(): _render_skipped_validators(excluded, json_mode) -def _validate_pat_locally(pat: str) -> bool: - """Validate PAT mirrors the validator-side checks: user identity + GraphQL access.""" +def _validate_pat_locally(pat: str) -> Optional[str]: + """Validate PAT mirrors the validator-side checks: user identity + GraphQL access. + + Returns the GitHub login on success, or None if the PAT is invalid. + """ try: - # Check basic auth + # Check basic auth and extract login user_resp = requests.get( f'{BASE_GITHUB_API_URL}/user', headers=make_headers(pat), timeout=GITHUB_HTTP_TIMEOUT_SECONDS ) if user_resp.status_code != 200: - return False + return None + login: Optional[str] = user_resp.json().get('login') or None # Check GraphQL access (same test the validator runs during PAT broadcast) gql_resp = requests.post( @@ -214,8 +219,8 @@ def _validate_pat_locally(pat: str) -> bool: console.print( '[red]PAT lacks GraphQL API access. Fine-grained PATs need "Public Repositories (read-only)" permission.[/red]' ) - return False + return None - return True + return login except requests.RequestException: - return False + return None From a0dead2dd3acfeb10579e96032f8defac102766d Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Tue, 5 May 2026 22:18:36 +0200 Subject: [PATCH 2/5] fix: use str | None instead of Optional to satisfy ruff F821 --- gittensor/cli/miner_commands/post.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gittensor/cli/miner_commands/post.py b/gittensor/cli/miner_commands/post.py index 2044b820c..19eda3712 100644 --- a/gittensor/cli/miner_commands/post.py +++ b/gittensor/cli/miner_commands/post.py @@ -194,7 +194,7 @@ async def _broadcast(): _render_skipped_validators(excluded, json_mode) -def _validate_pat_locally(pat: str) -> Optional[str]: +def _validate_pat_locally(pat: str) -> str | None: """Validate PAT mirrors the validator-side checks: user identity + GraphQL access. Returns the GitHub login on success, or None if the PAT is invalid. @@ -206,7 +206,7 @@ def _validate_pat_locally(pat: str) -> Optional[str]: ) if user_resp.status_code != 200: return None - login: Optional[str] = user_resp.json().get('login') or None + login: str | None = user_resp.json().get('login') or None # Check GraphQL access (same test the validator runs during PAT broadcast) gql_resp = requests.post( From 465864e4e21bcc40f4511ef1b1d4389016b7a2e1 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Tue, 5 May 2026 22:21:57 +0200 Subject: [PATCH 3/5] fix: update _validate_pat_locally mocks from bool to None/str --- tests/cli/test_miner_commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cli/test_miner_commands.py b/tests/cli/test_miner_commands.py index 53b668bd9..2a62329d6 100644 --- a/tests/cli/test_miner_commands.py +++ b/tests/cli/test_miner_commands.py @@ -48,7 +48,7 @@ def test_no_pat_json_mode_exits(self, runner, monkeypatch): output = json.loads(result.output) assert output['success'] is False - @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=False) + @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=None) def test_pat_flag_used(self, mock_validate, runner, monkeypatch): monkeypatch.delenv('GITTENSOR_MINER_PAT', raising=False) result = runner.invoke(cli, ['miner', 'post', '--pat', 'ghp_test123', '--wallet', 'test', '--hotkey', 'test']) @@ -56,7 +56,7 @@ def test_pat_flag_used(self, mock_validate, runner, monkeypatch): assert 'invalid' in result.output.lower() or 'expired' in result.output.lower() mock_validate.assert_called_once_with('ghp_test123') - @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=False) + @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=None) def test_invalid_pat_exits(self, mock_validate, runner, monkeypatch): monkeypatch.setenv('GITTENSOR_MINER_PAT', 'ghp_invalid') result = runner.invoke(cli, ['miner', 'post', '--wallet', 'test', '--hotkey', 'test']) @@ -96,7 +96,7 @@ async def __call__(self, **kwargs): return responses with ( - patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=True), + patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value='testuser'), patch( 'gittensor.cli.miner_commands.post._connect_bittensor', return_value=(wallet, object(), metagraph, FakeDendrite()), From 7a5d7efec810478d136d27a413c61d887c082a52 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Sat, 9 May 2026 02:34:11 +0200 Subject: [PATCH 4/5] fix(tests): fix miner post tests after stderr-routing refactor - Fix _status() and _print() calls in post.py that still passed json_mode after helpers.py removed that parameter (routing to err_console instead) - Use CliRunner(mix_stderr=False) so result.stdout captures only JSON in --json-output mode, compatible with Click 8.1.x and 8.2+ - Mock click.prompt in test_no_pat_prompts_interactively to avoid getpass.getpass opening /dev/tty and blocking indefinitely - Assert github_login in JSON envelope test to cover new PR field - Check result.stderr for error messages (now routed there via err_console) --- gittensor/cli/miner_commands/post.py | 4 ++-- tests/cli/test_miner_commands.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/gittensor/cli/miner_commands/post.py b/gittensor/cli/miner_commands/post.py index 3ae5f297f..b4cfe7310 100644 --- a/gittensor/cli/miner_commands/post.py +++ b/gittensor/cli/miner_commands/post.py @@ -96,14 +96,14 @@ def miner_post(wallet_name, wallet_hotkey, netuid, network, rpc_url, pat, min_vt pat = click.prompt('Enter your GitHub Personal Access Token', hide_input=True) # 1b. Validate PAT locally - with _status('[bold]Validating PAT...', json_mode): + with _status('[bold]Validating PAT...'): github_login = _validate_pat_locally(pat) if github_login is None: _error('GitHub PAT is invalid or expired. Check your GITTENSOR_MINER_PAT.', json_mode) sys.exit(1) - _print(f'[green]PAT is valid.[/green] GitHub account: [bold]@{github_login}[/bold]', json_mode) + _print(f'[green]PAT is valid.[/green] GitHub account: [bold]@{github_login}[/bold]') # 2. Resolve wallet and network wallet_name = wallet_name or _load_config_value('wallet') or 'default' diff --git a/tests/cli/test_miner_commands.py b/tests/cli/test_miner_commands.py index af758c0a1..7666ba665 100644 --- a/tests/cli/test_miner_commands.py +++ b/tests/cli/test_miner_commands.py @@ -32,14 +32,16 @@ def _fake_metagraph(rows: list[tuple[float, bool, float]]): @pytest.fixture def runner(): - return CliRunner() + return CliRunner(mix_stderr=False) class TestMinerPost: - def test_no_pat_prompts_interactively(self, runner, monkeypatch): + @patch('gittensor.cli.miner_commands.post.click.prompt', return_value='ghp_fake') + @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=None) + def test_no_pat_prompts_interactively(self, mock_validate, mock_prompt, runner, monkeypatch): monkeypatch.delenv('GITTENSOR_MINER_PAT', raising=False) - result = runner.invoke(cli, ['miner', 'post', '--wallet', 'test', '--hotkey', 'test'], input='') - assert 'Enter your GitHub Personal Access Token' in result.output + runner.invoke(cli, ['miner', 'post', '--wallet', 'test', '--hotkey', 'test']) + mock_prompt.assert_called_once_with('Enter your GitHub Personal Access Token', hide_input=True) def test_no_pat_json_mode_exits(self, runner, monkeypatch): monkeypatch.delenv('GITTENSOR_MINER_PAT', raising=False) @@ -53,7 +55,7 @@ def test_pat_flag_used(self, mock_validate, runner, monkeypatch): monkeypatch.delenv('GITTENSOR_MINER_PAT', raising=False) result = runner.invoke(cli, ['miner', 'post', '--pat', 'ghp_test123', '--wallet', 'test', '--hotkey', 'test']) assert result.exit_code != 0 - assert 'invalid' in result.output.lower() or 'expired' in result.output.lower() + assert 'invalid' in result.stderr.lower() or 'expired' in result.stderr.lower() mock_validate.assert_called_once_with('ghp_test123') @patch('gittensor.cli.miner_commands.post._validate_pat_locally', return_value=None) @@ -61,7 +63,7 @@ def test_invalid_pat_exits(self, mock_validate, runner, monkeypatch): monkeypatch.setenv('GITTENSOR_MINER_PAT', 'ghp_invalid') result = runner.invoke(cli, ['miner', 'post', '--wallet', 'test', '--hotkey', 'test']) assert result.exit_code != 0 - assert 'invalid' in result.output.lower() or 'expired' in result.output.lower() + assert 'invalid' in result.stderr.lower() or 'expired' in result.stderr.lower() def test_help_text(self, runner): result = runner.invoke(cli, ['miner', 'post', '--help']) @@ -119,6 +121,7 @@ async def __call__(self, **kwargs): assert result.exit_code == 0, result.output output = json.loads(result.stdout) + assert output['github_login'] == 'testuser' assert output['total_validators'] == 3 assert output['accepted'] == 1 assert output['rejected'] == 1 From 1de2ac649977eff683a756db7a6c8ae27b3b6b18 Mon Sep 17 00:00:00 2001 From: wdeveloper16 Date: Sat, 9 May 2026 02:37:32 +0200 Subject: [PATCH 5/5] fix(tests): remove mix_stderr=False incompatible with Click 8.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CliRunner.mix_stderr was removed in Click 8.3 — streams are always separate there. CI venv uses Click 8.3 so mix_stderr caused a TypeError. --- tests/cli/test_miner_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli/test_miner_commands.py b/tests/cli/test_miner_commands.py index 7666ba665..d465010ba 100644 --- a/tests/cli/test_miner_commands.py +++ b/tests/cli/test_miner_commands.py @@ -32,7 +32,7 @@ def _fake_metagraph(rows: list[tuple[float, bool, float]]): @pytest.fixture def runner(): - return CliRunner(mix_stderr=False) + return CliRunner() class TestMinerPost: