From 6eff8aab0fe94ad56ad173658fb47c92ba7475d8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 May 2026 22:11:17 +0000 Subject: [PATCH] fix(core): adaptive_threshold input validation + boundary tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Risponde alla code review di 7dc2fda: - Valida percentile in [0,1] e 0 < min_days < target_days, raise ValueError quando out-of-range. Fail-fast invece di IndexError o silent wrong result. - Aggiunge test boundary esattamente a min_days*96 e target_days*96 (spec §9.1 item 9 era mancante). - Aggiunge 4 test sulle nuove guards. - Fix typo docstring (Determinismic → Deterministic). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cerbero_bite/core/adaptive_threshold.py | 11 ++- tests/unit/test_adaptive_threshold.py | 79 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/cerbero_bite/core/adaptive_threshold.py b/src/cerbero_bite/core/adaptive_threshold.py index 34e08f8..0fbc334 100644 --- a/src/cerbero_bite/core/adaptive_threshold.py +++ b/src/cerbero_bite/core/adaptive_threshold.py @@ -2,7 +2,7 @@ Spec: ``docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md``. -Determinismic, no I/O. La query del repository è effettuata dal caller +Deterministic, no I/O. La query del repository è effettuata dal caller (``runtime/entry_cycle``) prima di chiamare questa funzione. """ @@ -45,6 +45,15 @@ def compute_adaptive_threshold( disabilitato), altrimenti il percentile della finestra, bounded dal floor. """ + if not (Decimal(0) <= percentile <= Decimal(1)): + raise ValueError( + f"percentile must be in [0, 1], got {percentile}" + ) + if min_days <= 0 or target_days <= 0 or min_days >= target_days: + raise ValueError( + f"require 0 < min_days < target_days, " + f"got min_days={min_days}, target_days={target_days}" + ) if not history: return None n_ticks = len(history) diff --git a/tests/unit/test_adaptive_threshold.py b/tests/unit/test_adaptive_threshold.py index 8d127b1..719a56a 100644 --- a/tests/unit/test_adaptive_threshold.py +++ b/tests/unit/test_adaptive_threshold.py @@ -140,3 +140,82 @@ def test_median_percentile_returns_p50() -> None: # P50 di [1..200] = (200+1)/2 = 100.5 assert out is not None assert Decimal("100") <= out <= Decimal("101") + + +def test_exactly_min_days_uses_min_window() -> None: + """Boundary: history == min_days*96 → window is min_days (per spec 9.1 item 9).""" + history = _ramp(30 * 96) # exactly 2880 ticks + out = compute_adaptive_threshold( + history=history, + percentile=Decimal("0.25"), + absolute_floor=Decimal("0"), + min_days=30, + target_days=60, + ) + # Window = last 2880 = all history; P25 of ramp [1..2880] ≈ 720.75 + assert out is not None + assert Decimal("720") <= out <= Decimal("721") + + +def test_exactly_target_days_uses_target_window() -> None: + """Boundary: history == target_days*96 → window is target_days.""" + history = _ramp(60 * 96) # exactly 5760 ticks + out = compute_adaptive_threshold( + history=history, + percentile=Decimal("0.25"), + absolute_floor=Decimal("0"), + min_days=30, + target_days=60, + ) + # Window = last 5760 = all history; P25 of ramp [1..5760] ≈ 1440.75 + assert out is not None + assert Decimal("1440") <= out <= Decimal("1441") + + +# --------------------------------------------------------------------------- +# Input validation +# --------------------------------------------------------------------------- + + +def test_invalid_percentile_above_one_raises() -> None: + with pytest.raises(ValueError, match="percentile must be in"): + compute_adaptive_threshold( + history=[Decimal("1")] * 200, + percentile=Decimal("1.5"), + absolute_floor=Decimal("0"), + min_days=30, + target_days=60, + ) + + +def test_invalid_percentile_negative_raises() -> None: + with pytest.raises(ValueError, match="percentile must be in"): + compute_adaptive_threshold( + history=[Decimal("1")] * 200, + percentile=Decimal("-0.1"), + absolute_floor=Decimal("0"), + min_days=30, + target_days=60, + ) + + +def test_invalid_window_inverted_raises() -> None: + with pytest.raises(ValueError, match="min_days < target_days"): + compute_adaptive_threshold( + history=[Decimal("1")] * 200, + percentile=Decimal("0.25"), + absolute_floor=Decimal("0"), + min_days=60, + target_days=30, + ) + + +def test_invalid_window_zero_raises() -> None: + with pytest.raises(ValueError, match="min_days < target_days"): + compute_adaptive_threshold( + history=[Decimal("1")] * 200, + percentile=Decimal("0.25"), + absolute_floor=Decimal("0"), + min_days=0, + target_days=60, + )