From 470990f76bbe5b47ea864806e0a205bab170807e Mon Sep 17 00:00:00 2001 From: jakearmstrong59 Date: Thu, 14 May 2026 22:04:02 +0200 Subject: [PATCH] fix: clear duplicate issue discovery rows --- gittensor/validator/emission_allocation.py | 8 +++-- .../oss_contributions/inspections.py | 1 + tests/validator/test_blend_emission_pools.py | 34 +++++++++++++++++++ .../test_duplicate_penalty_propagation.py | 32 ++++++++++++++++- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/gittensor/validator/emission_allocation.py b/gittensor/validator/emission_allocation.py index 14036824..73ee51ff 100644 --- a/gittensor/validator/emission_allocation.py +++ b/gittensor/validator/emission_allocation.py @@ -96,7 +96,7 @@ def _collect_repo_pr_scores( ) -> Dict[int, float]: scores: Dict[int, float] = {} for uid, evaluation in miner_evaluations.items(): - if uid not in miner_uids: + if not _is_scoring_evaluation(uid, evaluation, miner_uids): continue score = sum( @@ -117,7 +117,7 @@ def _collect_repo_issue_discovery_scores( ) -> Dict[int, float]: scores: Dict[int, float] = {} for uid, evaluation in miner_evaluations.items(): - if uid not in miner_uids: + if not _is_scoring_evaluation(uid, evaluation, miner_uids): continue score = sum( @@ -131,6 +131,10 @@ def _collect_repo_issue_discovery_scores( return scores +def _is_scoring_evaluation(uid: int, evaluation: MinerEvaluation, miner_uids: set[int]) -> bool: + return uid in miner_uids and evaluation.failed_reason is None + + def _allocate_scores_to_rewards( rewards: np.ndarray, uid_index: Dict[int, int], diff --git a/gittensor/validator/oss_contributions/inspections.py b/gittensor/validator/oss_contributions/inspections.py index 24ea4be4..e0adeead 100644 --- a/gittensor/validator/oss_contributions/inspections.py +++ b/gittensor/validator/oss_contributions/inspections.py @@ -92,6 +92,7 @@ def _zero_for_duplicate_penalty(eval_: MinerEvaluation, reason: str) -> None: eval_.total_valid_solved_issues = 0 eval_.total_closed_issues = 0 eval_.total_open_issues = 0 + eval_.issue_discovery_issues = [] def validate_response_and_initialize_miner_evaluation( diff --git a/tests/validator/test_blend_emission_pools.py b/tests/validator/test_blend_emission_pools.py index 6214b8e0..ee719164 100644 --- a/tests/validator/test_blend_emission_pools.py +++ b/tests/validator/test_blend_emission_pools.py @@ -130,6 +130,40 @@ def test_empty_repo_slice_recycles_without_redistribution(self): assert rewards[_idx(miner_uids, RECYCLE_UID)] == pytest.approx(0.6 * OSS_EMISSION_SHARE) +class TestFailedEvaluationFiltering: + def test_failed_pr_rows_do_not_consume_repo_slice(self): + repos = {'r/pr': _config(emission_share=1.0, issue_discovery_share=0.0)} + miner_uids = _uids(1, 2) + failed = _evaluation(1, prs=[_scored_pr('r/pr', 100, earned_score=30.0)]) + failed.failed_reason = 'penalized' + evaluations = { + 1: failed, + 2: _evaluation(2, prs=[_scored_pr('r/pr', 200, earned_score=10.0)]), + } + + rewards = blend_emission_pools(evaluations, repos, miner_uids) + + assert rewards[_idx(miner_uids, 1)] == pytest.approx(0.0) + assert rewards[_idx(miner_uids, 2)] == pytest.approx(OSS_EMISSION_SHARE) + assert rewards[_idx(miner_uids, RECYCLE_UID)] == pytest.approx(0.0) + + def test_failed_issue_rows_do_not_consume_repo_slice(self): + repos = {'r/issue': _config(emission_share=1.0, issue_discovery_share=1.0)} + miner_uids = _uids(1, 2) + failed = _evaluation(1, issues=[_discovered_issue('r/issue', 10, earned_score=30.0)]) + failed.failed_reason = 'penalized' + evaluations = { + 1: failed, + 2: _evaluation(2, issues=[_discovered_issue('r/issue', 20, earned_score=10.0)]), + } + + rewards = blend_emission_pools(evaluations, repos, miner_uids) + + assert rewards[_idx(miner_uids, 1)] == pytest.approx(0.0) + assert rewards[_idx(miner_uids, 2)] == pytest.approx(OSS_EMISSION_SHARE) + assert rewards[_idx(miner_uids, RECYCLE_UID)] == pytest.approx(0.0) + + class TestWithinRepoSpill: def test_issue_side_empty_spills_to_pr_side(self): repos = {'r/spill-pr': _config(emission_share=0.1, issue_discovery_share=0.6)} diff --git a/tests/validator/test_duplicate_penalty_propagation.py b/tests/validator/test_duplicate_penalty_propagation.py index e5f2646c..dbfff1c7 100644 --- a/tests/validator/test_duplicate_penalty_propagation.py +++ b/tests/validator/test_duplicate_penalty_propagation.py @@ -13,7 +13,7 @@ import numpy as np -from gittensor.classes import MinerEvaluation +from gittensor.classes import Issue, MinerEvaluation from gittensor.validator.oss_contributions.inspections import ( detect_and_penalize_miners_sharing_github, ) @@ -27,6 +27,17 @@ def __init__(self, scores: np.ndarray, alpha: float = 0.1): self.config.neuron.moving_average_alpha = alpha +def _discovered_issue(uid: int) -> Issue: + issue = Issue( + number=uid, + pr_number=uid + 100, + repository_full_name='r/issue', + title='cached', + discovery_earned_score=10.0, + ) + return issue + + def test_detected_duplicates_wipe_prior_ema_via_update_scores(): # Prior round: UID 1 and UID 2 posted PRs under the same GitHub account # and accumulated EMA weight. UID 0 is honest. On the unfixed call @@ -54,3 +65,22 @@ def test_detected_duplicates_wipe_prior_ema_via_update_scores(): assert validator.scores[2] == 0.0 assert validator.scores[0] > 0.0 assert np.isclose(validator.scores.sum(), 1.0) + + +def test_duplicate_penalty_clears_cached_issue_discovery_rows(): + evaluations = { + 1: MinerEvaluation(uid=1, hotkey='hotkey_1', github_id='gh_shared'), + 2: MinerEvaluation(uid=2, hotkey='hotkey_2', github_id='gh_shared'), + 3: MinerEvaluation(uid=3, hotkey='hotkey_3', github_id='gh_honest'), + } + for evaluation in evaluations.values(): + evaluation.issue_discovery_score = 10.0 + evaluation.issue_discovery_issues = [_discovered_issue(evaluation.uid)] + + penalized_uids = detect_and_penalize_miners_sharing_github(evaluations) + + assert penalized_uids == {1, 2} + for uid in penalized_uids: + assert evaluations[uid].issue_discovery_score == 0.0 + assert evaluations[uid].issue_discovery_issues == [] + assert len(evaluations[3].issue_discovery_issues) == 1