Skip to content
Open
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
59 changes: 49 additions & 10 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import copy
import re
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
Expand All @@ -12,6 +12,7 @@
# Forward-reference only — avoids importing the mirror subpackage at runtime
# and prevents accidental coupling. The mirror_* lists below are typed as
# strings to defer resolution.
from gittensor.utils.mirror.models import MirrorLinkedIssue, MirrorPullRequest
from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredMirrorPR

from gittensor.constants import (
Expand Down Expand Up @@ -635,6 +636,40 @@ class CachedEvaluation:
cached_at: datetime


def _copy_issues(issues: Optional[List[Issue]]) -> Optional[List[Issue]]:
if issues is None:
return None
return [copy.copy(issue) for issue in issues]


def _copy_pull_request_for_cache(pr: PullRequest) -> PullRequest:
pr_copy = copy.copy(pr)
pr_copy.file_changes = None
pr_copy.issues = _copy_issues(pr.issues)
return pr_copy


def _copy_mirror_linked_issue(issue: 'MirrorLinkedIssue') -> 'MirrorLinkedIssue':
issue_copy = copy.copy(issue)
issue_copy.labels = [copy.copy(label) for label in issue.labels]
return issue_copy


def _copy_mirror_pull_request(pr: 'MirrorPullRequest') -> 'MirrorPullRequest':
pr_copy = copy.copy(pr)
pr_copy.review_summary = copy.copy(pr.review_summary)
pr_copy.labels = [copy.copy(label) for label in pr.labels]
pr_copy.linked_issues = [_copy_mirror_linked_issue(issue) for issue in pr.linked_issues]
return pr_copy


def _copy_scored_mirror_pr_for_cache(scored: 'ScoredMirrorPR') -> 'ScoredMirrorPR':
scored_copy = copy.copy(scored)
scored_copy.pr = _copy_mirror_pull_request(scored.pr)
scored_copy.files = None
return scored_copy


class MinerEvaluationCache:
"""
In-memory cache for successful miner evaluations, keyed by UID.
Expand Down Expand Up @@ -690,17 +725,21 @@ def get(self, uid: int, hotkey: str, github_id: str) -> Optional['MinerEvaluatio

bt.logging.debug(f'Cache hit for UID {uid} (cached at {cached.cached_at.isoformat()})')

return deepcopy(cached.evaluation)
return self.create_lightweight_copy(cached.evaluation)

def create_lightweight_copy(self, evaluation: 'MinerEvaluation') -> 'MinerEvaluation':
"""Create a memory-efficient copy, stripping file patches."""
light_eval = deepcopy(evaluation)

for pr in light_eval.merged_pull_requests + light_eval.open_pull_requests + light_eval.closed_pull_requests:
if pr.file_changes:
for fc in pr.file_changes:
fc.patch = None

"""Create a cache-safe copy without retaining heavy file payloads."""
light_eval = copy.copy(evaluation)
light_eval.github_pat = None
light_eval.unique_repos_contributed_to = set(evaluation.unique_repos_contributed_to)

# Cached evaluations are only used as scoring fallback; DB storage is
# skipped for cached UIDs, so file payloads/rows are not needed here.
light_eval.merged_pull_requests = [_copy_pull_request_for_cache(pr) for pr in evaluation.merged_pull_requests]
light_eval.open_pull_requests = [_copy_pull_request_for_cache(pr) for pr in evaluation.open_pull_requests]
light_eval.closed_pull_requests = [_copy_pull_request_for_cache(pr) for pr in evaluation.closed_pull_requests]
light_eval.mirror_merged_prs = [_copy_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_merged_prs]
light_eval.mirror_open_prs = [_copy_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_open_prs]
light_eval.mirror_closed_prs = [_copy_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_closed_prs]

return light_eval
182 changes: 181 additions & 1 deletion tests/validator/test_validator_cache_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from datetime import datetime, timezone
from typing import cast

from gittensor.classes import MinerEvaluation, MinerEvaluationCache, PRState, PullRequest
from gittensor.classes import FileChange, Issue, MinerEvaluation, MinerEvaluationCache, PRState, PullRequest
from gittensor.utils.mirror.models import MirrorFile, MirrorLinkedIssue, MirrorPullRequest, MirrorReviewSummary
from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredMirrorPR
from neurons.validator import Validator


Expand Down Expand Up @@ -36,6 +38,104 @@ def _build_eval(uid: int, merged_prs: int, fetch_failed: bool) -> MinerEvaluatio
return eval_


def _make_file_change(pr_number: int = 1) -> FileChange:
return FileChange(
pr_number=pr_number,
repository_full_name='owner/repo',
filename='src/lib.py',
changes=4,
additions=3,
deletions=1,
status='modified',
patch='@@ heavy diff body @@',
)


def _make_issue(pr_number: int = 1) -> Issue:
return Issue(
number=42,
pr_number=pr_number,
repository_full_name='owner/repo',
title='linked issue',
author_github_id='99',
)


def _make_scored_mirror_pr(pr_number: int = 100, blob: str = 'heavy file content') -> ScoredMirrorPR:
now = datetime.now(timezone.utc)
linked_issue = MirrorLinkedIssue(
number=77,
title='mirror linked issue',
state='CLOSED',
state_reason='COMPLETED',
author_github_id='99',
author_association='CONTRIBUTOR',
created_at=now,
closed_at=now,
updated_at=now,
is_transferred=False,
solved_by_pr=pr_number,
)
pr = MirrorPullRequest(
repo_full_name='owner/repo',
pr_number=pr_number,
title='mirror pr',
body=None,
state='MERGED',
author_github_id='12345',
author_login='miner',
author_association='CONTRIBUTOR',
created_at=now,
closed_at=None,
merged_at=now,
last_edited_at=None,
edited_after_merge=False,
hours_since_merge=0.0,
merged_by_login='maintainer',
base_ref='main',
head_ref='feature',
head_repo_full_name='owner/repo',
default_branch='main',
head_sha='head',
base_sha='base',
merge_base_sha='merge-base',
additions=3,
deletions=1,
commits_count=1,
scoring_data_stored=True,
review_summary=MirrorReviewSummary(maintainer_changes_requested_count=1),
linked_issues=[linked_issue],
)
scored = ScoredMirrorPR(pr=pr, base_score=8.0, token_score=12.0)
scored.files = [
MirrorFile(
filename='src/lib.py',
previous_filename=None,
status='modified',
additions=3,
deletions=1,
changes=4,
is_binary=False,
head_content=blob,
base_content=blob,
)
]
return scored


def _build_eval_with_payloads() -> MinerEvaluation:
eval_ = MinerEvaluation(uid=1, hotkey='hotkey_1', github_id='12345', github_pat='secret')
legacy_pr = _make_pr(uid=1)
legacy_pr.file_changes = [_make_file_change()]
legacy_pr.issues = [_make_issue()]
legacy_pr.base_score = 4.0
legacy_pr.token_score = 6.0
eval_.merged_pull_requests = [legacy_pr]
eval_.mirror_merged_prs = [_make_scored_mirror_pr()]
eval_.unique_repos_contributed_to = {'owner/repo'}
return eval_


class TestStoreOrUseCachedEvaluation:
def test_legitimate_zero_prs_does_not_use_cache(self):
validator = _DummyValidator()
Expand Down Expand Up @@ -79,3 +179,83 @@ def test_fetch_failure_after_partial_load_skips_cache_store_and_fallback(self):
cached_eval = validator.evaluation_cache.get(uid=1, hotkey='hotkey_1', github_id='12345')
assert cached_eval is not None
assert cached_eval.total_prs == 2


class TestMinerEvaluationCacheCopies:
def test_lightweight_copy_drops_file_payloads_without_mutating_source(self):
source = _build_eval_with_payloads()

light = MinerEvaluationCache().create_lightweight_copy(source)

assert light.github_pat is None
assert light.merged_pull_requests[0].file_changes is None
assert light.mirror_merged_prs[0].files is None
assert source.github_pat == 'secret'
assert source.merged_pull_requests[0].file_changes == [_make_file_change()]
assert source.mirror_merged_prs[0].files is not None
assert source.mirror_merged_prs[0].files[0].head_content == 'heavy file content'
assert source.mirror_merged_prs[0].files[0].base_content == 'heavy file content'

def test_store_isolates_cache_from_source_mutations(self):
cache = MinerEvaluationCache()
source = _build_eval_with_payloads()

cache.store(source)
source.unique_repos_contributed_to.add('mutated/repo')
source.merged_pull_requests.append(_make_pr(uid=1))
source.merged_pull_requests[0].base_score = 999.0
assert source.merged_pull_requests[0].issues is not None
source.merged_pull_requests[0].issues[0].discovery_earned_score = 999.0
source.mirror_merged_prs.append(_make_scored_mirror_pr(pr_number=101))
source.mirror_merged_prs[0].base_score = 999.0
source.mirror_merged_prs[0].pr.title = 'mutated mirror pr'
source.mirror_merged_prs[0].pr.review_summary.maintainer_changes_requested_count = 99
source.mirror_merged_prs[0].pr.linked_issues[0].title = 'mutated mirror issue'

cached = cache.get(uid=1, hotkey='hotkey_1', github_id='12345')

assert cached is not None
assert cached.unique_repos_contributed_to == {'owner/repo'}
assert len(cached.merged_pull_requests) == 1
assert cached.merged_pull_requests[0].base_score == 4.0
assert cached.merged_pull_requests[0].issues is not None
assert cached.merged_pull_requests[0].issues[0].discovery_earned_score == 0.0
assert len(cached.mirror_merged_prs) == 1
assert cached.mirror_merged_prs[0].base_score == 8.0
assert cached.mirror_merged_prs[0].pr.title == 'mirror pr'
assert cached.mirror_merged_prs[0].pr.review_summary.maintainer_changes_requested_count == 1
assert cached.mirror_merged_prs[0].pr.linked_issues[0].title == 'mirror linked issue'

def test_get_returns_isolated_copy_for_later_cache_hits(self):
cache = MinerEvaluationCache()
cache.store(_build_eval_with_payloads())

first = cache.get(uid=1, hotkey='hotkey_1', github_id='12345')
assert first is not None
first.total_score = 999.0
first.unique_repos_contributed_to.add('mutated/repo')
first.merged_pull_requests[0].base_score = 999.0
assert first.merged_pull_requests[0].issues is not None
first.merged_pull_requests[0].issues[0].discovery_earned_score = 999.0
first.mirror_merged_prs[0].base_score = 999.0
first.mirror_merged_prs[0].pr.title = 'mutated mirror pr'
first.mirror_merged_prs[0].pr.linked_issues[0].title = 'mutated mirror issue'

second = cache.get(uid=1, hotkey='hotkey_1', github_id='12345')

assert second is not None
assert second.total_score == 0.0
assert second.unique_repos_contributed_to == {'owner/repo'}
assert second.merged_pull_requests[0].base_score == 4.0
assert second.merged_pull_requests[0].issues is not None
assert second.merged_pull_requests[0].issues[0].discovery_earned_score == 0.0
assert second.mirror_merged_prs[0].base_score == 8.0
assert second.mirror_merged_prs[0].pr.title == 'mirror pr'
assert second.mirror_merged_prs[0].pr.linked_issues[0].title == 'mirror linked issue'

def test_identity_mismatch_removes_cached_evaluation(self):
cache = MinerEvaluationCache()
cache.store(_build_eval_with_payloads())

assert cache.get(uid=1, hotkey='new_hotkey', github_id='12345') is None
assert cache.get(uid=1, hotkey='hotkey_1', github_id='12345') is None
Loading