diff --git a/gittensor/validator/storage/queries.py b/gittensor/validator/storage/queries.py index 4bc20efec..308a26d43 100644 --- a/gittensor/validator/storage/queries.py +++ b/gittensor/validator/storage/queries.py @@ -100,6 +100,16 @@ updated_at = NOW() """ +# Targeted state-only refresh for stale-closed PRs (avoids overwriting scored columns) +REFRESH_STALE_PR_STATES = """ +UPDATE pull_requests + SET pr_state = 'CLOSED', + updated_at = NOW() + WHERE number = %s + AND repository_full_name = %s + AND pr_state != 'CLOSED' +""" + # Issue Queries BULK_UPSERT_ISSUES = """ INSERT INTO issues ( diff --git a/gittensor/validator/storage/repository.py b/gittensor/validator/storage/repository.py index 4564f6662..4584b69c9 100644 --- a/gittensor/validator/storage/repository.py +++ b/gittensor/validator/storage/repository.py @@ -23,6 +23,7 @@ CLEANUP_STALE_MINER_EVALUATIONS_BY_HOTKEY, CLEANUP_STALE_MINERS, CLEANUP_STALE_MINERS_BY_HOTKEY, + REFRESH_STALE_PR_STATES, SET_MINER, ) @@ -217,6 +218,28 @@ def store_pull_requests_bulk(self, pull_requests: List[PullRequest], commit: boo self.logger.error(f'Error in bulk pull request storage: {e}') return 0 + def refresh_stale_pr_states(self, pull_requests: List[PullRequest], commit: bool = True) -> int: + """Update pr_state to CLOSED for stale PRs without touching scoring columns. + + Uses a targeted UPDATE so previously-computed scores (earned_score, base_score, + credibility_multiplier, etc.) are preserved on rows that were already scored. + Only rows currently stored as non-CLOSED are affected. + """ + if not pull_requests: + return 0 + values = [(pr.number, pr.repository_full_name) for pr in pull_requests] + try: + with self.get_cursor() as cursor: + cursor.executemany(REFRESH_STALE_PR_STATES, values) + if commit: + self.db.commit() + return len(values) + except Exception as e: + if commit: + self.db.rollback() + self.logger.error(f'Error refreshing stale PR states: {e}') + return 0 + def store_issues_bulk(self, issues: List[Issue], commit: bool = True) -> int: """ Bulk insert/update issues with efficient SQL conflict resolution diff --git a/gittensor/validator/utils/storage.py b/gittensor/validator/utils/storage.py index 80faa8b21..97e838592 100644 --- a/gittensor/validator/utils/storage.py +++ b/gittensor/validator/utils/storage.py @@ -71,7 +71,7 @@ def _adapt_mirror(scored_list): result.stored_counts['closed_pull_requests'] = self.repo.store_pull_requests_bulk( miner_eval.closed_pull_requests + _adapt_mirror(miner_eval.mirror_closed_prs), commit=False ) - result.stored_counts['stale_closed_pull_requests'] = self.repo.store_pull_requests_bulk( + result.stored_counts['stale_closed_pull_requests'] = self.repo.refresh_stale_pr_states( miner_eval.stale_closed_pull_requests, commit=False ) result.stored_counts['issues'] = self.repo.store_issues_bulk(miner_eval.get_all_issues(), commit=False) diff --git a/tests/validator/utils/test_storage_mirror.py b/tests/validator/utils/test_storage_mirror.py index f0b15d3b3..c516b7327 100644 --- a/tests/validator/utils/test_storage_mirror.py +++ b/tests/validator/utils/test_storage_mirror.py @@ -86,6 +86,7 @@ def _make_storage_with_mock_repo(): mock_repo = MagicMock() mock_repo.set_miner.return_value = 1 mock_repo.store_pull_requests_bulk.return_value = 0 # actual count irrelevant + mock_repo.refresh_stale_pr_states.return_value = 0 mock_repo.store_issues_bulk.return_value = 0 mock_repo.store_file_changes_bulk.return_value = 0 mock_repo.set_miner_evaluation.return_value = True @@ -173,12 +174,17 @@ def test_stale_closed_prs_are_stored_separately(self): storage.store_evaluation(eval_) - stale_call = mock_repo.store_pull_requests_bulk.call_args_list[3] - stale_arg = stale_call.args[0] + # Stale PRs must go through refresh_stale_pr_states (targeted UPDATE), not the + # full-column UPSERT, so previously-scored rows are not overwritten with defaults. + mock_repo.refresh_stale_pr_states.assert_called_once() + stale_arg = mock_repo.refresh_stale_pr_states.call_args.args[0] assert len(stale_arg) == 1 assert stale_arg[0].number == 7 assert stale_arg[0].pr_state == PRState.CLOSED assert eval_.total_closed_prs == 0 + # Verify the stale PR did not leak into store_pull_requests_bulk + for call in mock_repo.store_pull_requests_bulk.call_args_list: + assert not any(pr.number == 7 for pr in call.args[0]) def test_cleanup_stale_called_with_commit_false():