From d9e0491b5771e79a4c9a8c68714ed8e9d03ad42a Mon Sep 17 00:00:00 2001 From: rsnetworkinginc Date: Sat, 25 Jul 2026 02:31:40 +0300 Subject: [PATCH] fix(cli): do not crash issue commands on a null network in config.json resolve_network() reads the config-file fallback with config.get('network', '').lower(). When ~/.gittensor/config.json contains "network": null (hand-edited or written by an external tool), the key exists, so the .get() default is unused, None comes back, and None.lower() raises AttributeError. _resolve_contract_and_network() calls this on the startup path of every issue command, so gitt issues/admin/vote commands all die with a raw traceback instead of falling back. The miner-side resolver already guards this exact access (_resolve_endpoint checks `if config_network and ...`), so the two CLIs currently disagree on the same config file. Guard the access with (config.get('network') or '') and apply the same guard to the ws_endpoint fallback label, matching the miner helper. Add regression tests covering the null-network fallback paths. --- gittensor/cli/issue_commands/helpers.py | 8 ++++-- tests/cli/test_cli_helpers.py | 36 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/gittensor/cli/issue_commands/helpers.py b/gittensor/cli/issue_commands/helpers.py index 4a5a0da0c..0fd686cdf 100644 --- a/gittensor/cli/issue_commands/helpers.py +++ b/gittensor/cli/issue_commands/helpers.py @@ -625,13 +625,17 @@ def resolve_network(network: Optional[str] = None, rpc_url: Optional[str] = None # override a user who set `network: finney` to point at mainnet. config = load_config() - config_network = config.get('network', '').lower() + # `or ''` guards a null/absent `network` value (e.g. `"network": null` in a + # hand-edited config) — `config.get('network', '')` returns None when the + # key is present but null, and None.lower() would crash every issue + # command. Mirrors the guard in miner_commands.helpers._resolve_endpoint. + config_network = (config.get('network') or '').lower() if config_network and config_network in NETWORK_MAP: return NETWORK_MAP[config_network], config_network if config.get('ws_endpoint'): endpoint = config['ws_endpoint'] - name = _URL_TO_NETWORK.get(endpoint, config.get('network', 'custom')) + name = _URL_TO_NETWORK.get(endpoint, config.get('network') or 'custom') return endpoint, name # Default: finney (mainnet) diff --git a/tests/cli/test_cli_helpers.py b/tests/cli/test_cli_helpers.py index 71aa5db52..597dcd71e 100644 --- a/tests/cli/test_cli_helpers.py +++ b/tests/cli/test_cli_helpers.py @@ -27,6 +27,7 @@ STATUS_COLORS, colorize_status, format_alpha, + resolve_network, validate_bounty_amount, validate_github_issue, validate_repository, @@ -34,6 +35,7 @@ ) from gittensor.cli.issue_commands.vote import parse_pr_number from gittensor.cli.json_output import emit_json +from gittensor.constants import NETWORK_MAP # ============================================================================= # format_alpha @@ -919,3 +921,37 @@ def test_non_native_types_serialized_via_default(self, value, capsys): captured = capsys.readouterr() parsed = json.loads(captured.out) assert parsed['field'] == str(value) + + +class TestResolveNetwork: + """resolve_network config-file fallback, including a null `network` value. + + A hand-edited or externally-written ~/.gittensor/config.json can carry + `"network": null`; `config.get('network', '')` returns None for that (the + key exists, so the default is unused) and must not crash every issue + command with `AttributeError: 'NoneType' object has no attribute 'lower'`. + """ + + def _resolve_with_config(self, config): + with patch('gittensor.cli.issue_commands.helpers.load_config', return_value=config): + return resolve_network() + + def test_null_network_falls_back_to_default(self): + endpoint, name = self._resolve_with_config({'network': None}) + assert endpoint == NETWORK_MAP['finney'] + assert name == 'finney' + + def test_null_network_with_ws_endpoint_uses_endpoint(self): + endpoint, name = self._resolve_with_config({'network': None, 'ws_endpoint': 'ws://localhost:9944'}) + assert endpoint == 'ws://localhost:9944' + assert name == 'custom' + + def test_recognized_config_network(self): + endpoint, name = self._resolve_with_config({'network': 'test'}) + assert endpoint == NETWORK_MAP['test'] + assert name == 'test' + + def test_empty_config_falls_back_to_default(self): + endpoint, name = self._resolve_with_config({}) + assert endpoint == NETWORK_MAP['finney'] + assert name == 'finney'