Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Unit Tests

concurrency:
group: unit-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
pull-requests: read
contents: read

on:
push:
branches: [main, development, staging]

pull_request:
branches: [main, development, staging]
types: [opened, synchronize, reopened, ready_for_review]

workflow_dispatch:

jobs:
unit-tests:
name: Unit Tests / Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Check-out repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
uv sync --all-extras

- name: Run unit tests
run: |
uv run pytest tests/unit_tests -v --tb=short
4 changes: 2 additions & 2 deletions bittensor_cli/src/commands/crowd/update.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import json
from typing import Optional
from typing import Optional, Union

from bittensor_wallet import Wallet
from rich.prompt import IntPrompt, FloatPrompt
Expand Down Expand Up @@ -219,7 +219,7 @@ async def update_crowdloan(
cap = candidate_cap
break

value: Optional[Balance | int] = None
value: Optional[Union[Balance, int]] = None
call_function: Optional[str] = None
param_name: Optional[str] = None
update_type: Optional[str] = None
Expand Down
2 changes: 1 addition & 1 deletion bittensor_cli/src/commands/crowd/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)


def _shorten(account: str | None) -> str:
def _shorten(account: Optional[str]) -> str:
if not account:
return "-"
return f"{account[:6]}…{account[-6:]}"
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e_tests/test_axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
import re

from tests.e2e_tests.utils import execute_turn_off_hyperparam_freeze_window
from ..e2e_tests.utils import execute_turn_off_hyperparam_freeze_window


@pytest.mark.parametrize("local_chain", [None], indirect=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e_tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def extract_coldkey_balance(


def find_stake_entries(
stake_payload: dict, netuid: int, hotkey_ss58: str | None = None
stake_payload: dict, netuid: int, hotkey_ss58: Optional[str] = None
) -> list[dict]:
"""
Return stake entries matching a given netuid, optionally scoped to a specific hotkey.
Expand Down
13 changes: 8 additions & 5 deletions tests/unit_tests/test_axon_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import pytest
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from async_substrate_interface import AsyncSubstrateInterface
from bittensor_wallet import Wallet

from bittensor_cli.src.bittensor.extrinsics.serving import (
reset_axon_extrinsic,
set_axon_extrinsic,
ip_to_int,
)
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface


class TestIpToInt:
Expand Down Expand Up @@ -134,7 +136,8 @@ async def test_reset_axon_unlock_failure(self):
@pytest.mark.asyncio
async def test_reset_axon_user_cancellation(self):
"""Test axon reset when user cancels prompt."""
mock_subtensor = MagicMock()
mock_subtensor = MagicMock(spec=SubtensorInterface)
mock_subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface)
mock_wallet = MagicMock(spec=Wallet)
mock_wallet.hotkey.ss58_address = (
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
Expand All @@ -145,11 +148,11 @@ async def test_reset_axon_user_cancellation(self):
"bittensor_cli.src.bittensor.extrinsics.serving.unlock_key"
) as mock_unlock,
patch(
"bittensor_cli.src.bittensor.extrinsics.serving.Confirm"
"bittensor_cli.src.bittensor.extrinsics.serving.confirm_action"
) as mock_confirm,
):
mock_unlock.return_value = MagicMock(success=True)
mock_confirm.ask.return_value = False
mock_confirm.return_value = False

success, message, ext_id = await reset_axon_extrinsic(
subtensor=mock_subtensor,
Expand Down Expand Up @@ -362,11 +365,11 @@ async def test_set_axon_user_cancellation(self):
"bittensor_cli.src.bittensor.extrinsics.serving.unlock_key"
) as mock_unlock,
patch(
"bittensor_cli.src.bittensor.extrinsics.serving.Confirm"
"bittensor_cli.src.bittensor.extrinsics.serving.confirm_action"
) as mock_confirm,
):
mock_unlock.return_value = MagicMock(success=True)
mock_confirm.ask.return_value = False
mock_confirm.return_value = False

success, message, ext_id = await set_axon_extrinsic(
subtensor=mock_subtensor,
Expand Down
31 changes: 22 additions & 9 deletions tests/unit_tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import pytest
import typer
from async_substrate_interface import AsyncSubstrateInterface

from bittensor_cli.cli import parse_mnemonic, CLIManager
from bittensor_cli.src.bittensor.extrinsics.root import (
Expand All @@ -9,6 +10,8 @@
)
from unittest.mock import AsyncMock, patch, MagicMock, Mock

from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface


def test_parse_mnemonic():
# standard
Expand Down Expand Up @@ -58,7 +61,7 @@ async def test_subnet_sets_price_correctly():
assert subnet_info.price == mock_price


@patch("bittensor_cli.cli.Confirm")
@patch("bittensor_cli.cli.confirm_action")
@patch("bittensor_cli.cli.console")
def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm):
"""
Expand All @@ -67,7 +70,9 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm):
"""
# Setup
cli_manager = CLIManager()
mock_confirm.ask.return_value = False # User declines
cli_manager.subtensor = MagicMock(spec=SubtensorInterface)
cli_manager.subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface)
mock_confirm.return_value = False # User declines

# Mock dependencies to prevent actual execution
with (
Expand All @@ -90,6 +95,7 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm):
verbose=False,
prompt=True,
json_output=False,
proxy=None,
)

# Assert: Warning was displayed (4 console.print calls for the warning)
Expand All @@ -104,24 +110,26 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm):
)

# Assert: User was asked to confirm
mock_confirm.ask.assert_called_once()
confirm_message = mock_confirm.ask.call_args[0][0]
mock_confirm.assert_called_once()
confirm_message = mock_confirm.call_args[0][0]
assert "SURE" in confirm_message
assert "netuid 0" in confirm_message or "root network" in confirm_message

# Assert: Function returned None (early exit) because user declined
assert result is None


@patch("bittensor_cli.cli.Confirm")
@patch("bittensor_cli.cli.confirm_action")
@patch("bittensor_cli.cli.console")
def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_confirm):
"""
Test that swap_hotkey proceeds when netuid=0 and user confirms
"""
# Setup
cli_manager = CLIManager()
mock_confirm.ask.return_value = True # User confirms
cli_manager.subtensor = MagicMock(spec=SubtensorInterface)
cli_manager.subtensor.substrate = MagicMock(spec=AsyncSubstrateInterface)
mock_confirm.return_value = True # User confirms

# Mock dependencies
with (
Expand All @@ -146,10 +154,11 @@ def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_conf
verbose=False,
prompt=True,
json_output=False,
proxy=None,
)

# Assert: Warning was shown and confirmed
mock_confirm.ask.assert_called_once()
mock_confirm.assert_called_once()

# Assert: Command execution proceeded
mock_run_command.assert_called_once()
Expand Down Expand Up @@ -186,6 +195,7 @@ def test_swap_hotkey_netuid_0_no_warning_with_no_prompt(mock_console):
verbose=False,
prompt=False, # No prompt
json_output=False,
proxy=None,
)

# Assert: No warning messages about netuid 0
Expand Down Expand Up @@ -226,6 +236,7 @@ def test_swap_hotkey_netuid_1_no_warning(mock_console):
verbose=False,
prompt=True,
json_output=False,
proxy=None,
)

# Assert: No warning messages about netuid 0
Expand Down Expand Up @@ -733,12 +744,14 @@ async def test_set_root_weights_fetches_current_weights_with_prompt():
"bittensor_cli.src.bittensor.extrinsics.root.get_current_weights_for_uid"
) as mock_get_current,
patch("bittensor_cli.src.bittensor.extrinsics.root.console"),
patch("bittensor_cli.src.bittensor.extrinsics.root.Confirm") as mock_confirm,
patch(
"bittensor_cli.src.bittensor.extrinsics.root.confirm_action"
) as mock_confirm,
):
mock_unlock.return_value = MagicMock(success=True)
mock_limits.return_value = (1, 0.5)
mock_get_current.return_value = {0: 0.5, 1: 0.3, 2: 0.2}
mock_confirm.ask.return_value = False
mock_confirm.return_value = False

netuids = np.array([0, 1, 2], dtype=np.int64)
weights = np.array([0.4, 0.3, 0.3], dtype=np.float32)
Expand Down
Loading