111 lines
3.9 KiB
Python
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)
|