"""Tests for evaluate_credits_notices — pure threshold reconciliation policy (L4.1). All tests use fresh latch = {"active": set(), "seen_below_90": False, "usage_band": None} per scenario. CreditsState is constructed directly (not parsed from headers). """ from __future__ import annotations import pytest from agent.credits_tracker import ( CREDITS_NOTICE_KIND, CREDITS_RESTORED_TTL_MS, AgentNotice, CreditsState, evaluate_credits_notices, ) # ── Helpers ────────────────────────────────────────────────────────────────── def fresh_latch() -> dict: return {"active": set(), "seen_below_90": False, "usage_band": None} def state_with_fraction( uf: float | None, *, paid_access: bool = True, denominator_kind: str = "subscription_cap", purchased_micros: int = 0, purchased_usd: str = "0.00", subscription_limit_usd: str | None = "20.00", ) -> CreditsState: """Build a minimal CreditsState that yields the desired used_fraction. used_fraction = (limit - subscription_micros) / limit When uf is None, we set limit to None so used_fraction returns None. """ if uf is None: return CreditsState( subscription_limit_micros=None, subscription_limit_usd=None, subscription_micros=0, denominator_kind="none", paid_access=paid_access, purchased_micros=purchased_micros, purchased_usd=purchased_usd, ) # We want (limit - sub) / limit == uf → sub = limit * (1 - uf) limit = 20_000_000 # $20 in micros sub = int(limit * (1.0 - uf)) return CreditsState( subscription_limit_micros=limit, subscription_limit_usd=subscription_limit_usd, subscription_micros=sub, denominator_kind=denominator_kind, paid_access=paid_access, purchased_micros=purchased_micros, purchased_usd=purchased_usd, ) # ── Scenario 1: crossing 90% threshold ─────────────────────────────────────── class TestWarn90Crossing: def test_below_lowest_band_no_notice_but_latch_set(self): latch = fresh_latch() s = state_with_fraction(0.10) # below the 50% band to_show, to_clear = evaluate_credits_notices(s, latch) assert all(n.key != "credits.usage" for n in to_show) assert "credits.usage" not in to_clear assert latch["seen_below_90"] is True def test_crossing_to_90_fires_once(self): latch = fresh_latch() # First call: uf < 0.5 — sets seen_below_90 (below lowest band) s1 = state_with_fraction(0.10) evaluate_credits_notices(s1, latch) # Second call: uf >= 0.9 — should fire the usage band at 90 s2 = state_with_fraction(0.95) to_show, to_clear = evaluate_credits_notices(s2, latch) keys = [n.key for n in to_show] assert "credits.usage" in keys assert "credits.usage" not in to_clear def test_no_refire_on_repeated_over_90(self): latch = fresh_latch() s_below = state_with_fraction(0.10) evaluate_credits_notices(s_below, latch) s_over = state_with_fraction(0.95) evaluate_credits_notices(s_over, latch) # Third call: still ≥ 0.9 — must NOT re-fire to_show, to_clear = evaluate_credits_notices(s_over, latch) assert all(n.key != "credits.usage" for n in to_show) assert "credits.usage" not in to_clear # ── Scenario 2: recovery + re-cross ────────────────────────────────────────── class TestWarn90RecoveryReCross: def test_recovery_clears_warn90(self): latch = fresh_latch() # Cross below → above evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.95), latch) # Recovery: uf drops back below ALL bands → usage notice clears entirely to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.10), latch) assert "credits.usage" in to_clear assert "credits.usage" not in latch["active"] def test_recross_after_recovery_fires_again(self): latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.95), latch) evaluate_credits_notices(state_with_fraction(0.10), latch) # recovery # Re-cross: uf >= 0.9 again — should fire again because the band is clearable to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.95), latch) keys = [n.key for n in to_show] assert "credits.usage" in keys # ── Scenario 3: open-already-over (hybrid Q3 gate) ─────────────────────────── class TestOpenAlreadyOver: def test_warn90_does_not_fire_without_seen_below_90(self): """First call uf≥0.9 with seen_below_90=False — warn90 must NOT fire.""" latch = fresh_latch() assert latch["seen_below_90"] is False s = state_with_fraction(0.95) to_show, to_clear = evaluate_credits_notices(s, latch) assert all(n.key != "credits.usage" for n in to_show) assert "credits.usage" not in to_clear # ── Scenario 3b: boundary — exact 0.9 and just-below-1.0 ──────────────────── class TestBoundaryFractions: def test_exact_0_9_fires_warn90(self): """used_fraction == 0.9 exactly must fire warn90 (threshold is inclusive).""" latch = fresh_latch() # First: prime seen_below_90 with a sub-50% observation evaluate_credits_notices(state_with_fraction(0.10), latch) # Now construct a state where used_fraction is EXACTLY 0.9: # subscription_limit_micros=20_000_000, subscription_micros=2_000_000 # → used = 18_000_000 / 20_000_000 = 0.9 exactly s = CreditsState( subscription_limit_micros=20_000_000, subscription_limit_usd="20.00", subscription_micros=2_000_000, denominator_kind="subscription_cap", paid_access=True, ) assert s.used_fraction == 0.9 to_show, to_clear = evaluate_credits_notices(s, latch) keys = [n.key for n in to_show] assert "credits.usage" in keys assert "credits.usage" not in to_clear def test_just_below_1_0_does_not_fire_grant_spent(self): """subscription_micros = limit - 1 (used_fraction just under 1.0) must NOT fire grant_spent. Locks the boundary so a future used_fraction clamp refactor cannot fire grant_spent a micro early. """ latch = fresh_latch() limit = 20_000_000 s = CreditsState( subscription_limit_micros=limit, subscription_limit_usd="20.00", subscription_micros=1, # limit - 1 → used_fraction < 1.0 denominator_kind="subscription_cap", purchased_micros=5_000_000, purchased_usd="5.00", paid_access=True, ) assert s.used_fraction is not None and s.used_fraction < 1.0 to_show, to_clear = evaluate_credits_notices(s, latch) assert all(n.key != "credits.grant_spent" for n in to_show) assert "credits.grant_spent" not in to_clear # ── Scenario 4: grant_spent ─────────────────────────────────────────────────── class TestGrantSpent: def _grant_state(self, purchased_micros: int = 12_340_000) -> CreditsState: return state_with_fraction( 1.0, denominator_kind="subscription_cap", purchased_micros=purchased_micros, purchased_usd="12.34", ) def test_grant_spent_fires_on_first_obs(self): """No crossing gate for grant_spent — fires immediately on first obs.""" latch = fresh_latch() to_show, to_clear = evaluate_credits_notices(self._grant_state(), latch) keys = [n.key for n in to_show] assert "credits.grant_spent" in keys def test_grant_spent_no_refire(self): latch = fresh_latch() evaluate_credits_notices(self._grant_state(), latch) to_show, to_clear = evaluate_credits_notices(self._grant_state(), latch) assert all(n.key != "credits.grant_spent" for n in to_show) assert "credits.grant_spent" not in to_clear def test_grant_spent_clears_when_purchased_zero(self): latch = fresh_latch() evaluate_credits_notices(self._grant_state(), latch) # Now purchased → 0: grant_cond becomes False s_no_purchase = state_with_fraction( 1.0, denominator_kind="subscription_cap", purchased_micros=0, purchased_usd="0.00", ) to_show, to_clear = evaluate_credits_notices(s_no_purchase, latch) assert "credits.grant_spent" in to_clear assert all(n.key != "credits.grant_spent" for n in to_show) # ── Scenario 5: depleted + recovery ────────────────────────────────────────── class TestDepleted: def test_depleted_fires_level_error_kind_sticky(self): latch = fresh_latch() s = CreditsState(paid_access=False) to_show, to_clear = evaluate_credits_notices(s, latch) depleted_notices = [n for n in to_show if n.key == "credits.depleted"] assert len(depleted_notices) == 1 n = depleted_notices[0] assert n.level == "error" assert n.kind == CREDITS_NOTICE_KIND def test_recovery_emits_clear_and_restored(self): latch = fresh_latch() # Fire depleted evaluate_credits_notices(CreditsState(paid_access=False), latch) # Now recovered to_show, to_clear = evaluate_credits_notices(CreditsState(paid_access=True), latch) assert "credits.depleted" in to_clear restored = [n for n in to_show if n.key == "credits.restored"] assert len(restored) == 1 r = restored[0] assert r.level == "success" assert r.kind == "ttl" assert r.ttl_ms == CREDITS_RESTORED_TTL_MS def test_depleted_refires_after_recovery(self): latch = fresh_latch() evaluate_credits_notices(CreditsState(paid_access=False), latch) evaluate_credits_notices(CreditsState(paid_access=True), latch) # Goes depleted again to_show, to_clear = evaluate_credits_notices(CreditsState(paid_access=False), latch) keys = [n.key for n in to_show] assert "credits.depleted" in keys # ── Scenario 5b: free-model suppression of the depleted notice ─────────────── class TestDepletedFreeModelSuppression: def test_depleted_suppressed_when_model_is_free(self): latch = fresh_latch() s = CreditsState(paid_access=False) to_show, to_clear = evaluate_credits_notices(s, latch, model_is_free=True) assert all(n.key != "credits.depleted" for n in to_show) assert "credits.depleted" not in latch["active"] assert to_clear == [] def test_switch_to_free_model_clears_without_restored(self): latch = fresh_latch() # Depleted on a paid model → notice fires evaluate_credits_notices(CreditsState(paid_access=False), latch) assert "credits.depleted" in latch["active"] # Same depleted account, but now on a free model → clear, NO "restored" to_show, to_clear = evaluate_credits_notices( CreditsState(paid_access=False), latch, model_is_free=True ) assert "credits.depleted" in to_clear assert "credits.depleted" not in latch["active"] assert all(n.key != "credits.restored" for n in to_show) def test_switch_back_to_paid_model_while_depleted_reshows(self): latch = fresh_latch() evaluate_credits_notices(CreditsState(paid_access=False), latch) evaluate_credits_notices(CreditsState(paid_access=False), latch, model_is_free=True) # Back on a paid model, still depleted → notice re-fires to_show, to_clear = evaluate_credits_notices(CreditsState(paid_access=False), latch) keys = [n.key for n in to_show] assert "credits.depleted" in keys assert "credits.depleted" in latch["active"] def test_genuine_recovery_on_free_model_no_spurious_restored(self): """Recovery observed while suppressed (notice never shown) → nothing to clear, no 'restored' (there was no visible depleted state to restore).""" latch = fresh_latch() evaluate_credits_notices(CreditsState(paid_access=False), latch, model_is_free=True) to_show, to_clear = evaluate_credits_notices( CreditsState(paid_access=True), latch, model_is_free=True ) assert to_clear == [] assert all(n.key != "credits.restored" for n in to_show) def test_genuine_recovery_still_emits_restored_when_notice_active(self): """paid_access flip back to True with the notice showing → clear + restored (unchanged behaviour, regardless of the model-free flag).""" latch = fresh_latch() evaluate_credits_notices(CreditsState(paid_access=False), latch) to_show, to_clear = evaluate_credits_notices( CreditsState(paid_access=True), latch, model_is_free=True ) assert "credits.depleted" in to_clear restored = [n for n in to_show if n.key == "credits.restored"] assert len(restored) == 1 def test_free_flag_does_not_affect_other_notices(self): """Usage-band and grant notices are independent of the model-free gate.""" latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch, model_is_free=True) to_show, _ = evaluate_credits_notices( state_with_fraction(0.95, paid_access=False), latch, model_is_free=True ) keys = [n.key for n in to_show] assert "credits.usage" in keys assert "credits.depleted" not in keys # ── Scenario 5c: is_free_tier_model (local-data-only check) ────────────────── class TestIsFreeTierModel: def test_free_suffix_is_free(self): from agent.credits_tracker import is_free_tier_model assert is_free_tier_model("nvidia/nemotron-3-ultra:free") is True assert is_free_tier_model("Hermes-4-70B:free", "https://inference-api.nousresearch.com") is True def test_empty_or_paid_model_is_not_free(self): from agent.credits_tracker import is_free_tier_model assert is_free_tier_model("") is False assert is_free_tier_model("Hermes-4-405B") is False def test_pricing_cache_peek_zero_priced_model(self, monkeypatch): from agent.credits_tracker import is_free_tier_model import hermes_cli.models as models_mod # The picker keys the cache on the pre-/v1 root (get_pricing_for_provider # strips a trailing /v1 before fetch_models_with_pricing). monkeypatch.setattr( models_mod, "_pricing_cache", { "https://inference-api.nousresearch.com": { "some/zero-priced": {"prompt": "0", "completion": "0"}, "some/paid": {"prompt": "0.000001", "completion": "0.000002"}, } }, ) # The agent holds the /v1-suffixed URL (DEFAULT_NOUS_INFERENCE_URL) — # the helper must normalize it down to the picker's cache key. base = "https://inference-api.nousresearch.com/v1" assert is_free_tier_model("some/zero-priced", base) is True assert is_free_tier_model("some/paid", base) is False # Pre-stripped and trailing-slash variants resolve to the same key. assert is_free_tier_model("some/zero-priced", "https://inference-api.nousresearch.com/") is True assert is_free_tier_model("some/zero-priced", "https://inference-api.nousresearch.com/v1/") is True def test_cache_miss_is_not_free_and_no_fetch(self, monkeypatch): from agent.credits_tracker import is_free_tier_model import hermes_cli.models as models_mod monkeypatch.setattr(models_mod, "_pricing_cache", {}) def _boom(*args, **kwargs): # any network attempt fails the test raise AssertionError("is_free_tier_model must never hit the network") import urllib.request monkeypatch.setattr(urllib.request, "urlopen", _boom) assert is_free_tier_model("some/model", "https://inference-api.nousresearch.com/v1") is False def test_exception_fails_open_to_false(self, monkeypatch): from agent.credits_tracker import is_free_tier_model import hermes_cli.models as models_mod class _Exploding: def get(self, *_a, **_kw): raise RuntimeError("boom") monkeypatch.setattr(models_mod, "_pricing_cache", _Exploding()) assert is_free_tier_model("some/model", "https://inference-api.nousresearch.com") is False # ── Scenario 6: denominator none (uf is None) ──────────────────────────────── class TestDenominatorNone: def test_no_warn90_when_uf_none(self): latch = fresh_latch() s = state_with_fraction(None) to_show, to_clear = evaluate_credits_notices(s, latch) assert all(n.key != "credits.usage" for n in to_show) assert "credits.usage" not in to_clear def test_no_grant_spent_when_uf_none(self): latch = fresh_latch() s = CreditsState( subscription_limit_micros=None, denominator_kind="none", purchased_micros=5_000_000, purchased_usd="5.00", ) to_show, to_clear = evaluate_credits_notices(s, latch) assert all(n.key != "credits.grant_spent" for n in to_show) def test_warn90_clears_when_uf_becomes_none(self): """If warn90 was active and uf becomes None, it should clear.""" latch = fresh_latch() # Establish usage notice active: cross below → above evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.95), latch) assert "credits.usage" in latch["active"] # Now uf becomes None (denominator changed to "none") s_none = state_with_fraction(None) to_show, to_clear = evaluate_credits_notices(s_none, latch) assert "credits.usage" in to_clear assert "credits.usage" not in latch["active"] # ── Scenario 7: copy / verbatim USD strings ────────────────────────────────── class TestNoticeCopy: def test_warn90_contains_verbatim_subscription_limit_usd(self): latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) s = state_with_fraction(0.95, subscription_limit_usd="20.00") to_show, _ = evaluate_credits_notices(s, latch) warn_notice = next(n for n in to_show if n.key == "credits.usage") assert "$20.00" in warn_notice.text assert "cap" in warn_notice.text def test_grant_spent_contains_verbatim_purchased_usd(self): latch = fresh_latch() s = state_with_fraction( 1.0, denominator_kind="subscription_cap", purchased_micros=12_340_000, purchased_usd="12.34", ) to_show, _ = evaluate_credits_notices(s, latch) grant_notice = next(n for n in to_show if n.key == "credits.grant_spent") assert "$12.34" in grant_notice.text assert "top-up left" in grant_notice.text def test_depleted_mentions_credits_command(self): latch = fresh_latch() s = CreditsState(paid_access=False) to_show, _ = evaluate_credits_notices(s, latch) depleted_notice = next(n for n in to_show if n.key == "credits.depleted") assert "/credits" in depleted_notice.text # ── Scenario 8: severity order in a single call ────────────────────────────── class TestSeverityOrder: def test_multiple_new_notices_ordered_ascending_severity(self): """grant_spent < depleted in to_show when both fire in one call. (usage is suppressed here: purchased>0 — see TestTopUpSuppression. usage + grant_spent are now mutually exclusive by design.) """ latch = {"active": set(), "seen_below_90": True, "usage_band": None} # Build state: subscription_cap, uf >= 1.0, purchased_micros > 0, NOT paid_access # grant_cond: subscription_cap + uf >= 1.0 + purchased > 0 ✓ # depleted_cond: not paid_access ✓ # usage band: suppressed (purchased > 0) s = CreditsState( subscription_limit_micros=20_000_000, subscription_limit_usd="20.00", subscription_micros=0, # uf = 1.0 denominator_kind="subscription_cap", purchased_micros=5_000_000, purchased_usd="5.00", paid_access=False, ) to_show, _ = evaluate_credits_notices(s, latch) keys = [n.key for n in to_show] assert "credits.usage" not in keys assert "credits.grant_spent" in keys assert "credits.depleted" in keys # Ascending severity: grant_spent before depleted assert keys.index("credits.grant_spent") < keys.index("credits.depleted") def test_usage_before_depleted_without_topup(self): """With no top-up funds, usage fires and precedes depleted.""" latch = {"active": set(), "seen_below_90": True, "usage_band": None} s = CreditsState( subscription_limit_micros=20_000_000, subscription_limit_usd="20.00", subscription_micros=0, # uf = 1.0 denominator_kind="subscription_cap", purchased_micros=0, purchased_usd="0.00", paid_access=False, ) to_show, _ = evaluate_credits_notices(s, latch) keys = [n.key for n in to_show] assert "credits.usage" in keys assert "credits.depleted" in keys assert keys.index("credits.usage") < keys.index("credits.depleted") # ── Scenario 8b: top-up suppression of the usage gauge ─────────────────────── class TestTopUpSuppression: """purchased_micros > 0 suppresses the sub-cap usage gauge: the cap is the wrong denominator for an account that can keep spending top-up funds.""" def test_no_usage_band_with_topup_at_90pct(self): latch = fresh_latch() evaluate_credits_notices( state_with_fraction(0.10, purchased_micros=5_000_000, purchased_usd="5.00"), latch, ) to_show, to_clear = evaluate_credits_notices( state_with_fraction(0.95, purchased_micros=5_000_000, purchased_usd="5.00"), latch, ) assert all(n.key != "credits.usage" for n in to_show) assert latch["usage_band"] is None def test_topup_landing_mid_session_clears_active_band(self): """A showing 90% warn must clear when a top-up lands (purchased 0 → >0).""" latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.95), latch) assert latch["usage_band"] == 90 to_show, to_clear = evaluate_credits_notices( state_with_fraction(0.95, purchased_micros=10_000_000, purchased_usd="10.00"), latch, ) assert "credits.usage" in to_clear assert latch["usage_band"] is None assert all(n.key != "credits.usage" for n in to_show) def test_band_resumes_after_topup_spent(self): """purchased back to 0 with usage still in-band → gauge resumes.""" latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices( state_with_fraction(0.95, purchased_micros=10_000_000, purchased_usd="10.00"), latch, ) assert latch["usage_band"] is None to_show, _ = evaluate_credits_notices(state_with_fraction(0.95), latch) n = next(n for n in to_show if n.key == "credits.usage") assert "90%" in n.text assert latch["usage_band"] == 90 def test_grant_spent_still_fires_with_topup(self): """Suppression only affects the gauge — grant_spent (which NEEDS purchased>0) is untouched.""" latch = fresh_latch() s = state_with_fraction( 1.0, denominator_kind="subscription_cap", purchased_micros=12_340_000, purchased_usd="12.34", ) to_show, _ = evaluate_credits_notices(s, latch) keys = [n.key for n in to_show] assert "credits.grant_spent" in keys assert "credits.usage" not in keys def test_depleted_unaffected_by_topup_suppression(self): latch = fresh_latch() s = CreditsState(paid_access=False, purchased_micros=5_000_000, purchased_usd="5.00") to_show, _ = evaluate_credits_notices(s, latch) assert any(n.key == "credits.depleted" for n in to_show) # ── Invariant: never fire + clear same key in one call ──────────────────────── class TestNoFireAndClearSameKey: def test_usage_never_both_fired_and_cleared(self): latch = fresh_latch() # Run many state transitions; across each, assert no key is in both lists states = [ state_with_fraction(0.10), state_with_fraction(0.95), state_with_fraction(0.10), state_with_fraction(0.95), state_with_fraction(None), ] for s in states: to_show, to_clear = evaluate_credits_notices(s, latch) fired_keys = {n.key for n in to_show} cleared_keys = set(to_clear) overlap = fired_keys & cleared_keys assert not overlap, f"Key(s) both fired and cleared: {overlap}" def test_depleted_never_both_fired_and_cleared(self): latch = fresh_latch() states = [ CreditsState(paid_access=False), CreditsState(paid_access=True), CreditsState(paid_access=False), ] for s in states: to_show, to_clear = evaluate_credits_notices(s, latch) fired_keys = {n.key for n in to_show} cleared_keys = set(to_clear) overlap = fired_keys & cleared_keys assert not overlap, f"Key(s) both fired and cleared: {overlap}" # ── Scenario 9: escalating usage bands (50 → 75 → 90) ──────────────────────── class TestUsageBands: """The usage notice shows the HIGHEST crossed band as a single escalating line.""" def _band_text(self, to_show): n = next((n for n in to_show if n.key == "credits.usage"), None) return n.text if n else None def test_50_band_fires_info(self): latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) # prime to_show, _ = evaluate_credits_notices(state_with_fraction(0.55), latch) n = next(n for n in to_show if n.key == "credits.usage") assert "50%" in n.text and n.level == "info" assert latch["usage_band"] == 50 def test_75_band_fires_warn(self): latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) to_show, _ = evaluate_credits_notices(state_with_fraction(0.80), latch) n = next(n for n in to_show if n.key == "credits.usage") assert "75%" in n.text and n.level == "warn" assert latch["usage_band"] == 75 def test_climb_replaces_band(self): """Climbing 50→75→90 replaces the single line (clear old + show new).""" latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) # 55% → 50 band evaluate_credits_notices(state_with_fraction(0.55), latch) assert latch["usage_band"] == 50 # 80% → climbs to 75, clearing the 50 line to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.80), latch) assert "credits.usage" in to_clear assert "75%" in self._band_text(to_show) assert latch["usage_band"] == 75 # 95% → climbs to 90 to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.95), latch) assert "credits.usage" in to_clear assert "90%" in self._band_text(to_show) assert latch["usage_band"] == 90 def test_step_down_on_recovery(self): """Recovering steps the band back down, then clears below the lowest band.""" latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.95), latch) assert latch["usage_band"] == 90 # drop to 80% → steps down to 75 to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.80), latch) assert "credits.usage" in to_clear assert "75%" in self._band_text(to_show) # drop to 55% → steps down to 50 to_show, _ = evaluate_credits_notices(state_with_fraction(0.55), latch) assert "50%" in self._band_text(to_show) # drop below 50% → clears entirely to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.10), latch) assert "credits.usage" in to_clear assert latch["usage_band"] is None def test_no_refire_same_band(self): latch = fresh_latch() evaluate_credits_notices(state_with_fraction(0.10), latch) evaluate_credits_notices(state_with_fraction(0.80), latch) # fires 75 # still 80% → same band, no re-emit, no clear to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.80), latch) assert all(n.key != "credits.usage" for n in to_show) assert "credits.usage" not in to_clear def test_exact_band_boundaries_inclusive(self): """Thresholds are inclusive: exactly 0.50 / 0.75 / 0.90 land in their band.""" for uf, want in [(0.50, 50), (0.75, 75), (0.90, 90)]: latch = fresh_latch() latch["seen_below_90"] = True # allow firing evaluate_credits_notices(state_with_fraction(uf), latch) assert latch["usage_band"] == want, (uf, latch["usage_band"]) def test_open_below_lowest_band_no_notice(self): latch = fresh_latch() to_show, to_clear = evaluate_credits_notices(state_with_fraction(0.30), latch) assert all(n.key != "credits.usage" for n in to_show) assert latch["usage_band"] is None