Working-tree changes that had accumulated without being committed. The test updates matter most: tests/test_caller_gen.py was left behind when caller_gen started requiring voice and age in regulars_included, so the committed tree had a failing suite that only passed locally. - Caller dialog moves from Haiku 4.5 to Sonnet 4.6 (~$1/show to ~$3-4/show) - Grok pinned to x-ai/grok-4.3; grok-4, grok-4-fast and grok-4.1-fast were retired from OpenRouter, and llm.py swallows the 404 and returns empty text, so a retired id makes callers go silent with nothing in the logs - Devon's web_search now runs against SearXNG on the NAS - Assorted TTS, audio, news, cost tracker and control-panel changes - CLAUDE.md updated to match Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
import pytest
|
|
from backend.services.caller_gen import CallerIdentity, parse_batch_response
|
|
|
|
SAMPLE_JSON = """
|
|
{
|
|
"callers": [
|
|
{
|
|
"name": "Danny Ortega",
|
|
"age": 47,
|
|
"voice_suggestion": "Marcus",
|
|
"location": "Silver City, NM",
|
|
"identity": "A plumber who inherited his uncle's taxidermy shop...",
|
|
"situation": "He's been getting calls from people...",
|
|
"reason_calling": "Someone left a note in his mailbox tonight...",
|
|
"opening_line": "Luke, I need to ask you something weird.",
|
|
"secret_want": "Permission to just throw it all away",
|
|
"specific_details": ["the elk head in the basement", "the note said 'she forgot'", "his uncle's Rolodex"],
|
|
"emotional_register": "quietly unsettled, trying to sound casual"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
|
|
def test_parse_batch_response_returns_caller_list():
|
|
callers = parse_batch_response(SAMPLE_JSON)
|
|
assert len(callers) == 1
|
|
assert callers[0].name == "Danny Ortega"
|
|
assert callers[0].age == 47
|
|
assert "taxidermy" in callers[0].identity
|
|
assert len(callers[0].specific_details) == 3
|
|
|
|
|
|
def test_parse_batch_response_rejects_missing_fields():
|
|
bad = '{"callers": [{"name": "Jim"}]}'
|
|
with pytest.raises(ValueError, match="missing"):
|
|
parse_batch_response(bad)
|
|
|
|
|
|
FENCED_JSON = '''```json
|
|
{"callers": [{"name": "Terrence", "age": 61, "voice_suggestion": "Marcus", "location": "Tucumcari, NM", "identity": "Retired irrigation engineer", "situation": "Reading water bill", "reason_calling": "Found clause", "opening_line": "Luke.", "secret_want": "Vindication", "specific_details": ["a", "b"], "emotional_register": "intense"}]}
|
|
```'''
|
|
|
|
|
|
def test_parse_batch_response_strips_markdown_fences():
|
|
callers = parse_batch_response(FENCED_JSON)
|
|
assert len(callers) == 1
|
|
assert callers[0].name == "Terrence"
|
|
|
|
|
|
def test_resolve_voice_matches_exact():
|
|
from backend.services.caller_gen import resolve_voice
|
|
roster = ["Marcus", "Dennis", "Priya", "Edward"]
|
|
assert resolve_voice("Marcus", roster) == "Marcus"
|
|
|
|
|
|
def test_resolve_voice_case_insensitive():
|
|
from backend.services.caller_gen import resolve_voice
|
|
roster = ["Marcus", "Dennis"]
|
|
assert resolve_voice("marcus", roster) == "Marcus"
|
|
|
|
|
|
def test_resolve_voice_falls_back_when_no_match():
|
|
from backend.services.caller_gen import resolve_voice
|
|
roster = ["Marcus", "Dennis"]
|
|
# Random fallback, so hallucinated voices don't all collapse onto roster[0]
|
|
assert resolve_voice("Santiago", roster) in roster
|
|
|
|
|
|
def test_resolve_voice_fallback_spreads_across_roster():
|
|
from backend.services.caller_gen import resolve_voice
|
|
roster = ["Marcus", "Dennis", "Priya", "Edward"]
|
|
picked = {resolve_voice("Santiago", roster) for _ in range(60)}
|
|
assert len(picked) > 1, "fallback collapsed onto a single voice"
|
|
|
|
|
|
def test_resolve_voice_empty_suggestion_falls_back():
|
|
from backend.services.caller_gen import resolve_voice
|
|
assert resolve_voice("", ["Marcus"]) == "Marcus"
|
|
|
|
|
|
def test_build_batch_prompt_includes_context():
|
|
from backend.services.caller_gen import build_batch_prompt
|
|
ctx = {
|
|
"date": "Saturday, April 5, 2026",
|
|
"weather": "cool desert night, 48°F",
|
|
"headlines": ["New Mexico legislature approves water bill"],
|
|
"recent_caller_summaries": ["Jerry called about his neighbor's goat"],
|
|
"regulars_included": [],
|
|
"caller_count": 12,
|
|
"voice_roster": ["Marcus", "Dennis", "Priya"],
|
|
}
|
|
prompt = build_batch_prompt(ctx)
|
|
assert "Saturday, April 5, 2026" in prompt
|
|
assert "water bill" in prompt
|
|
assert "Jerry called about his neighbor's goat" in prompt
|
|
assert "12 callers" in prompt
|
|
assert "Marcus" in prompt # voice roster listed
|
|
assert "Stern" in prompt
|
|
assert "Coast to Coast" in prompt
|
|
assert "Loveline" in prompt
|
|
assert "Delilah" in prompt
|
|
assert "Opie and Anthony" in prompt
|
|
|
|
|
|
def test_build_batch_prompt_includes_silas_lore_when_present():
|
|
from backend.services.caller_gen import build_batch_prompt
|
|
ctx = {
|
|
"date": "...",
|
|
"weather": "...",
|
|
"headlines": [],
|
|
"recent_caller_summaries": [],
|
|
"regulars_included": [{"name": "Silas", "voice": "Sebastian", "age": 52, "lore": "Silas leads a small desert cult...", "arc_state": "seeking new members"}],
|
|
"caller_count": 12,
|
|
"voice_roster": ["Marcus"],
|
|
}
|
|
prompt = build_batch_prompt(ctx)
|
|
assert "Silas" in prompt
|
|
assert "desert cult" in prompt
|
|
assert "seeking new members" in prompt
|
|
assert "DO NOT alter his voice, personality, or core traits" in prompt
|