diff --git a/gittensor/validator/issue_discovery/scan.py b/gittensor/validator/issue_discovery/scan.py index 4f6310663..9ff91ad3d 100644 --- a/gittensor/validator/issue_discovery/scan.py +++ b/gittensor/validator/issue_discovery/scan.py @@ -40,7 +40,7 @@ import bittensor as bt -from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache +from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache, RepoEvaluation from gittensor.constants import ( MAINTAINER_ASSOCIATIONS, ) @@ -252,6 +252,22 @@ async def run_issue_discovery( ) +def _reset_repo_issue_fields(repo_eval: RepoEvaluation) -> None: + """Zero one repo_evaluation's issue-discovery fields. Shared by the no-issues + reset path and the scoring path (for repos not seen this round), so a repo + issue-scored in a prior round can't leave stale per-repo values that + _roll_up_issue_totals would sum back into the round-level totals. See #1610. + """ + repo_eval.is_issue_eligible = False + repo_eval.issue_credibility = 0.0 + repo_eval.issue_discovery_score = 0.0 + repo_eval.issue_token_score = 0.0 + repo_eval.total_solved_issues = 0 + repo_eval.total_valid_solved_issues = 0 + repo_eval.total_closed_issues = 0 + repo_eval.total_open_issues = 0 + + def _clear_issue_discovery_fields(evaluation: MinerEvaluation) -> None: """Reset issue-discovery aggregates after a successful fetch with no mirror issues.""" evaluation.issue_discovery_score = 0.0 @@ -264,14 +280,7 @@ def _clear_issue_discovery_fields(evaluation: MinerEvaluation) -> None: evaluation.total_open_issues = 0 evaluation.issue_discovery_issues = [] for repo_eval in evaluation.repo_evaluations.values(): - repo_eval.is_issue_eligible = False - repo_eval.issue_credibility = 0.0 - repo_eval.issue_discovery_score = 0.0 - repo_eval.issue_token_score = 0.0 - repo_eval.total_solved_issues = 0 - repo_eval.total_valid_solved_issues = 0 - repo_eval.total_closed_issues = 0 - repo_eval.total_open_issues = 0 + _reset_repo_issue_fields(repo_eval) def _apply_open_issue_counts(evaluation: MinerEvaluation, open_counts: Dict[str, int]) -> None: @@ -546,7 +555,17 @@ def _finalize_repo_issue_scores( """Gate + score issue discovery per repository, then roll up the totals.""" evaluation.issue_discovery_issues = [] - for repo_name in sorted(set(repo_acc) | set(open_counts)): + seen_repos = set(repo_acc) | set(open_counts) + # A repo issue-scored in a prior round but absent this round (e.g. an + # evaluation restored from the cache after an OSS fetch failure) is never + # rewritten by the loop below, so its stale per-repo issue values would be + # summed back into the round totals by _roll_up_issue_totals. Zero those + # here — matching the full reset in _clear_issue_discovery_fields. See #1610. + for stale_name, stale_eval in evaluation.repo_evaluations.items(): + if stale_name not in seen_repos: + _reset_repo_issue_fields(stale_eval) + + for repo_name in sorted(seen_repos): repo_config = mirror_repos.get(repo_name) if repo_config is None: continue diff --git a/tests/validator/issue_discovery/test_scan.py b/tests/validator/issue_discovery/test_scan.py index 34f44d6f5..e75c8025c 100644 --- a/tests/validator/issue_discovery/test_scan.py +++ b/tests/validator/issue_discovery/test_scan.py @@ -1919,3 +1919,43 @@ def _run_discovery(issues): assert ev_unlabeled.issue_discovery_score > 0.0 assert ev_labeled.issue_discovery_score < ev_unlabeled.issue_discovery_score assert all(i.discovery_label_multiplier == pytest.approx(0.25) for i in ev_labeled.issue_discovery_issues) + + +class TestFinalizeStaleRepoReset: + """A repo issue-scored in a prior round but absent this round must be zeroed by + the scoring path, not summed into the round totals — parity with the full reset + in _clear_issue_discovery_fields. See #1610.""" + + def test_repo_absent_this_round_is_zeroed_not_summed(self): + ev = _eval(uid=1, github_id='999') + # stale per-repo issue values carried over from a prior round (e.g. a + # cache-restored evaluation after an OSS fetch failure). + ev.repo_evaluations['owner/repo-a'] = RepoEvaluation( + repository_full_name='owner/repo-a', + total_solved_issues=7, + total_valid_solved_issues=7, + total_closed_issues=1, + total_open_issues=4, + issue_discovery_score=8.12, + issue_token_score=700.0, + is_issue_eligible=True, + issue_credibility=1.0, + ) + # this round scored no repos: repo-a is absent from repo_acc / open_counts. + scan_module._finalize_repo_issue_scores(ev, {}, {}, _mirror_repos('owner/repo-a')) + + a = ev.repo_evaluations['owner/repo-a'] + assert a.total_solved_issues == 0 + assert a.total_valid_solved_issues == 0 + assert a.total_closed_issues == 0 + assert a.total_open_issues == 0 + assert a.issue_discovery_score == 0.0 + assert a.issue_token_score == 0.0 + assert a.is_issue_eligible is False + assert a.issue_credibility == 0.0 + # round-level roll-up reflects only this round (nothing), not the stale 7 / 8.12. + assert ev.total_solved_issues == 0 + assert ev.total_valid_solved_issues == 0 + assert ev.issue_discovery_score == 0.0 + assert ev.issue_token_score == 0.0 + assert ev.is_issue_eligible is False