Files

207 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)