AI-Pricing-Agent/tests/test_cover_bands.py

174 lines
7.3 KiB
Python

"""COSMOS display bands vs the engine's pricing triggers — two things, one definition each.
They are NOT the same thing and the difference is deliberate:
* `theme.COVER_BANDS` — COSMOS Inventory Planning's own Alpha/Beta shading. Pink at 70 days
is a REPLENISHMENT warning: don't reorder yet.
* `low/high_cover_days` in pricing_rules.yaml — where this engine spends margin, raising 5%
to slow a burn or cutting 5% to clear stock.
Measured on 47 SKUs of the covered line: moving the triggers to the band edges (40/70) would
put six more SKUs on a discount, all of them in the 70-90 day window. So the bands match COSMOS
for display parity, the triggers stay put, and the UI says which is which.
What these tests protect is that each has exactly ONE definition, and that a change to either
is visible rather than silent.
"""
from __future__ import annotations
import pytest
from config.settings import get_rules
from dashboard import theme
from dashboard.live_data import HIGH_COVER_DAYS, LOW_COVER_DAYS
# ------------------------------------------------------- one definition each
def test_the_engine_reads_its_thresholds_from_config_not_from_code():
r = get_rules()
assert (LOW_COVER_DAYS, HIGH_COVER_DAYS) == (r.low_cover_days, r.high_cover_days)
def test_the_thresholds_are_still_the_backtested_ones():
"""35/90 is what the impact table in pricing_rules.yaml was measured against.
Changing them is allowed — it is a config line — but it invalidates that table, so this
fails loudly rather than letting the documented evidence quietly describe another policy.
"""
assert (LOW_COVER_DAYS, HIGH_COVER_DAYS) == (35, 90), (
"cover triggers changed — re-measure the impact table in pricing_rules.yaml")
def test_bands_match_the_cosmos_legend_exactly():
"""Alpha 0-20-40-70-100, Beta 0-15-30-60-90 — straight off the INVP legend."""
assert [b[0] for b in theme.COVER_BANDS["alpha"]] == [20, 40, 70, 100, None]
assert [b[0] for b in theme.COVER_BANDS["beta"]] == [15, 30, 60, 90, None]
@pytest.mark.parametrize("days,word", [
(0, "very low"), (19, "very low"), (20, "low"), (39, "low"),
(40, "healthy"), (69, "healthy"), (70, "high"), (99, "high"),
(100, "excess"), (5000, "excess"),
])
def test_alpha_band_boundaries(days, word):
assert theme.cover_band(days, "alpha")[1] == word
@pytest.mark.parametrize("days,word", [
(14, "very low"), (15, "low"), (29, "low"), (30, "healthy"),
(59, "healthy"), (60, "high"), (89, "high"), (90, "excess"),
])
def test_beta_bands_are_tighter_than_alpha(days, word):
assert theme.cover_band(days, "beta")[1] == word
def test_an_unknown_cover_gets_no_colour_verdict():
"""A missing number must not be shaded as though we had judged it."""
colour, word = theme.cover_band(None)
assert word == "unknown" and colour == theme.BORDER
def test_an_unknown_class_falls_back_to_alpha_rather_than_raising():
assert theme.cover_band(50, "gamma") == theme.cover_band(50, "alpha")
# ------------------------------------------------- matching COSMOS's own figure
def _cover(cosmos, onhand):
"""The reconciliation `build_live_sku` performs, in one line."""
return int(cosmos) if cosmos else onhand
def test_cover_matches_cosmos_when_cosmos_has_an_answer():
"""The dashboard must not quote a different number from the INVP grid for one SKU.
Real values for UBMICROFIBERDUVETTWINWHITE: 3,999 on hand + 1,030 inbound = 5,029, over a
64/day 7-day velocity = 78 days. On-hand only is 63.
"""
assert _cover(78, 63) == 78
def test_a_zero_from_cosmos_with_stock_on_hand_falls_back():
"""`coverDays: 0` on a stocked SKU is a missing answer, not a full shelf.
UBMICROFIBERBS4PCFULLGREY: 167 units, 334 real days of cover, COSMOS returned 0. Taken
literally it satisfies neither inventory rule and silences both.
"""
assert _cover(0, 334) == 334
assert _cover(None, 334) == 334
def test_a_genuine_empty_shelf_stays_zero():
"""No stock and no velocity must not be inflated into cover by the fallback."""
assert _cover(0, 0) == 0
def test_matching_cosmos_can_mask_a_thin_shelf_and_that_is_recorded():
"""The accepted trade-off, pinned so it is a decision rather than a surprise.
UBMICROFIBERBS4PCKINGWHITE holds 12 days on the shelf and 84 counting inbound, so matching
COSMOS stops it triggering LOW_STOCK. If the shipment slips, nothing protects it.
"""
onhand, cosmos = 12, 84
assert 0 < onhand <= LOW_COVER_DAYS # would have raised on shelf stock alone
matched = _cover(cosmos, onhand)
assert not (0 < matched <= LOW_COVER_DAYS) # ...and does not, once inbound counts
# ------------------------------------------- the gap between band and trigger
def test_there_is_a_window_where_the_band_warns_and_the_engine_does_not_act():
"""The 70-90 day window on Alpha. Six real SKUs sit in it.
This is the state that looked like a bug on screen, so it is pinned: the tile must be able
to detect it in order to explain it.
"""
band_warns = theme.cover_band(82, "alpha")[1] == "high"
engine_acts = 0 < 82 <= LOW_COVER_DAYS or 82 >= HIGH_COVER_DAYS
assert band_warns and not engine_acts
@pytest.mark.parametrize("days", [12, 30, 95, 140])
def test_where_the_engine_acts_the_band_is_never_green(days):
"""The colour may be more cautious than the engine; it must never be LESS.
A green tile beside a SKU the engine is discounting would be the genuinely dangerous
direction of disagreement.
"""
acts = 0 < days <= LOW_COVER_DAYS or days >= HIGH_COVER_DAYS
assert acts
assert theme.cover_band(days, "alpha")[1] != "healthy"
def test_every_acting_cover_is_outside_the_green_band_for_alpha():
"""Swept, not sampled — no Alpha cover may read green while a price move fires.
Alpha is the only class in use: `inv_class` is hardcoded to "alpha" for every SKU in
`live_data.build_live_sku`, so this is the invariant that actually holds today.
"""
for days in range(0, 400):
if 0 < days <= LOW_COVER_DAYS or days >= HIGH_COVER_DAYS:
assert theme.cover_band(days, "alpha")[1] != "healthy", days
def test_beta_would_break_that_invariant_if_it_were_ever_populated():
"""A latent conflict, recorded rather than hidden.
Beta's healthy band starts at 30 days, but the raise trigger is a single global 35 — so a
Beta SKU at 30-35 days would be repriced while its own tile showed green. Nothing does
that today because `inv_class` is hardcoded to "alpha"; the moment COSMOS's real
classification is plumbed through, the cover triggers have to become per-class and Beta's
raise threshold must drop below 30.
This test passes on the CURRENT behaviour and is written to fail the day Beta appears with
the global thresholds still in force.
"""
conflict = [d for d in range(0, 400)
if (0 < d <= LOW_COVER_DAYS or d >= HIGH_COVER_DAYS)
and theme.cover_band(d, "beta")[1] == "healthy"]
assert conflict == list(range(30, LOW_COVER_DAYS + 1)), conflict
# The guard that keeps it harmless: no SKU is Beta.
from dashboard import live_data
import inspect
src = inspect.getsource(live_data.build_live_sku)
assert 'inv_class="alpha"' in src, (
"a SKU can now be Beta — make the cover triggers per-class before shipping this")