From 6327c350b4ccb643664f0d2d1a30859431b96e80 Mon Sep 17 00:00:00 2001 From: mkdev11 Date: Fri, 1 May 2026 23:47:38 +0200 Subject: [PATCH] fix mirror issue open spam count --- gittensor/classes.py | 2 +- gittensor/validator/issue_discovery/scan.py | 23 ++++++---- tests/validator/issue_discovery/test_scan.py | 44 +++++++++++++++++++- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/gittensor/classes.py b/gittensor/classes.py index c944e1d0..326e2a18 100644 --- a/gittensor/classes.py +++ b/gittensor/classes.py @@ -288,7 +288,7 @@ class MinerEvaluation: total_solved_issues: int = 0 total_valid_solved_issues: int = 0 # solved issues where solving PR has token_score >= 5 total_closed_issues: int = 0 - total_open_issues: int = 0 # mirror-tracked open issues in lookback window (set by issue_discovery.scan) + total_open_issues: int = 0 # current mirror-tracked open issues (set by issue_discovery.scan) @property def total_prs(self) -> int: diff --git a/gittensor/validator/issue_discovery/scan.py b/gittensor/validator/issue_discovery/scan.py index ad771fe2..e9241fec 100644 --- a/gittensor/validator/issue_discovery/scan.py +++ b/gittensor/validator/issue_discovery/scan.py @@ -164,17 +164,23 @@ async def run_issue_discovery( fetch_errors += 1 continue + try: + current_response = await asyncio.to_thread(client.get_miner_issues, evaluation.github_id) + except MirrorRequestError as e: + bt.logging.warning(f'├─ UID {uid}: open-issue count fetch failed ({e}) — skipped this miner') + _restore_issue_discovery_from_cache(evaluation, evaluation_cache) + fetch_errors += 1 + continue + + open_issue_count = _count_open_issues(current_response.issues, enabled_names) filtered = [i for i in response.issues if i.repo_full_name in enabled_names] if not filtered: _clear_issue_discovery_fields(evaluation) + evaluation.total_open_issues = open_issue_count cacheable_uids.add(uid) no_issues += 1 continue - # Count this miner's currently-open issues across registered repos - # (within the lookback window). Used as the spam-multiplier signal and - # also written to evaluation.total_open_issues for the DB row. - open_issue_count = sum(1 for i in filtered if i.state == 'OPEN') pending.append((evaluation, filtered, open_issue_count)) canonical_pr_owners = _build_canonical_pr_owners(pending) @@ -287,6 +293,10 @@ def _build_canonical_pr_owners( return canonical +def _count_open_issues(issues: List[MirrorIssue], enabled_names: Set[str]) -> int: + return sum(1 for issue in issues if issue.repo_full_name in enabled_names and issue.state == 'OPEN') + + def _build_solving_pr_cache( miner_evaluations: Dict[int, MinerEvaluation], ) -> Dict[Tuple[str, int], CachedSolvingPR]: @@ -325,9 +335,8 @@ async def _score_miner_issues( ) -> bool: """Classify + score one miner's mirror issues, populate MinerEvaluation fields. - ``open_issue_count`` is the miner's currently-OPEN issue count across - registered repos within the lookback window — the source-of-truth for the - open-issue spam multiplier. + ``open_issue_count`` is the miner's current OPEN issue count across + registered repos, independent of the issue-scoring lookback window. ``canonical_pr_owners`` enforces the cross-miner one-issue-per-PR rule: only the marker-matching issue scores, siblings count for credibility. diff --git a/tests/validator/issue_discovery/test_scan.py b/tests/validator/issue_discovery/test_scan.py index bb445b27..d6f01ac5 100644 --- a/tests/validator/issue_discovery/test_scan.py +++ b/tests/validator/issue_discovery/test_scan.py @@ -928,9 +928,49 @@ def test_unavailable_scoring_data_is_not_cached_across_sibling_lookups(self): class TestOpenIssueSpamSourceIsMirror: """The open-issue spam multiplier sources its count from mirror's response, - and mirror_scan also writes that count to evaluation.total_open_issues so + and scan also writes that count to evaluation.total_open_issues so the DB row reflects mirror-scoped state.""" + def test_old_open_issues_outside_scoring_window_still_trip_spam(self): + """Scoring stays lookback-bounded, but open-issue load is current.""" + solved_issues = [_issue_dict(issue_number=300 + i, author_github_id=f'discoverer{i}') for i in range(8)] + old_open_issues = [ + _issue_dict( + issue_number=200 + i, + state='OPEN', + state_reason=None, + solved_by_pr=None, + created_at='2026-01-01T00:00:00Z', + ) + for i in range(6) + ] + client = Mock() + client.get_miner_issues.side_effect = [ + _response(solved_issues), # lookback-bounded scoring response + _response(old_open_issues), # current/open-count response + ] + + eval_ = _eval() + eval_.merged_prs = [_scored_mirror_pr('entrius/gittensor-ui', 100, token_score=100.0)] + + _run( + run_issue_discovery( + {1: eval_}, + _mirror_repos('entrius/gittensor-ui'), + _EMPTY_LANGS, + _EMPTY_TOKEN_CONFIG, + client=client, + ) + ) + + assert eval_.total_open_issues == 6 + assert eval_.issue_discovery_score == 0 + assert client.get_miner_issues.call_count == 2 + scoring_since = client.get_miner_issues.call_args_list[0].kwargs['since'] + open_count_call = client.get_miner_issues.call_args_list[1] + assert scoring_since is not None + assert open_count_call.kwargs.get('since') is None + def test_all_mirror_miner_with_many_open_issues_trips_spam(self): """6 open issues in mirror response trips the spam multiplier.""" open_issues = [ @@ -963,7 +1003,7 @@ def test_all_mirror_miner_with_many_open_issues_trips_spam(self): # 6 open issues > threshold (5) → spam_mult=0 → all scored issues earn 0 assert eval_.issue_discovery_score == 0 - # mirror_scan now records the mirror-scoped open count + # scan records the mirror-scoped open count assert eval_.total_open_issues == 6 def test_all_mirror_miner_below_threshold_passes_spam(self):