diff --git a/gittensor/validator/issue_discovery/scan.py b/gittensor/validator/issue_discovery/scan.py index 4f6310663..0a52b3c5f 100644 --- a/gittensor/validator/issue_discovery/scan.py +++ b/gittensor/validator/issue_discovery/scan.py @@ -33,7 +33,6 @@ """ import asyncio -import os from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Set, Tuple @@ -57,6 +56,7 @@ calculate_base_score_for_pr_files, check_merged_branch_eligibility, ) +from gittensor.validator.utils.config import dev_mode_enabled from gittensor.validator.utils.datetime_utils import calculate_time_decay from gittensor.validator.utils.load_weights import ( LanguageConfig, @@ -121,7 +121,7 @@ def _should_include_issue(issue: MirrorIssue) -> bool: discovery rewards in repos they maintain — mirrors the PR-side maintainer skip in ``oss_contributions/mirror/load.py``. Bypassed under DEV_MODE. """ - if not os.environ.get('DEV_MODE') and issue.author_association in MAINTAINER_ASSOCIATIONS: + if not dev_mode_enabled() and issue.author_association in MAINTAINER_ASSOCIATIONS: return False return True diff --git a/gittensor/validator/oss_contributions/mirror/load.py b/gittensor/validator/oss_contributions/mirror/load.py index 029a77ee7..f2306f0dc 100644 --- a/gittensor/validator/oss_contributions/mirror/load.py +++ b/gittensor/validator/oss_contributions/mirror/load.py @@ -14,7 +14,6 @@ merged_count used by ``check_eligibility`` isn't inflated by ineligible PRs. """ -import os from datetime import datetime, timedelta, timezone from typing import Dict, Optional @@ -26,6 +25,7 @@ from gittensor.utils.mirror.models import MirrorPullRequest from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredPR from gittensor.validator.oss_contributions.mirror.scoring import _should_skip_merged_mirror_pr +from gittensor.validator.utils.config import dev_mode_enabled from gittensor.validator.utils.load_weights import RepositoryConfig, resolve_scoring @@ -103,7 +103,7 @@ def _maybe_add_pr( # Silent maintainer skip — logging every maintainer-merged PR would dominate # the skip-reason log. - if not os.environ.get('DEV_MODE') and pr.author_association in MAINTAINER_ASSOCIATIONS: + if not dev_mode_enabled() and pr.author_association in MAINTAINER_ASSOCIATIONS: return if pr.state == 'OPEN': diff --git a/gittensor/validator/oss_contributions/mirror/scoring.py b/gittensor/validator/oss_contributions/mirror/scoring.py index f02564341..c24c357da 100644 --- a/gittensor/validator/oss_contributions/mirror/scoring.py +++ b/gittensor/validator/oss_contributions/mirror/scoring.py @@ -23,7 +23,6 @@ import asyncio import math -import os from dataclasses import dataclass from typing import Dict, List, Optional, Tuple @@ -48,6 +47,7 @@ from gittensor.validator.oss_contributions.scoring import ( calculate_review_quality_multiplier, ) +from gittensor.validator.utils.config import dev_mode_enabled from gittensor.validator.utils.datetime_utils import calculate_time_decay from gittensor.validator.utils.isolated_scoring import isolated_calculate_token_score from gittensor.validator.utils.load_weights import ( @@ -260,7 +260,7 @@ def _should_skip_merged_mirror_pr(scored: ScoredPR, repo_config: RepositoryConfi return True, f'PR #{pr.pr_number} is MERGED but missing merged_at' # Defensive recheck — load already drops these (with DEV_MODE bypass) - if not os.environ.get('DEV_MODE') and pr.author_association in MAINTAINER_ASSOCIATIONS: + if not dev_mode_enabled() and pr.author_association in MAINTAINER_ASSOCIATIONS: return True, f'PR #{pr.pr_number} author is {pr.author_association}' if pr.merged_by_login and pr.merged_by_login.lower() == pr.author_login.lower(): diff --git a/gittensor/validator/utils/config.py b/gittensor/validator/utils/config.py index 39d11f755..7d2e1c457 100644 --- a/gittensor/validator/utils/config.py +++ b/gittensor/validator/utils/config.py @@ -14,6 +14,19 @@ # optional env vars STORE_DB_RESULTS = os.getenv('STORE_DB_RESULTS', 'false').lower() == 'true' +_TRUTHY_ENV_VALUES = frozenset({'1', 'true', 'yes', 'on'}) + + +def dev_mode_enabled() -> bool: + """Whether DEV_MODE bypasses the maintainer filters. + + Read at call time rather than import time so tests and operators can toggle it + per process. Matched by value, not presence: DEV_MODE=false must disable the + bypass on a production validator. + """ + return os.getenv('DEV_MODE', '').strip().lower() in _TRUTHY_ENV_VALUES + + # log values bt.logging.info(f'VALIDATOR_WAIT: {VALIDATOR_WAIT}') bt.logging.info(f'VALIDATOR_STEPS_INTERVAL: {VALIDATOR_STEPS_INTERVAL}') diff --git a/neurons/validator.py b/neurons/validator.py index 4dffa3bf5..62cf0e198 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -16,7 +16,6 @@ # DEALINGS IN THE SOFTWARE. -import os import time from functools import partial from typing import Dict, List, Set @@ -36,7 +35,12 @@ priority_pat_broadcast, priority_pat_check, ) -from gittensor.validator.utils.config import STORE_DB_RESULTS, WANDB_PROJECT, WANDB_VALIDATOR_NAME +from gittensor.validator.utils.config import ( + STORE_DB_RESULTS, + WANDB_PROJECT, + WANDB_VALIDATOR_NAME, + dev_mode_enabled, +) from gittensor.validator.utils.load_weights import RepositoryConfig from gittensor.validator.utils.storage import DatabaseStorage from neurons.base.validator import BaseValidatorNeuron @@ -55,7 +59,7 @@ class Validator(BaseValidatorNeuron): def __init__(self, config=None): super(Validator, self).__init__(config=config) - if os.environ.get('DEV_MODE'): + if dev_mode_enabled(): bt.logging.warning('⚠ DEV_MODE is active — maintainer PR filtering is bypassed') # Ensure PAT storage file exists on boot diff --git a/tests/validator/oss_contributions/mirror/test_load.py b/tests/validator/oss_contributions/mirror/test_load.py index 6bc1003c3..cb7a272b4 100644 --- a/tests/validator/oss_contributions/mirror/test_load.py +++ b/tests/validator/oss_contributions/mirror/test_load.py @@ -202,6 +202,24 @@ def test_dev_mode_bypasses_maintainer_skip(self, monkeypatch): assert len(eval_.merged_prs) == 1 + @pytest.mark.parametrize('dev_mode', ['0', 'false', 'off', 'no', '', ' ']) + def test_dev_mode_disabled_values_keep_maintainer_skip(self, dev_mode, monkeypatch): + # An explicitly-disabled DEV_MODE must not bypass the filter on a + # production validator. + monkeypatch.setenv('DEV_MODE', dev_mode) + client = Mock() + client.get_miner_pulls.return_value = _build_response( + [ + _pr_dict(1, author_association='OWNER'), + _pr_dict(2, author_association='CONTRIBUTOR'), + ] + ) + eval_ = _eval() + load_miner_prs(eval_, _mirror_repos('entrius/gittensor-ui'), client=client) + + assert len(eval_.merged_prs) == 1 + assert eval_.merged_prs[0].pr.pr_number == 2 + # ============================================================================ # Lookback window (sent to the mirror; windowing happens server-side)