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
15 changes: 14 additions & 1 deletion gittensor/utils/mirror/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ def from_dict(cls, data: dict) -> 'MirrorReviewSummary':

@dataclass
class MirrorLinkedIssue:
"""Issue that a PR closes, as nested inside a ``MirrorPullRequest``."""
"""Issue that a PR closes, as nested inside a ``MirrorPullRequest``.

``repository_full_name`` is the home repo of the issue itself — distinct
from the home repo of the PR that closes it. GitHub's ``Closes
owner/other-repo#N`` syntax lets a PR reference issues in different repos;
without this field the validator can't reject cross-repo linked issues
from the issue-bonus multiplier (see ``_is_valid_linked_issue``).
Defaults to ``None`` for mirror snapshots predating the schema addition.
"""

number: int
title: str
Expand All @@ -87,13 +95,17 @@ class MirrorLinkedIssue:
is_transferred: bool
solved_by_pr: Optional[int]
labels: List[MirrorLabel] = field(default_factory=list)
repository_full_name: Optional[str] = None

@classmethod
def from_dict(cls, data: dict) -> 'MirrorLinkedIssue':
# Mirror sometimes serializes github_id as int; coerce to match the
# str-typed field so downstream `==` comparisons with author_github_id
# from MirrorPullRequest don't silently mismatch on type.
author_github_id = data.get('author_github_id')
# Lowercased to match MirrorPullRequest.repo_full_name normalization;
# the cross-repo guard compares the two case-insensitively.
repo_full_name = data.get('repository_full_name')
return cls(
number=data['number'],
title=data.get('title', ''),
Expand All @@ -107,6 +119,7 @@ def from_dict(cls, data: dict) -> 'MirrorLinkedIssue':
is_transferred=bool(data.get('is_transferred', False)),
solved_by_pr=data.get('solved_by_pr'),
labels=[MirrorLabel.from_dict(label) for label in data.get('labels') or []],
repository_full_name=repo_full_name.lower() if repo_full_name else None,
)


Expand Down
17 changes: 17 additions & 0 deletions gittensor/validator/oss_contributions/mirror/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,12 @@ def _is_valid_linked_issue(li: MirrorLinkedIssue, pr: MirrorPullRequest) -> bool
- Reject transferred issues.
- Missing author / self-issue (uses github_id for immutability).
- Issue created after the PR.
- Cross-repo references: when the mirror surfaces the issue's home repo
and it differs from the PR's repo, reject — a ``Closes other/repo#N``
keyword should not pay the issue-bonus multiplier in this repo. Fails
open when ``repository_full_name`` is ``None`` (older mirror snapshots
without the field); the guard arms automatically once das-github-mirror
populates the field.
- Any CLOSED issue must have state_reason=COMPLETED — NOT_PLANNED / reopened
closures never grant a multiplier. Applies regardless of PR state, so the
gate covers OPEN-PR collateral as well.
Expand All @@ -456,6 +462,17 @@ def _is_valid_linked_issue(li: MirrorLinkedIssue, pr: MirrorPullRequest) -> bool
bt.logging.warning(f'Skipping linked issue #{li.number} - created after PR')
return False

# Mirror payload may omit repository identity on older snapshots; only
# gate when the field is populated. Both sides are normalized to lowercase
# at parse time (see ``MirrorLinkedIssue.from_dict`` and
# ``MirrorPullRequest.from_dict``), so a direct ``!=`` is case-correct.
if li.repository_full_name is not None and li.repository_full_name != pr.repo_full_name:
bt.logging.warning(
f'Skipping linked issue #{li.number} - cross-repo reference '
f'(issue in {li.repository_full_name}, PR in {pr.repo_full_name})'
)
return False

# state_reason check applies regardless of PR state — OPEN-PR collateral
# also requires that any CLOSED linked issue closed as COMPLETED.
if li.state == 'CLOSED' and li.state_reason != 'COMPLETED':
Expand Down
62 changes: 61 additions & 1 deletion tests/validator/oss_contributions/mirror/test_scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,9 @@ def _linked_issue(
closed_at: str | None = '2026-04-18T10:00:00Z',
author_association: str | None = 'CONTRIBUTOR',
number: int = 50,
repository_full_name: str | None = None,
):
return {
payload = {
'number': number,
'title': 't',
'state': state,
Expand All @@ -721,6 +722,9 @@ def _linked_issue(
'solved_by_pr': 100,
'labels': [],
}
if repository_full_name is not None:
payload['repository_full_name'] = repository_full_name
return payload


class TestIssueMultiplier:
Expand Down Expand Up @@ -823,6 +827,62 @@ def test_open_issue_on_open_pr_still_valid(self):
assert _is_valid_linked_issue(li, scored.pr) is True


class TestLinkedIssueCrossRepo:
"""Cross-repo `Closes owner/other-repo#N` references must not pay the issue
multiplier on a PR in a different repo (parity with #1019 / PR #1038 on the
legacy path, ported here after the mirror-only cutover #1202).

The mirror payload may omit ``repository_full_name`` on older snapshots; in
that case the guard fails open so existing data behaves as before. Once
das-github-mirror populates the field, the guard arms automatically.
"""

def test_cross_repo_linked_issue_rejected(self):
"""PR in `entrius/gittensor-ui` linked to issue in `outsider/throwaway`
— the guard must reject."""
scored = ScoredPR(pr=_pr()) # repo_full_name='entrius/gittensor-ui'
li = MirrorLinkedIssue.from_dict(_linked_issue(repository_full_name='outsider/throwaway'))
assert _is_valid_linked_issue(li, scored.pr) is False

def test_same_repo_linked_issue_passes(self):
"""Same repo populated explicitly — guard does not fire."""
scored = ScoredPR(pr=_pr())
li = MirrorLinkedIssue.from_dict(_linked_issue(repository_full_name='entrius/gittensor-ui'))
assert _is_valid_linked_issue(li, scored.pr) is True

def test_repo_identity_missing_falls_open(self):
"""Pre-schema mirror rows have no ``repository_full_name``; guard must
not fire (backwards-compat with current production mirror)."""
scored = ScoredPR(pr=_pr())
li = MirrorLinkedIssue.from_dict(_linked_issue()) # field absent
assert li.repository_full_name is None
assert _is_valid_linked_issue(li, scored.pr) is True

def test_cross_repo_case_insensitive(self):
"""Mirror normalizes repo names to lowercase at parse time; the
guard's direct ``!=`` comparison must remain correct against
mixed-case payloads."""
scored = ScoredPR(pr=_pr())
li = MirrorLinkedIssue.from_dict(_linked_issue(repository_full_name='OUTSIDER/Throwaway'))
# Mirror lowercased the field at parse time
assert li.repository_full_name == 'outsider/throwaway'
assert _is_valid_linked_issue(li, scored.pr) is False

def test_cross_repo_multiplier_stays_neutral(self):
"""End-to-end: a cross-repo linked issue should not produce a
STANDARD/MAINTAINER multiplier from ``_calculate_issue_multiplier``."""
scored = ScoredPR(pr=_pr(linked_issues=[_linked_issue(repository_full_name='outsider/throwaway')]))
assert _calculate_issue_multiplier(scored) == 1.0

def test_same_repo_multiplier_still_applies(self):
"""End-to-end regression guard: a known same-repo linked issue still
earns the standard multiplier."""
from gittensor.constants import STANDARD_ISSUE_MULTIPLIER

scored = ScoredPR(pr=_pr(linked_issues=[_linked_issue(repository_full_name='entrius/gittensor-ui')]))
assert _calculate_issue_multiplier(scored) == STANDARD_ISSUE_MULTIPLIER


class TestIssueMultiplierPreference:
def test_prefer_maintainer_authored_when_multiple_valid(self):
"""Legacy parity (PR #673): the issue multiplier should pick a
Expand Down