Mercury Genesys Token Calculator
This commit is contained in:
206
calculators/Genesys_Token_Calculator/tests/test_usage.py
Normal file
206
calculators/Genesys_Token_Calculator/tests/test_usage.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""Volumes → tokens, with the three published rules that trip the naive math.
|
||||
|
||||
Every number below was computed by hand first, then pinned.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from genesyscalc.meters import LicenceModel, Tier
|
||||
from genesyscalc.usage import (
|
||||
DeflectionMix,
|
||||
VoiceBotUsage,
|
||||
Volumes,
|
||||
apply_copilot_covers_summary,
|
||||
subscription_tokens,
|
||||
tier_interactions,
|
||||
virtual_agent_tokens,
|
||||
voice_bot_billable_minutes,
|
||||
voice_bot_roundup_uplift,
|
||||
voice_bot_tokens,
|
||||
)
|
||||
|
||||
# ── 1 · the 15-second per-call round-up ──────────────────────────────────
|
||||
|
||||
|
||||
def test_voice_bot_15_second_roundup() -> None:
|
||||
"""1,000 calls at a 40s average.
|
||||
|
||||
ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month.
|
||||
The naive 40/60 × 1,000 = 666.67 min understates by 12.5%.
|
||||
"""
|
||||
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
|
||||
assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
|
||||
assert voice_bot_tokens(usage) == pytest.approx(750.0 / 17.0)
|
||||
assert voice_bot_tokens(usage) == pytest.approx(44.117647, rel=1e-6)
|
||||
|
||||
|
||||
def test_roundup_uplift_is_reportable() -> None:
|
||||
"""The stage-facing number: 12.5% more than the raw duration implies."""
|
||||
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
|
||||
assert voice_bot_roundup_uplift(usage) == pytest.approx(0.125)
|
||||
|
||||
|
||||
def test_roundup_is_exact_on_increment_boundaries() -> None:
|
||||
"""45s is exactly three increments — no inflation, uplift 0."""
|
||||
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=45)
|
||||
assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
|
||||
assert voice_bot_roundup_uplift(usage) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_roundup_penalises_short_calls_hardest() -> None:
|
||||
"""A 5-second bot greeting bills as 15 seconds — 3× its duration.
|
||||
|
||||
1,000 calls × 5s = 83.33 raw minutes, billed as 250.0.
|
||||
"""
|
||||
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=5)
|
||||
assert voice_bot_billable_minutes(usage) == pytest.approx(250.0)
|
||||
assert voice_bot_roundup_uplift(usage) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_zero_bot_volume_is_free_and_does_not_divide_by_zero() -> None:
|
||||
usage = VoiceBotUsage(calls_per_month=0, avg_bot_seconds_per_call=0)
|
||||
assert voice_bot_billable_minutes(usage) == 0.0
|
||||
assert voice_bot_roundup_uplift(usage) == 0.0
|
||||
assert voice_bot_tokens(usage) == 0.0
|
||||
|
||||
|
||||
def test_negative_bot_usage_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="calls_per_month"):
|
||||
VoiceBotUsage(calls_per_month=-1, avg_bot_seconds_per_call=30)
|
||||
|
||||
|
||||
# ── 2 · the highest-tier rule as a partition ─────────────────────────────
|
||||
|
||||
|
||||
def test_tier_shares_must_partition() -> None:
|
||||
"""Shares summing past 1.0 would bill some interaction twice."""
|
||||
with pytest.raises(ValueError, match="sum to at most 1.0"):
|
||||
DeflectionMix(
|
||||
bot_only_share=0.5, virtual_agent_share=0.4, agentic_va_share=0.3
|
||||
)
|
||||
|
||||
|
||||
def test_tier_share_bounds() -> None:
|
||||
with pytest.raises(ValueError, match="must be in"):
|
||||
DeflectionMix(virtual_agent_share=1.4)
|
||||
|
||||
|
||||
def test_agent_handled_share_is_the_remainder() -> None:
|
||||
mix = DeflectionMix(
|
||||
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
|
||||
)
|
||||
assert mix.total_deflected_share == pytest.approx(0.50)
|
||||
assert mix.agent_handled_share == pytest.approx(0.50)
|
||||
|
||||
|
||||
def test_highest_tier_charges_each_interaction_once() -> None:
|
||||
"""100,000 interactions: 30% bot, 15% VA, 5% agentic VA.
|
||||
|
||||
VA = 100,000 × 0.15 × 0.5 tokens = 7,500
|
||||
AVA = 100,000 × 0.05 × 1.2 tokens = 6,000
|
||||
------
|
||||
13,500
|
||||
|
||||
Applying every tier to the whole volume — the additive reading — would
|
||||
give 50,000 + 120,000 = far more, billing the same interaction at
|
||||
several tiers at once.
|
||||
"""
|
||||
mix = DeflectionMix(
|
||||
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
|
||||
)
|
||||
tokens = virtual_agent_tokens(100_000, mix)
|
||||
assert tokens["virtual_agent"] == pytest.approx(7_500.0)
|
||||
assert tokens["agentic_virtual_agent"] == pytest.approx(6_000.0)
|
||||
assert sum(tokens.values()) == pytest.approx(13_500.0)
|
||||
|
||||
|
||||
def test_tier_interactions_never_exceed_the_volume() -> None:
|
||||
mix = DeflectionMix(
|
||||
bot_only_share=0.40, virtual_agent_share=0.35, agentic_va_share=0.25
|
||||
)
|
||||
split = tier_interactions(100_000, mix)
|
||||
assert sum(split.values()) == pytest.approx(100_000.0)
|
||||
assert split[Tier.BOT] == pytest.approx(40_000.0)
|
||||
|
||||
|
||||
def test_bot_tier_is_not_priced_per_interaction() -> None:
|
||||
"""Voice bots bill in minutes with a round-up, so they are absent here."""
|
||||
mix = DeflectionMix(bot_only_share=1.0)
|
||||
assert virtual_agent_tokens(100_000, mix) == {
|
||||
"virtual_agent": 0.0,
|
||||
"agentic_virtual_agent": 0.0,
|
||||
}
|
||||
|
||||
|
||||
# ── 3 · Copilot covers Supervisor summaries ──────────────────────────────
|
||||
|
||||
|
||||
def test_summary_billed_when_copilot_off() -> None:
|
||||
usage = apply_copilot_covers_summary(
|
||||
{"ai_summary_and_insights": 120_000.0}, copilot_enabled=False
|
||||
)
|
||||
assert usage.get("ai_summary_and_insights") == pytest.approx(120_000.0)
|
||||
assert usage.notes == ()
|
||||
|
||||
|
||||
def test_copilot_covers_summary_zeroes_the_line() -> None:
|
||||
"""The published exclusion, enforced — and reported, not silent."""
|
||||
usage = apply_copilot_covers_summary(
|
||||
{"ai_summary_and_insights": 120_000.0}, copilot_enabled=True
|
||||
)
|
||||
assert usage.get("ai_summary_and_insights") == 0.0
|
||||
assert len(usage.notes) == 1
|
||||
assert "Agent Copilot is enabled" in usage.notes[0]
|
||||
|
||||
|
||||
def test_copilot_exclusion_leaves_other_meters_alone() -> None:
|
||||
usage = apply_copilot_covers_summary(
|
||||
{"ai_summary_and_insights": 100.0, "ai_scoring": 500.0}, copilot_enabled=True
|
||||
)
|
||||
assert usage.get("ai_scoring") == pytest.approx(500.0)
|
||||
|
||||
|
||||
def test_copilot_exclusion_is_quiet_when_there_is_nothing_to_zero() -> None:
|
||||
usage = apply_copilot_covers_summary(
|
||||
{"ai_summary_and_insights": 0.0}, copilot_enabled=True
|
||||
)
|
||||
assert usage.notes == ()
|
||||
|
||||
|
||||
# ── subscription tokens ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_per_user_tokens_are_exact_and_licence_sensitive() -> None:
|
||||
"""500 users × 40 = 20,000 named; × 60 = 30,000 concurrent."""
|
||||
keys = frozenset({"agent_copilot_named"})
|
||||
assert subscription_tokens(500, keys, LicenceModel.NAMED) == {
|
||||
"agent_copilot_named": 20_000.0
|
||||
}
|
||||
assert subscription_tokens(500, keys, LicenceModel.CONCURRENT) == {
|
||||
"agent_copilot_named": 30_000.0
|
||||
}
|
||||
|
||||
|
||||
def test_subscription_rejects_a_consumption_meter() -> None:
|
||||
with pytest.raises(ValueError, match="not a per-user meter"):
|
||||
subscription_tokens(10, frozenset({"ai_scoring"}), LicenceModel.NAMED)
|
||||
|
||||
|
||||
# ── volumes ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_volume_totals() -> None:
|
||||
v = Volumes(
|
||||
voice_inbound_monthly=100_000,
|
||||
voice_outbound_monthly=20_000,
|
||||
digital_sessions_monthly=8_000,
|
||||
email_monthly=12_000,
|
||||
)
|
||||
assert v.voice_total_monthly == pytest.approx(120_000.0)
|
||||
assert v.all_interactions_monthly == pytest.approx(140_000.0)
|
||||
|
||||
|
||||
def test_negative_volume_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="voice_inbound_monthly"):
|
||||
Volumes(voice_inbound_monthly=-1)
|
||||
Reference in New Issue
Block a user