diff --git a/gittensor/cli/miner_commands/post.py b/gittensor/cli/miner_commands/post.py index 70d8aabb2..b4cfe7310 100644 --- a/gittensor/cli/miner_commands/post.py +++ b/gittensor/cli/miner_commands/post.py @@ -97,13 +97,13 @@ def miner_post(wallet_name, wallet_hotkey, netuid, network, rpc_url, pat, min_vt # 1b. Validate PAT locally with _status('[bold]Validating PAT...'): - 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]') + _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' @@ -167,6 +167,7 @@ async def _broadcast(): json.dumps( { 'success': accepted_count > 0, + 'github_login': github_login, 'total_validators': len(results), **counts, 'skipped': excluded, @@ -192,15 +193,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) -> 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. + """ 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: str | None = user_resp.json().get('login') or None # Check GraphQL access (same test the validator runs during PAT broadcast) gql_resp = requests.post( @@ -213,8 +218,8 @@ def _validate_pat_locally(pat: str) -> bool: err_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 diff --git a/tests/cli/test_miner_commands.py b/tests/cli/test_miner_commands.py index 07e95d977..fc610898e 100644 --- a/tests/cli/test_miner_commands.py +++ b/tests/cli/test_miner_commands.py @@ -37,10 +37,12 @@ def runner(): 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) @@ -49,20 +51,20 @@ def test_no_pat_json_mode_exits(self, runner, monkeypatch): output = json.loads(result.stdout) 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']) 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=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']) 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']) @@ -97,7 +99,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()), @@ -120,6 +122,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