Skip to content
Closed
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
1 change: 1 addition & 0 deletions gittensor/cli/issue_commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@

CommandFunc = TypeVar('CommandFunc', bound=Callable[..., Any])
NETWORK_CHOICE = click.Choice(['finney', 'test', 'local'], case_sensitive=False)
STATUS_CHOICE = click.Choice(['registered', 'active', 'completed', 'cancelled'], case_sensitive=False)


def apply_click_options(*decorators: Callable[[CommandFunc], CommandFunc]) -> Callable[[CommandFunc], CommandFunc]:
Expand Down
26 changes: 25 additions & 1 deletion gittensor/cli/issue_commands/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from .help import StyledCommand
from .helpers import (
STATUS_CHOICE,
_read_contract_packed_storage,
_read_issues_from_child_storage,
_resolve_contract_and_network,
Expand Down Expand Up @@ -65,21 +66,36 @@ def _fill_percent(bounty: int, target: int) -> float:
type=REPO,
help='Filter issues to a specific repository (owner/name).',
)
@click.option(
'--status',
'status_filter',
default=None,
type=STATUS_CHOICE,
help='Filter issues by lifecycle status (registered/active/completed/cancelled).',
)
@with_cli_behavior_options(
include_verbose=True,
include_json=True,
verbose_help='Show debug output for contract reads',
)
@with_network_contract_options('Contract address (uses default if empty)')
def issues_list(
issue_id: int, repo_filter: str, network: str, rpc_url: str, contract: str, verbose: bool, as_json: bool
issue_id: int,
repo_filter: str,
status_filter: str,
network: str,
rpc_url: str,
contract: str,
verbose: bool,
as_json: bool,
):
"""List issues or view a specific issue.

[dim]Examples:
$ gitt issues list
$ gitt i list --network test
$ gitt i list --id 1
$ gitt i list --status active
$ gitt i list --json
[/dim]
"""
Expand Down Expand Up @@ -114,6 +130,10 @@ def issues_list(
if repo_filter and issue_id is None:
issues = [i for i in issues if i.get('repository_full_name', '').lower() == repo_filter.lower()]

# Apply --status filter before rendering (--id takes precedence)
if status_filter and issue_id is None:
issues = [i for i in issues if str(i.get('status', '')).lower() == status_filter.lower()]

if issue_id is not None:
issue = next((i for i in issues if i['id'] == issue_id), None)
if issue is None:
Expand Down Expand Up @@ -152,6 +172,10 @@ def issues_list(
if repo_filter and issue_id is None:
issues = [i for i in issues if i.get('repository_full_name', '').lower() == repo_filter.lower()]

# Apply --status filter before table render (--id takes precedence)
if status_filter and issue_id is None:
issues = [i for i in issues if str(i.get('status', '')).lower() == status_filter.lower()]

# Table view of all issues
err_console.print('[bold cyan]Available Issues[/bold cyan]\n')

Expand Down
37 changes: 37 additions & 0 deletions tests/cli/test_issues_list_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
'target_bounty': 100_000_000_000,
'status': 'Active',
},
{
'id': 2,
'repository_full_name': 'owner/repo',
'issue_number': 11,
'bounty_amount': 0,
'target_bounty': 100_000_000_000,
'status': 'Completed',
},
]


Expand Down Expand Up @@ -66,6 +74,35 @@ def test_issues_list_rejects_invalid_id_human(cli_root, runner, bad_id):
mock_read.assert_not_called()


def test_issues_list_json_status_filter_is_case_insensitive(cli_root, runner):
"""`--status` must filter JSON output by lifecycle state regardless of input casing."""
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', '--json', '--status', 'ACTIVE'], catch_exceptions=False)

assert result.exit_code == 0

payload = json.loads(result.stdout)
assert payload['success'] is True
assert payload['issue_count'] == 1
assert [i['id'] for i in payload['issues']] == [1]


def test_issues_list_rejects_invalid_status(cli_root, runner):
"""An unknown --status value must be rejected at parse time without any contract read."""
with patch('gittensor.cli.issue_commands.view.read_issues_from_contract') as mock_read:
result = runner.invoke(cli_root, ['issues', 'list', '--status', 'bogus'], catch_exceptions=False)

assert result.exit_code != 0
assert 'bogus' in result.output
mock_read.assert_not_called()


def test_issues_list_json_contract_read_failure_returns_structured_error(cli_root, runner):
"""A contract read failure must surface as `success: false` JSON with non-zero exit,
not as a `success: true, issue_count: 0` payload that looks like a clean empty contract."""
Expand Down
Loading