Files
palladium/calculators/Genesys_Token_Calculator/tests/test_sensitivity.py

111 lines
3.9 KiB
Python

"""Sweeps and tornado data."""
from __future__ import annotations
import pytest
from genesyscalc.meters import TokenPrice
from genesyscalc.model import CalculatorInputs, calculate
from genesyscalc.sensitivity import (
DRIVERS,
driver_value,
set_driver,
sweep,
tornado,
)
from genesyscalc.tts import TtsUsage
from genesyscalc.usage import DeflectionMix, VoiceBotUsage, Volumes
@pytest.fixture
def base() -> CalculatorInputs:
return CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=100_000, voice_outbound_monthly=20_000),
users=500,
enabled=frozenset(
{"agent_copilot_named", "bots_voice", "virtual_agent"}
),
mix=DeflectionMix(bot_only_share=0.30, virtual_agent_share=0.15),
price=TokenPrice(),
voice_bot=VoiceBotUsage(calls_per_month=30_000, avg_bot_seconds_per_call=40),
tts=TtsUsage(calls_monthly=100_000),
)
def test_driver_round_trip(base: CalculatorInputs) -> None:
assert driver_value(base, "users") == 500
assert driver_value(set_driver(base, "users", 750), "users") == 750
def test_nested_driver_round_trip(base: CalculatorInputs) -> None:
assert driver_value(base, "concession_pct") == 0.0
moved = set_driver(base, "concession_pct", 0.25)
assert moved.price.concession_pct == pytest.approx(0.25)
assert base.price.concession_pct == 0.0 # inputs are frozen; no mutation
def test_integer_drivers_stay_integers(base: CalculatorInputs) -> None:
assert isinstance(set_driver(base, "users", 512.6).users, int)
assert set_driver(base, "users", 512.6).users == 513
def test_unknown_driver_is_named(base: CalculatorInputs) -> None:
with pytest.raises(KeyError, match="not a known driver"):
driver_value(base, "vibes")
def test_sweep_is_monotone_in_token_price(base: CalculatorInputs) -> None:
rows = sweep(base, "token_price", [0.5, 1.0, 1.5, 2.0])
costs = [r["token_cost_annual"] for r in rows]
assert costs == sorted(costs)
assert len(rows) == 4
def test_sweep_carries_the_driver_value(base: CalculatorInputs) -> None:
rows = sweep(base, "concession_pct", [0.0, 0.25])
assert rows[0]["concession_pct"] == 0.0
assert rows[1]["grand_total_annual"] < rows[0]["grand_total_annual"]
def test_tornado_is_sorted_by_swing(base: CalculatorInputs) -> None:
rows = tornado(base, ["token_price", "users", "bot_seconds", "concession_pct"])
swings = [r["swing"] for r in rows]
assert swings == sorted(swings, reverse=True)
assert all(r["swing"] >= 0 for r in rows)
def test_tornado_brackets_the_baseline(base: CalculatorInputs) -> None:
baseline = calculate(base).grand_total_annual
for row in tornado(base, ["token_price", "users"]):
assert row["low"] <= baseline <= row["high"]
assert row["baseline"] == pytest.approx(baseline)
def test_tornado_skips_inactive_levers() -> None:
"""No TTS and no voice bot in the scenario — those levers are noise."""
plain = CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=1_000),
users=10,
enabled=frozenset({"agent_copilot_named"}),
)
drivers = [r["driver"] for r in tornado(plain, list(DRIVERS))]
assert "bot_seconds" not in drivers
assert "tts_chars_per_call" not in drivers
assert "users" in drivers
def test_tornado_skips_a_lever_that_would_break_the_partition() -> None:
"""Raising a share past the partition is illegal, not a data point."""
saturated = CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=1_000),
users=10,
enabled=frozenset({"virtual_agent"}),
mix=DeflectionMix(bot_only_share=0.5, virtual_agent_share=0.5),
)
drivers = [r["driver"] for r in tornado(saturated, ["virtual_agent_share"])]
assert drivers == []
def test_invalid_delta_rejected(base: CalculatorInputs) -> None:
with pytest.raises(ValueError, match="delta"):
tornado(base, ["users"], delta=1.5)