diff --git a/packages/cli/src/repowise/cli/commands/update_cmd/persistence.py b/packages/cli/src/repowise/cli/commands/update_cmd/persistence.py index 2adb77fd9..ae546be59 100644 --- a/packages/cli/src/repowise/cli/commands/update_cmd/persistence.py +++ b/packages/cli/src/repowise/cli/commands/update_cmd/persistence.py @@ -16,7 +16,7 @@ import structlog -from repowise.cli.helpers import console, head_commit_ts, run_async, save_state +from repowise.cli.helpers import console, err_console, head_commit_ts, run_async, save_state from repowise.core.analysis.health import HEALTH_ANALYZER_VERSION from .incremental import _build_repo_graph @@ -1251,14 +1251,27 @@ def _run_full_health_rescore( # REPOWISE_GIT_WINDOW_ANCHOR / historical checkouts; override for tests. _FULL_RESCORE_INTERVAL_DAYS = 7.0 +# The malformed-value warning is global, not per-repo. `_full_rescore_interval_days` +# is consulted once per repo in a workspace fan-out, `repowise watch` and post-commit +# hook updates, so a naive warn-in-except would spam the terminal once per repo. Hoist +# the once-per-invocation guarantee into a module-level guard (#1371). +_FULL_RESCORE_WARNING_SENT = False + def _full_rescore_interval_days() -> float: + global _FULL_RESCORE_WARNING_SENT raw = os.environ.get("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", "").strip() if raw: try: return max(0.0, float(raw)) except ValueError: - pass + if not _FULL_RESCORE_WARNING_SENT: + _FULL_RESCORE_WARNING_SENT = True + err_console.print( + "[yellow]Warning:[/yellow] " + f"REPOWISE_FULL_RESCORE_INTERVAL_DAYS must be a number " + f"(got '{raw}'); using {_FULL_RESCORE_INTERVAL_DAYS:.0f}-day default" + ) return _FULL_RESCORE_INTERVAL_DAYS diff --git a/tests/unit/cli/test_health_rescore_gate.py b/tests/unit/cli/test_health_rescore_gate.py index 2a5482fde..1f966d541 100644 --- a/tests/unit/cli/test_health_rescore_gate.py +++ b/tests/unit/cli/test_health_rescore_gate.py @@ -10,6 +10,7 @@ import pytest from repowise.cli.commands.update_cmd.persistence import ( + _full_rescore_interval_days, full_rescore_due, health_analyzer_changed, ) @@ -161,3 +162,71 @@ def test_init_never_stamps_the_cadence_for_a_run_that_scored_nothing(module_name f"{module_name} resolves the cadence stamp without checking that this " f"run produced a health report: {line.strip()}" ) + + +def _capture_warnings(monkeypatch) -> list[str]: + """Collect what ``_full_rescore_interval_days`` warns, via the shared err_console.""" + from repowise.cli import helpers + + printed: list[str] = [] + monkeypatch.setattr( + helpers.err_console, "print", lambda *a, **k: printed.append(" ".join(str(x) for x in a)) + ) + return printed + + +class TestFullRescoreIntervalWarningOnce: + """#1371: a malformed REPOWISE_FULL_RESCORE_INTERVAL_DAYS must warn once per invocation. + + ``_full_rescore_interval_days`` is consulted once per repo in a workspace + fan-out, ``repowise watch`` and post-commit hook updates. A naive warn-in- + except spams one line per repo; the module-level guard collapses it to a + single warning. + """ + + def test_warns_once_across_many_repos(self, monkeypatch): + monkeypatch.setenv("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", "not-a-number") + monkeypatch.setattr( + "repowise.cli.commands.update_cmd.persistence._FULL_RESCORE_WARNING_SENT", False + ) + printed = _capture_warnings(monkeypatch) + + # Simulate the per-repo fan-out: many independent reads in one invocation. + for _ in range(5): + assert _full_rescore_interval_days() == 7.0 + + (line,) = printed + assert "REPOWISE_FULL_RESCORE_INTERVAL_DAYS" in line + assert "not-a-number" in line + assert len(printed) == 1 # not once per repo + + def test_valid_value_stays_silent(self, monkeypatch): + monkeypatch.setenv("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", "7") + monkeypatch.setattr( + "repowise.cli.commands.update_cmd.persistence._FULL_RESCORE_WARNING_SENT", False + ) + printed = _capture_warnings(monkeypatch) + + assert _full_rescore_interval_days() == 7.0 + assert printed == [] + + def test_default_is_unchanged(self, monkeypatch): + monkeypatch.delenv("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", raising=False) + monkeypatch.setattr( + "repowise.cli.commands.update_cmd.persistence._FULL_RESCORE_WARNING_SENT", False + ) + assert _full_rescore_interval_days() == 7.0 + + def test_warning_survives_valid_reads_after_invalid(self, monkeypatch): + """A valid read after the invalid one must not re-arm the guard.""" + monkeypatch.setenv("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", "oops") + monkeypatch.setattr( + "repowise.cli.commands.update_cmd.persistence._FULL_RESCORE_WARNING_SENT", False + ) + printed = _capture_warnings(monkeypatch) + + assert _full_rescore_interval_days() == 7.0 + # Valid now: still no re-warning. + monkeypatch.setenv("REPOWISE_FULL_RESCORE_INTERVAL_DAYS", "7") + assert _full_rescore_interval_days() == 7.0 + assert len(printed) == 1